有相当一部分来自shader圣经
Base of CG Concepts
Tangent, Normal and Binormal
N:法线(Normal, N)垂直于表面
T:切线(Tangent, T)与U方向同向
B:副切线(BiTangent, B)与V同向
BiTangent和BiNormal是一个东西,一般在shader中无法直接获取,需要去计算(三向量是两两正交的,其计算方式是显然的)
获取Bitangent的意义,最主要的是配合法线贴图使用,即法线贴图的采样信息左乘TBN矩阵得到世界空间法线
sampler2D _NormalMap;
struct Input
{float2 uv_MainTex;float2 uv_NormalMap;float3 worldNormal;float4 tangent; // Tangent vector
};// Surface shader function
void surf(Input IN, inout SurfaceOutput o)
{// Sample the normal mapfloat3 normalMap = tex2D(_NormalMap, IN.uv_NormalMap).xyz * 2.0 - 1.0;// Get the normal, tangent, and bitangentfloat3 N = normalize(IN.worldNormal);float3 T = normalize(IN.tangent.xyz);float3 B = cross(N, T) * IN.tangent.w;// Create TBN matrixfloat3x3 TBN = float3x3(T, B, N);// Transform normal from tangent space to world spacefloat3 worldNormal = normalize(mul(TBN, normalMap));// Assign the transformed normal to the outputo.Normal = worldNormal;
}
- × 2.0 − 1.0 ×2.0-1.0 ×2.0−1.0是把采样得到的rgb通道的值从 [ 0 , 1 ] [0,1] [0,1]映射到 [ − 1 , 1 ] [-1,1] [−1,1],这是法线向量各通道的范围
IN.tangent.w
是因为IN.tangent.xyz
表示切线向量 T 的方向分量,但是向量叉乘得到的新向量只是说垂直,方向是不确定的,所以Unity把副切线 B 的方向分量放在了IN.tangent.w
,用来确定副切线的方向。- 关于
mul(TBN, normalMap)
:当一个向量左乘一个正交基(orthonormal basis),其几何意义是将这个向量从原空间(切线空间)转换到与正交基所定义的新坐标系(世界空间)中,以便与光照模型和其他世界空间中的几何信息进行正确的交互 - 切线空间是针对每个顶点而言的,但是
即:TBN*采样法线方向(切线空间)=世界坐标法线方向
切线空间 | 纹理空间 | |
---|---|---|
维度 | 3D | 2D |
维度构成 | NTB | UV |
不太严谨的说,切线空间是带有法线信息的UV空间
Render mode
Forward Rendering | Deferred Rendering | |
---|---|---|
光照计算时机 | 光照计算直接在对象渲染时进行,光照计算分散在对象渲染的多个阶段 | 光照在全局的像素级别进行一次性计算 |
光源处理 | 每个对象处理其可见的所有光源,光源数量较多时效率低 | 每个对象处理其可见的所有光源,光源数量较多时效率低 |
维度构成 | 直接支持复杂的透明处理 | 透明物体需要单独处理(通常需要额外的前向渲染通道) |
性能 | 光源较少时性能好,但随着光源增加性能下降 | 适合光源数量多的场景,在处理大量光源时性能较好 |
二者的主要区别在于光照计算的时机和方式不同,延迟渲染的核心思想是将光照计算延迟到屏幕空间进行,从而提高渲染效率。然而在延迟渲染的流程中难以确定透明物体的前后顺序,而透明物体的颜色取决于它后面的物体,关于改进方法我直接问的AI:
-
正向渲染透明物体: 这是最简单直接的方法,将不透明物体使用延迟渲染,而透明物体则使用传统的正向渲染。
- 优点: 易于实现
- 缺点: 失去了延迟渲染的效率优势,尤其是在场景中存在大量灯光和透明物体的情况下。
-
排序透明物体: 对透明物体进行排序,然后从后到前进行渲染,以便正确混合颜色。
- 优点: 能够获得相对准确的透明效果
- 缺点: 排序操作本身会带来一定的性能开销, 而且无法处理相交的透明物体。
-
深度剥离: 将场景渲染多次,每次都只渲染一定深度范围内的透明物体,然后将结果混合在一起。
- 优点: 能够处理复杂的透明物体,例如相交的透明物体
- 缺点: 需要多次渲染场景,性能开销较大。
-
Order Independent Transparency (OIT): 这是一个比较复杂的解决方案,它通过在渲染过程中存储额外的信息(例如深度和颜色),来实现与顺序无关的透明效果。
- 优点: 能够获得高质量的透明效果,并且不依赖于渲染顺序
- 缺点: 实现复杂,需要额外的内存和计算资源。
关于如何从shader代码判断其渲染模式:
在使用CGPROGRAM
开启代码块后使用#pragma
指令,不过顶点着色器和片元着色器是无需指定的,这俩着色器和渲染路径无关
/* 默认使用前向渲染 */
CGPROGRAM
#pragma surface surf Standard
ENDCG/* 延迟渲染 */
CGPROGRAM
#pragma surface surf Standard deferred
// 着色器代码
ENDCG
Render Stage and DC
在渲染之前,图形引擎会进行一系列步骤(阶段)来准备和更新要渲染的内容,然后将这些内容通过Draw call提交给GPU进行实际渲染。
一般包含这几方面:
- 更新场景状态:根据输入、脚本、动画更新场景状态
- 准备渲染数据:更新相机的视图矩阵、投影矩阵等;剔除(视锥剔除(Frustum Culling)和遮挡剔除(Occlusion Culling));计算光源影响,更新灯光矩阵;批次处理
- 提交DC
- 几何处理、光栅化、像素处理
After DC: Pipeline
- 几何处理阶段
- 光栅化
- 像素处理阶段
几何处理主要就是顶点着色器做的一些事情,包括计算顶点位置、对位置做一些处理,映射到不同的坐标系以用以计算不同的东西。投影个和裁剪是在顶点着色器阶段完成的。
顶点着色器主要的输出是经过变换后的顶点位置(Vertex Position), 以及一些会传递给片元着色器(Fragment Shader)插值变量。
其中,顶点位置是裁剪空间(Clip Space)下的
光栅化则是把连续的数学模型离散化,三角形面片经过光栅化生成了一组称之为“片元”(或者片段)的像素
像素处理阶段会计算每个像素最终的颜色,并通过深度测试(Depth Testing)和模板测试(Stencil Testing)来决定他们是否显示。片元着色器只负责计算每个像素的最终颜色,它并不能决定这个像素最终是否被显示。
关于像素是否显示,这里要和裁剪做区分,裁剪是光栅化之前的,他只是针对顶点进行检测和去除,但是视口内那些有重叠的并不会丢弃(很显然一个例子是透明/半透明的物体遮挡了它后面的物体,但是后面的物体也能被显示一部分)
模版测试在深度测试之前,因为官方文档有这样一句话:如果模板测试通过,则 GPU 会执行深度测试。
在片元着色器计算出像素颜色之后要进行逐片元操作,主要有如下内容:
- 模版测试
- 深度测试
- alpha测试
- alpha混合
哎,这个顺序不一定的,尤其是SRP
Early-Z会导致深度测试提前(造成结果混乱)什么是Early-Z见后面
Rendering Path and Rendering Pipeline
管线可以配置不同的路径,路径则侧重于光照和阴影的计算方式,它是渲染管线中的一部分。渲染路径影响了渲染管线中的特定阶段(主要是光照处理),而渲染管线则定义了整个渲染流程。
从CPU提交DC到接受到渲染好的图像之间的过程都是由渲染管线处理的,不过在一帧内,CPU可以提交多个绘制调用,每个调用都会经历整个渲染管线的处理流程。
渲染路径一般包括:
- forward rendering(Unity默认是这个)
- deferred shading
- legacy deferred
- legacy vertex lit
URP只能前向渲染,HDRP前向渲染和延迟着色都可以
有没有别的渲染路径?有,比如说:
Clustered Rendering(集群渲染)使用空间分割技术(例如3D网格或八叉树)将光源和像素按照位置或视锥体(frustum)区域分组,以便更有效地管理和计算光照。这种方法可以提高渲染效率,特别是在大型场景和多光源情况下。
Tiled Rendering(瓦片渲染)是一种基于硬件的优化技术,将屏幕分割成多个小矩形区域(瓦片),并在每个瓦片上执行光照和渲染计算。这种方法可以更有效地利用现代图形处理单元(GPU)的并行能力,减少不必要的计算和内存访问,从而提高性能和能效。用过Blender的Cycles渲染器的对这个大抵是不陌生。
forward rendering
虽然说一个Shader Pass数量没有一个固定的上限,它取决于具体的渲染管线实现和硬件能力。但是前向渲染通常存在两种不同的渲染通道(Rendering Passes),它们在着色器中的使用方式有所不同:
-
基础通道(Base Pass):
- 基础通道是主要的渲染过程,用于绘制场景中的主要几何体和其基本材质属性。这包括对物体的基本颜色、法线、光照等进行计算和输出到屏幕。
- 在基础通道中,通常会执行一些基本的光照计算,如漫反射、环境光照等,以及处理物体的表面细节。
-
附加通道(Additional Passes):
- 附加通道则是额外的渲染过程,用于处理额外的光照效果、特殊效果或者后期处理。这些通道可以在基础通道之后执行,通常用于实现高级效果,如镜面反射、抗锯齿、全局光照、屏幕空间环境遮挡(SSAO)等。
- 附加通道的使用可以根据需要进行多次,每个通道可以执行不同的计算或效果,从而实现更复杂的渲染。
shader圣经说:光照探针、全局照明和环境照明(天光)也是在Base Pass进行的
Pass增加意味着额外性能开销
deferred shading
延迟着色确保只有一个光照Pass来计算场景中的每个光源
Occlusion Culling & Clipping
简单来说,遮挡检测像是 “看不着就别画”,而裁剪像是 “一个位置画得太多就别画那些被覆盖的”。
遮挡剔除 (Occlusion Culling)
- 作用: 识别并剔除完全被其他物体遮挡的物体,避免它们进行光栅化和着色。
- 原理: 通过判断物体是否完全在其他物体后面,来确定它是否可见。
- 时机: 渲染之前
- 实现方式: 可以使用硬件或软件进行实现。
- 目的: 减少不必要的渲染工作,提高渲染效率。
裁剪 (Clipping)
- 作用: 将超出视锥体范围的几何体部分移除,只保留可见部分。
- 原理: 通过判断几何体是否位于视锥体内部,并进行相应的切割和删除。
- 时机: 在渲染管线的几何阶段中,通常在顶点着色器之后进行。
- 实现方式: 主要是通过硬件实现。
- 目的: 避免渲染不可见的部分,提高渲染效率。
总结:
- 遮挡检测剔除的是完全不可见的物体,而裁剪剔除的是超出视锥体范围的部分。
- 遮挡检测的判断依据是物体之间的遮挡关系,而裁剪的判断依据是物体是否位于视锥体内部。
- 遮挡检测可以在软件或硬件上实现,而裁剪通常由硬件完成。
About Z
Depth Testing
像素离相机越近,Z-Buffer 值越低。每当要把一个像素写入缓冲区时就会比较它的z值和欲覆盖像素的Z-Buffer 值,若待写入的小则覆盖。
利用Z-Buffer比较哪个像素离相机更近的过程就是深度测试。
Z value of a pixel
透视除法的计算是将裁剪空间坐标的 X、Y、Z 分量分别除以 W 分量。裁剪空间中的坐标经过透视除法后得到标准化设备坐标(Normalized Device Coordinates, NDC),其中 X 和 Y 分量在 -1 到 1 之间,Z 分量在 0 到 1 之间。
x n d c = x c w c y n d c = y c w c z n d c = z c w c x_{ndc} = \frac{x_c}{w_c}\\ y_{ndc} = \frac{y_c}{w_c}\\ z_{ndc} = \frac{z_c}{w_c} xndc=wcxcyndc=wcyczndc=wczc
Z-Fighting
当两个或多个物体在屏幕上非常接近时产生的可见性问题。
解决方法
- 增加相机的近平面(near plane)和远平面(far plane)之间的距离可以提高深度缓冲区的精度分配。
- 干脆使用更高精度的深度缓冲区
- 调整深度偏移(Depth Bias):通过给物体添加一个小的深度偏移量,使得它们在深度缓冲区中的值稍有不同,从而避免(ShaderLab的
Offset
指令) - 据说开启 MSAA 可以减少 Z-fighting 产生的视觉伪影,但这并不能从根本上解决问题,只是视觉上有所缓解。
- 修改几何体(解决不了问题就解决产生问题的…)
Z-Cull
Z-Cull(Z剔除)核心思想是通过在光栅化阶段之前丢弃那些在最终图像中不可见的几何体或片段,从而减少需要处理的像素数量。这样可以显著减少 GPU 的工作量,提高渲染效率。
Unity开发通常不需要显式地处理 Z-Cull,据说是预置了的。
主要流程:
- 深度预传递(Depth Pre-Pass):在实际的渲染过程之前,进行一次仅写入深度缓冲区的渲染。这一步主要用于确定场景中的深度信息,但不会计算颜色。深度预传递的结果用于在后续的渲染过程中进行深度测试和剔除。
- 深度测试(Depth Test):在每个片段被光栅化和着色之前,根据深度缓冲区中的信息进行深度测试。如果新片段的深度值表明它被现有的片段遮挡,则丢弃该片段,不进行着色计算(Early-Z)
Early-Z
Early-Z 通过在片元着色器之前剔除被遮挡的片元,减少了需要计算的片元数量(提升性能)
这是一张来自Unity官方文档的图(这是启用了Early - Z的)
对于透明物体,Early-Z 并不起作用,因为透明物体需要按照从远到近的顺序进行绘制,以确保正确的颜色混合。
Post Processing
后处理需要借助一个后处理材质实现,本质相当于哪来之前渲染好的一张图作为Texture输入处理。
如果片元着色器中包含会修改深度值的操作,Early-Z 可能无法生效,因为最终的深度值只有在片元着色器执行后才能确定。
OnRenderImage
OnRenderImage
是 MonoBehaviour
类的一个回调函数,用于在相机渲染完成后,将渲染结果进行处理。这个函数一般是注册到相机的事件中,典型用法如下:
void OnRenderImage(RenderTexture src, RenderTexture dest) {// 在这里进行渲染结果的处理,例如应用图像效果Graphics.Blit(src, dest, myMaterial); // myMaterial是后处理材质
}
Math
Homogeneous Coordinates
在三维空间中,我们通常用 (x, y, z)
来表示一个点的位置。然而,在计算机图形学中,我们使用四维齐次坐标 (x, y, z, w)
来表示点,其中 w
是一个非零标量。
- 欧几里得坐标空间使用三个数值 ( x , y , z ) (x, y, z) (x,y,z) 表示一个点的位置。
- 齐次坐标空间使用四个数值 ( x , y , z , w ) (x, y, z, w) (x,y,z,w),这里的 w w w是一个额外的分量,称为齐次坐标的齐次分量, w w w分量主要用于表示缩放和透视投影。
意义:
- 缩放: 当
w
不等于 1 时,它会影响x
,y
,z
的值,从而实现缩放效果。例如,当w
等于 2 时,点会被放大两倍;当w
等于 0.5 时,点会被缩小一半。 - 透视投影: 透视投影是模拟现实世界中物体随着距离变远而变小的现象。在透视投影中,
w
的值会随着点距离观察者的距离而变化。当w
趋近于 0 时,点会无限缩小,从而模拟远处的物体消失的效果。(即表示无穷远点) - 方向向量的表示:当 w = 0 w=0 w=0时, ( x , y , z , 0 ) (x,y,z,0) (x,y,z,0)表示一个方向向量,而不是一个点的位置。这种表示对于描述射线、光线的方向或者平行线的情况很有用。
当 w
等于 1 时,齐次坐标表示的点与原始的 3D 坐标完全一致。因此在许多情况下会将 w
设置为 1,例如在进行平移、旋转和缩放等操作时。
【坐标转换】:通过除以齐次分量 ( w ),可以将齐次坐标转换为欧几里得坐标:
( x , y , z , w ) → ( x w , y w , z w ) (x, y, z, w) \rightarrow \left( \frac{x}{w}, \frac{y}{w}, \frac{z}{w} \right) (x,y,z,w)→(wx,wy,wz)
关于w,AI补充一下:
除了缩放和透视投影,齐次坐标的第四个分量 w
还可以用于其他一些操作,例如:
- 平移: 虽然平移操作通常用矩阵来表示,但也可以通过调整
w
来实现。例如,将w
设置为 0,并将x
,y
,z
设为目标位置,就可以将点平移到目标位置。1 - 仿射变换: 仿射变换是一系列线性变换(旋转、缩放、剪切)和平移的组合。通过矩阵乘法和调整
w
,可以实现更复杂的仿射变换。2 - 投影: 除了透视投影,齐次坐标也可以用来表示其他类型的投影,例如正交投影。通过调整
w
和投影矩阵,可以实现不同的投影效果。3 - 齐次裁剪: 在图形渲染中,需要对超出屏幕范围的几何体进行裁剪。齐次坐标可以用于裁剪操作,通过比较
w
的值来判断点是否在视锥体内部。4
齐次裁剪是在顶点着色器之后,透视除法(除w)之前的操作
CSDN - 计算机图形学补充2:齐次空间裁剪(Homogeneous Space Clipping)
除w发生在顶点着色器之后
Unity Shader Intro
Language
尽量用HLSL,CG只在Built-in得到支持。而不管是Built-in还是URP还是HDRP都可以使用HLSL
.hlsl
文件连接到.shadergraph
的HLSLPROGRAM,就如同.cginc
文件链接到.shader
的CGPROGRAM
Shader types
- Standard Surface Shader. (内置了PBR模型,输出基础色、金属度和粗糙度什么的)
- Unlit Shader. (无光照,顾名思义要自己实现所有效果)
- Image Effect Shader. (实现屏幕后处理的,输入一般是相机的渲染出的纹理)
- Compute Shader.(用于高性能并行计算任务而非直接用于图形渲染)
- Ray Tracing Shader.
Standard Surface Shader只支持Built-In管线,在URP或者HDRP则需要Lit Shader
Material property drawer
Toggle
勾选框,类型一般是float或者int,string这种是不行的。默认值非0即默认勾选。
[Toggle] _Propertyname ("Display Name" , float) = 2
据观察,属性对应的特性必须是属性名称大写且后缀_ON
#pragma shader_feature _PROPERTYNAME_ON
Keyword Enum
下拉选框相当于TogglePlus
[KeywordEnum(DefaultState, State01, State02,State03)]
_Prop2 ("Prop2",float) = 0
第一个状态是默认的,最多9个,有重复的state的话后续的会被忽略
和特性的命名对应规则是这样的:大写+后缀大写State名
[KeywordEnum(Off, Red, Blue)]
_Options ("Color Options", Float) = 0#pragma multi_compile _OPTIONS_OFF _OPTIONS_RED _OPTIONS_BLUE
Enum
和KeywordEnum不太一样的地方是它可以指定每个枚举项的值
[Enum(Red,1,Blue,2)]
_Prop3 ("Prop3",float) = 0
可以在Pass外使用也可以在Pass内使用,在Pass内使用得声明变量。然后if对应值就可以,注意是if不是#if
Range
// 刻度条不均匀的
[PowerSlider(3.0)]
_PropertyName ("Display name", Range (0.01, 1)) = 0.08
// 只能变化整数
[IntRange]
_PropertyName ("Display name", Range (0, 255)) = 100
PowerSlider刻度条不均匀,就是x^a
次方的图像映射到range的上下范围那样
Header&Space
调界面格式的
注意一下,内容TextContent
不需要加双引号,也不能有中文
[Header(TextContent)]
About Shader
-
Tagex写在SubShader下就影响其下所有Pass,写在Pass只影响对应Pass。重复声明Pass级别的声明会覆盖SubShader级别的声明
-
未Batch的情况下,通常一个Mesh上的每个材质对应的Shader的每个Pass通常都分别会生成一个DC
但是存在一个Pass产生多个DC的情况
- SubShader的执行是要条件匹配才可以的
也就是说条件合适的话,一个Shader内也会有多个SubShader被执行
Render Order
场景中的物体绘制顺序一般是由远及近,可以通过在Tags
中指定Queue
属性来选渲染队列编号,较小编号的物体会优先进行绘制
// 可以这样写
"Queue" = "Geometry+1"
当然HDRP中划分跟这个不太一样,渲染队列被细分为Material Order和Render Order。
Material Order类似于Render Queue,使用Tags语义和"Queue"指定。
- Background (1000)
- Geometry (2000)
- AlphaTest (2450)
- Transparent (3000)
- Overlay (4000)
Render Order在材质的Inspector窗口中可以手动设置,它是在Material Order确定后,进一步精确控制渲染顺序的参数。
Render Type
语法:
Tags { "RenderType"="type"}
- Opaque(默认是这个)
- Transparent.
- TransparentCutout.
- Background.
- Overlay.
- TreeOpaque.
- TreeTransparentCutout.
- TreeBillboard.
- Grass.
- GrassBillboard
Merge Stage
想混合Queue不可以是Opaque,一般是Transparent
合并阶段发生在渲染管线的末尾,片元着色器之后。一般包含3步:
- 深度测试(Depth Testing): 确定当前片元是否可见,即是否在屏幕空间中最前面的物体上。
- 模板测试(Stencil Testing): 检查片元是否符合模板缓冲区的特定条件。
- 混合(Blending): 如果片元通过了深度测试和模板测试,它将与帧缓冲中的当前像素进行混合,从而影响最终像素的颜色和透明度。
混合默认是Blend Off,但是可以指定混合模式,语法如下:
Blend [SourceFactor] [DestinationFactor]
Blend的计算公式如下
B = SrcFactor * SrcValue [OP] DstFactor * DstValue.
- SrcValue(源像素值): 指的是当前正在被渲染的像素的颜色值。这个值可以由当前片元着色器计算得出,包括基本颜色、纹理采样结果等。
- DstValue(目标像素值): 指的是已经存在于帧缓冲区中的像素的颜色值。一般就是那个
SV_Target
,没启用Blend的时候SrcValue直接就覆盖了DstValue,但启用之后就得经过上面的计算公式再覆盖 - OP默认是
Add
,还可以是ReverseSubtract
、Subtract
、Min
、Max
- 混合可以在SubShader或者Pass指定(这也决定了其作用范围)
用法:
BlendOp Subtract
常用的因子有这些
Alpha通道的混合方式与RGB颜色通道的处理方式类似,但是在许多场景中,对Alpha通道的混合操作并不经常使用,所以这是独立进行的过程。而由于Alpha通道混合不总是必需的,所以不进行Alpha通道混合可以优化渲染目标的写入。
下拉菜单直接选择,这里定义了一些常见的混合模式:
Properties {[Enum(UnityEngine.Rendering.BlendMode)]_SrcBlend ("Source Factor", Float) = 1[Enum(UnityEngine.Rendering.BlendMode)]_DstBlend ("Destination Factor", Float) = 1
}
AlphaToMask
在SubShader或在Pass中使用
AlphaToMask On
AlphaToMask Off
内容:将 alpha 值转换为二进制遮罩,在处理片元时,alpha 值大于某个阈值的部分会被渲染,小于阈值的部分则被丢弃。
注意:
- 启用MSAA
- 较旧的显卡可能不支持(做好特殊处理)
- 使用Clip函数来裁剪
Shader "Custom/AlphaToMaskExample"
{Properties{_MainTex ("Texture", 2D) = "white" {}_AlphaThreshold ("Alpha Threshold", Range(0,1)) = 0.5}SubShader{Tags { "RenderType"="TransparentCutout" }Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma target 4.0 // AlphaToMask需要Shader Model 4.0或更高版本sampler2D _MainTex;float _AlphaThreshold;struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.uv;return o;}fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);// 使用 alpha 通道作为遮罩,比较 alpha 值与阈值clip(col.a - _AlphaThreshold);return col;}ENDCG// 启用 AlphaToMaskAlphaToMask On}}FallBack "Diffuse"
}
ColorMask
用于指定哪些颜色通道(RGBA中的0个到4个)能够被写入到颜色缓冲区。注意返回≠写入。返回的可以有四个通道,但是写入的只有指定的。
ColorMask R // 仅写入红色通道
ColorMask G // 仅写入绿色通道
ColorMask B // 仅写入蓝色通道
ColorMask A // 仅写入 alpha 通道
ColorMask RG // 仅写入红色和绿色通道
ColorMask RGB // 仅写入红色、绿色和蓝色通道
ColorMask 0 // 禁止所有颜色通道的写入
ColorMask RGBA // 允许所有颜色通道的写入
Cull, ZWrite and ZTest
这仨都可以用于SubShader或者Pass
- Cull(面剔除)用于确定是否要剔除掉某些三角形面
Cull Off
:不进行面剔除,渲染所有的三角形面。Cull Front
:剔除正面朝向相机的三角形面。Cull Back
:剔除背面朝向相机的三角形面(默认选项,注意默认不是Off)。
- ZWrite(深度写入)用于确定是否允许更新Z-Buffer(时机是深度测试后的像素深度值更新)
ZWrite On
:允许更新深度缓冲区,即根据深度测试的结果来更新每个像素的深度值。ZWrite Off
:禁止更新深度缓冲区,即不修改深度缓冲区的内容,适用于特定效果或后期处理阶段。
- ZTest(深度测试)用于设置深度测试的比较函数
ZTest Less
:如果当前像素的深度值小于深度缓冲区中的值,则允许写入。ZTest Greater
:如果当前像素的深度值大于深度缓冲区中的值,则允许写入。ZTest Equal
:如果当前像素的深度值等于深度缓冲区中的值,则允许写入。ZTest Always
:始终允许写入当前像素,无论深度测试的结果如何。ZTest Off
:禁用深度测试,始终允许写入当前像素,不进行深度比较。
同样UnityEngine.Rendering
也提供了类似的便捷:
Shader "InspectorPath/shaderName"
{Properties{[Enum(UnityEngine.Rendering.CullMode)]_Cull ("Cull", Float) = 0}SubShader{Cull [_Cull]}
}
// Cull Off时才有效
fixed4 frag (v2f i, bool face : SV_IsFrontFace) : SV_Target
{fixed4 colFront = tex2D(_FrontTexture, i.uv);fixed4 colBack = tex2D(_BackTexture, i.uv);
}
多一句嘴,SV_Target
是告诉编译器,函数 frag 的返回值要写入渲染目标。而face这里是指示读取,并非写入。语义在不同着色器并不都可用,即使可用,在不同着色器可能指示的使用方式不同(比如SV_IsFrontFace
可由几何着色器写入,并由像素着色器读取。)
其余语义可参见MSDN - 语义
谁需要ZWrite off
?透明物体和半透明效果一般是需要的,根据所需的视觉效果,后处理和粒子有可能需要。
为什么透明的物体需要ZWrite off
?因为透明物体写到输出缓冲区是混合不是覆盖。以下图为例,如果绘制顺序是A→C→B,且在绘制完C之后写入了深度缓冲区,那么B就不会被绘制。但是如果绘制完C之后不写入深度缓冲区,此时深度缓冲区的还是A的信息,所以B的Z值比A小,依旧可以绘制。
关于Ztest语义则指定了深度测试的比较逻辑。
算了,直接问AI复制来一份说明:
在 Unity 中,ZTest
(深度测试)控制着渲染管线如何比较新片段的深度值与当前深度缓冲区中的值。这种比较决定了是否绘制新片段。ZTest
有多个选项,每个选项都表示一种比较模式。
- Less(默认值):
- 描述:如果新片段的深度值小于当前深度缓冲区中的值,则通过测试。
- 用途:适用于大多数场景,确保更近的物体覆盖较远的物体。
- Greater:
- 描述:如果新片段的深度值大于当前深度缓冲区中的值,则通过测试。
- 用途:在某些特定效果中使用,如反转深度的场景。
- LEqual:
- 描述:如果新片段的深度值小于或等于当前深度缓冲区中的值,则通过测试。
- 用途:用于确保新的片段覆盖相同深度或更深的片段。
- GEqual:
- 描述:如果新片段的深度值大于或等于当前深度缓冲区中的值,则通过测试。
- 用途:用于特定的深度反转或覆盖效果。
- Equal:
- 描述:如果新片段的深度值等于当前深度缓冲区中的值,则通过测试。
- 用途:用于需要精确匹配深度的效果。
- NotEqual:
- 描述:如果新片段的深度值不等于当前深度缓冲区中的值,则通过测试。
- 用途:用于创建一些特殊的视觉效果。
- Always:
- 描述:新片段总是通过深度测试。
- 用途:用于忽略深度缓冲区,确保片段总是被绘制。
- Never:
- 描述:新片段从不通过深度测试。
- 用途:用于完全禁止片段绘制。
选择合适的 ZTest
选项取决于具体需求和渲染效果,以下是一些常见场景和建议:
- 常规不透明物体:使用
ZTest Less
或LEqual
,以确保更近的物体正确覆盖远处的物体。 - 深度反转效果:使用
ZTest Greater
或GEqual
,以实现特定的深度覆盖效果。 - 特殊视觉效果:使用
ZTest Equal
或NotEqual
,以精确控制深度匹配。 - 忽略深度测试:使用
ZTest Always
,以确保片段总是被绘制。
通过正确设置 ZTest
,可以实现各种复杂的渲染效果并优化渲染性能。
Stencil
模板缓冲区为帧缓冲区中的每个像素存储一个 8 位整数值。为给定像素执行片元着色器之前,GPU 可以将模板缓冲区中的当前值与给定参考值进行比较。这称为模板测试。如果模板测试通过,则 GPU 会执行深度测试。如果模板测试失败,则 GPU 会跳过对该像素的其余处理。这意味着可以使用模板缓冲区作为遮罩来告知 GPU 要绘制的像素以及要丢弃的像素。
Unity Documentation - ShaderLab 命令:模板
if ((StencilRef & StencilReadMask) [Comp] (StencilBufferValue & StencilReadMask))
{// Accept Pixel
}
else
{// Discard Pixel
}
&
是按位与[Comp]
是模板比较函数,如==
、!=
、<
、>
、<=
、>=
等。
正儿八经在Shader里是这么用的,Stencil
可以配置的其他的语义在上面的官方文档都能查到。
Stencil
{Ref 0xCC // 二进制: 11001100Comp equal // 比较操作是等于,即比较二者是否想等Pass replace // 通过测试的操作,这里是replace(替换)ReadMask 0xF0 // 二进制: 11110000WriteMask 0xFF // 设置模板读取和写入掩码为 0xFF(即 255),这意味着所有位都有效。
}
由此看来,模版缓冲区其实可以理解为8个状态位(Unity模版缓冲区每个像素存储一个 8 位整数值),只是给压缩到一个整数来表示了。然后通过StencilReadMask
来控制读取哪些位,示意如下:
StencilRef = 0b11001100; // 二进制表示,等于204
StencilReadMask = 0b11110000; // 二进制表示,等于240
StencilBufferValue = 0b10101010; //二进制表示,等于170StencilRef & StencilReadMask = 0b11001100 & 0b11110000 = 0b11000000
StencilBufferValue & StencilReadMask = 0b10101010 & 0b11110000 = 0b10100000
模版测试结合具体应用的例子:
// shader1
ColorMask 0
ZWrite Off
Stencil{Ref 2Comp Alwayspass Replace
}
// shader2
Stencil{Ref 2Comp NotEqualpass Keep
}
比如说shader1给到下图的平面(虽然看不见但是有线框,可以知道在哪),把他所在片元的模版缓冲区的值设为了2,并且由于Comp是Always,所以它所涵盖的像素对应的模版缓冲区值都是2,并且Zwrite Off
指明其颜色不写入颜色缓冲区,这导致它所在的位置啥也看不到
然后正方体设置shader2,它的模版值也是2,但是它使用“等于”进行比较,所以在模版值为2的地方是无法通过测试的,只有在不为2的地方才会Keep,即绘制出来。这导致被平面遮挡的地方是不会绘制的。所以就显示出后面的内容了,即那个球体。
Pass
通常情况下第一个Pass是默认的。在Pass中使用CGPROGRAM
&ENDCG
或者HLSLPROGRAM
&ENDHLSL
来包含相应语言的着色器代码块。
代码块的头部往往会声明一些和Properties对应的变量。不过属性有很多种,光浮点数就有3种:
- float:32位高精度,通常使用在计算世界空间位置、纹理映射以及涉及复杂函数(例如三角函数或指数函数)的计算。
- half:16位中精度。常用在计算较小向量、方向、模型空间位置和高动态范围(HDR)色彩。
- fixed:11位低精度。用于计算一些简单的操作(例如基本的颜色存储)。
这些浮点数都可以后缀n或者nxn
( n ∈ [ 2 , 4 ] n \in [2,4] n∈[2,4]),nxn
是矩阵
// 4*4矩阵
fixed4x4 name = fixed4x4
( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0
);
其他类型
- Sampler:纹理的采样状态,在该数据类型中可以存储纹理及其 UV 坐标;
Sampler3D
,SamplerCUBE
也是
其次是声明着色器的函数的指令
// 允许将名为 vert 的函数作为顶点着色器编译到 GPU
#pragma vertex vert
// 允许名为frag的函数作为片元着色器编译到GPU
#pragma fragment frag#pragma multi_compile_fog
.cginc
的意思是Cg include,例如UnityCG.cginc
包含了一些常见的预设(常量、函数),比如:
UNITY_PI
: π π π常量UnityObjectToClipPos(inputVertex)
,作用是:用于将对象空间的顶点位置转换为裁剪空间中的位置(顶点着色器输出的顶点坐标是裁剪空间的)
然后我们使用一些DTO作为顶点着色器和片元着色器的输入,默认为开发者创建的是appdata
和 v2f
,显然v2f
是vertex to fragment的缩写。
以结构体作为DTO,其中包含了很多着色器要用到的数据,例如:、
struct appdata{float4 vertPos : POSITION;float2 texCoord : TEXCOORD0;float3 normal : NORMAL0;float3 tangent : TANGENT0;float3 vertColor: COLOR0;
}
冒号后面的是语义,语义允许程序单独访问顶点位置、切线、法线、UV 坐标和颜色等属性。说白了就是指导一个变量输入输出。比如说作为输入的语义它就会从引擎中读取相关的信息(例如模型某个点的位置),作为输出语义则会输出到某个缓冲区(用于后续处理)
常见的语义有
- POSITION[n]:其中
0
表示主要的顶点位置信息,通常是顶点的空间坐标 - TEXCOORD[n]:用于传递顶点的纹理坐标信息
- TANGENT[n].
- NORMAL[n].
- COLOR[n]:顶点色
SV_
则是系统值的意思
Other
URP compatible
UnityCg.cginc
的一些东西要换成Core.hlsl
,不过要写全Package/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl
CGPROGRAM
/ENDCG
块分别由HLSLPROGRAM
和ENDHLSL
代替- 而
UnityObjectToClipPos
要换成TransformObjectToHClip
(后者更高效) - HLSL是没
fixedN
的 - 必须包含
Core.hlsl
(它取代了UnityCg.cginc
,这意味着很多UnityCg.cginc
的函数也要换成前者的) Tags
指定"RenderPipeline" = "UniversialRenderPipeline"
Stages of semantics
语义具有阶段性,我意思是,例如:顶点输出的结构体无法使用NORMAL
和TANGENT
语义
unity_ObjectToWorld
unity_ObjectToWorld
是把物体从它的本地坐标系变换到世界坐标系,而MVP矩阵是变到裁剪空间。
// MVP使用方式
float4 clipPosition = mul(ProjectionMatrix, mul(ViewMatrix, mul(ModelMatrix, float4(localPosition, 1.0))));
float4 clipPosition = mul(UNITY_MATRIX_MVP, float4(localPosition, 1.0));
由于M矩阵就是把物体变到世界坐标系的,所以世界坐标系的物体要变到裁剪空间就不是左乘MVP
了,而是左乘VP,即:
float4 clipPosition = mul(UNITY_MATRIX_VP, worldPosition);
Reference
书籍:《The Unity Shaders Bible A linear shader explanation from beginner to advanced. Improve your game graphics with Unity and… (Fabrizio Espнndola, Pablo Yeber etc.)》
博客园 - 切线空间详解(我感觉这里说的不全对,细节上有点问题的)
稀土掘金 - 图形学的数学基础(三十四):TBN空间与TBN矩阵
LearnOpenGL CN - 法线贴图
Medium - Generating Perfect Normal Maps for Unity (and Other Programs)
Unity Documentation - Mesh Data
Reverse Depth Buffer in OpenGL