Unity Build-in 渲染管线
Build-in渲染管线的基本知识,同时包含部分坐标转换的知识
注:此基于内置渲染管线(build-in),使用CG语言
Unity Shader
关于坐标
注意 clip space与NDC之间的区别容易混淆的Clip Space vs NDC,透视除法
- 注意三个坐标之间的区别:Clip Space, NDC, Screen Space
- Cilp Space: 是顶点着色器输出的坐标, 是将view space的坐标进行裁剪,并投影变换后的坐标,其范围为[-w, w]的1立方体。
- NDC: 是将Clip Space的坐标应用透视除法并进行归一化后的坐标,(Direct中)其范围为x,y[-1,1],z轴的范围为[0,1]的立方体空间
- Screen Space: 是将NDC坐标映射到屏幕坐标的坐标,其范围为屏幕的宽高,一般为[0, width]和[0, height]
- (Vertex Shader) => Clip Space => (透视除法) => NDC => (视口变换) => Screen Space => (Fragment Shader)
- 注意:在URP提供的GetVertexPositionInputs函数中,positionNDC与实际的NDC坐标有所不同,其z轴的范围为[-w, w],而不是[-1,1],实际只对x,y进行了一定缩放将其从[-w,w]缩放到[0,w]
1 2 3 4 5 6 7 8 9 10 11 12 13
VertexPositionInputs GetVertexPositionInputs(float3 positionOS) { VertexPositionInputs input; input.positionWS = TransformObjectToWorld(positionOS); input.positionVS = TransformWorldToView(input.positionWS); input.positionCS = TransformWorldToHClip(input.positionWS); float4 ndc = input.positionCS * 0.5f; input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w; input.positionNDC.zw = input.positionCS.zw; return input; }
注意:Unity在生成深度图时使用的深度是非线性的高精度浮点,其实就是_CameraDepthTexture.r = input.positionNDC.z / input.positionNDC.w。可以使用
LinearEyeDepth(offsetDepth, _ZBufferParams)
将其转换为线性深度。- View Space -> Clip Space: \(\begin{aligned}\boldsymbol{p}_{clip}&=\boldsymbol{M}_{projection}\boldsymbol{p}_{view}\\&=\begin{bmatrix}\frac{\cot\frac{FOV}{2}}{Aspect}&0&0&0\\0&\cot\frac{FOV}{2}&0&0\\0&0&\frac{Far+Near}{Far-Near}&-\frac{2.Near\cdot Far}{Far-Near}\\0&0&-1&0\end{bmatrix}\begin{bmatrix}x\\y\\z\\1\end{bmatrix}\\&=\begin{bmatrix}\cot\frac{FOV}{2}\\x\frac{\cot\frac{FOV}{2}}{Aspect}\\y\cot\frac{FOV}{2}\\-z\frac{Far+Near}{Far-Near}-\frac{2.Near\cdot Far}{Far-Near}\\-z\end{bmatrix}\end{aligned}\)
其中,$FOV$为视野角度(视锥的全顶角),$Aspect$为宽高比,$Near$和$Far$分别为近裁剪面和远裁剪面的距离。在viewspace - > clip space过程中,x,y分量做了缩放,使其坐标从矩形范围映射到一正方形范围内,z分量做了缩放与平移,其表示的距离是非线性的,z越大,缩放越大。w分量为-z,用于透视除法。
- 关于Linear01Depth/LinearEyeDepth/rawDepth:
- LinearEyeDepth: 是将深度值(即深度图的数值)从非线性的[0,1]范围映射到线性的视角空间深度,对应positionCS.w
- Linear01Depth: 是将深度值(即深度图的数值)从非线性的[0,1]范围映射到线性的[0,1]范围,即把positionCS.w线性映射到[0,1]范围
- RawDepth:深度图的数值,即将NDC空间下的z值映射到[0,1]范围的值,对应positionCS.z/positionCS.w*0.5+0.5
- ref LinearEyeDepth和Linear01Depth-CSDN博客强烈推荐!!!
- Unity的Raw Depth / Linear01Depth / Linear Eye Depth区别 - 知乎 (zhihu.com)
- 相关函数可以参考[[URP]]
关于渲染流水线
这里是指前向渲染
ref : https://blog.csdn.net/qq_30100043/article/details/125883016
前向渲染流程
cpu上进行剔除和排序,剔除为相机的视椎体剔除,将相机视角外的模型剔除掉,不再渲染。遮挡剔除,将视椎体内不可见的模型剔除掉,不再渲染。排序为了保证性能,不透明为从近到远,减少不必要的渲染,半透明物体为了保证正确性,从远到近。
cpu上获取到所需的相关模型数据:顶点数据 贴图数据 还有材质属性,将数据提交给gpu,并调用gpu进行渲染,每调用渲染一次,为一次drawcall。
在gpu上,分为两部分,一部分是顶点着色器,另一部分是片元着色器。 在顶点着色器里,主要是位置变换,将顶点的位置,依次是 模型空间 世界空间 视线空间 裁剪空间 这个过程是 MVP矩阵变换。
在顶点着色器到片元着色器中间还有一个过程,这个过程中,将进行裁剪剔除,将不在屏幕中的片元裁剪到只在裁剪显示空间内的,显示范围为[-w, w],在这里得到的四维变量 xy是裁剪空间下的xy坐标,z是深度,w就是w,注意这里,因为获取屏幕uv也需要在这一步操作,而且不同平台的兼容的问题也需要从这一步开始出现,因为深度也就是z从这一步根据平台也不同。然后进行NDC(归一化设备坐标系),归一化操作,在unity里面,xy限制在0到w之间,而深度z,根据平台不同,则为-w到w或者0-w。NDC操作完成之后,将进行背面剔除,将一些无法看到图元(主要是三角形面,点和线不需要)给剔除掉。图元三角形的三个点如果是顺时针渲染的为背面,将被剔除,逆时针渲染的面为正面,将被渲染。
背面剔除完成以后,将要转换到屏幕空间(或者缓冲区空间),这里将根据渲染的宽高进行转换,转换完成后,将进行图元装配,然后进行光栅化,光栅化可以理解为获取到图元占用屏幕的像素(一般叫片元),占用多少像素,将调用多少次的片元着色器进行运算颜色。这个步骤是图元的所有的片元结果同步计算的。
片元着色器运算完成,就到了最后输出合并阶段。你计算完成后,也不一定能够给像素填充颜色,我们还需要进行多个测试,通过这些测试以后,才可以将颜色写入帧缓冲区。这些步骤分别是:
半透明测试 根据透明度,将多少透明度以下的颜色直接放弃写入。unity里面函数是clip,内部如果传入的值小于0,则不会写入颜色。 模板测试 根据之前设置的模板,对比,如果通过将继续下一步。 深度测试 对比帧缓冲区的深度,通过继续下一步。 混合测试 这一步主要针对于半透明物体的,当前的颜色和帧缓冲区的像素颜色按比例进行混合,得到最终颜色。 最后,完成后,可以顺利的将颜色写入到缓冲区。注意,缓冲区不单单只存储了像素的颜色,还有深度信息,以及模板信息。
结构
ShaderLab
用于统一各种render 接口的高级抽象接口, 书写为一文本语言
Shaderlab Property
说明:
结构总览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// name: TestShader
Shader "Custom/TestShader"
{
// 定义一系列属性,便于调节
Properties
{
Name {"display name", PropertyType} = DefaultValue
}
SubShader
{
[RenderSetup] // 可选, 设置渲染状态(深度测试/混合模式等)
[Tags] //可选, 设置标签块
Tags {"Queue" = "Transparent"} //e.g.
Pass{
//[Name]
Name "TestShader/MYPASSNAME"
//[Tags]
Tags {"LightMode" = "ForwardBase"} //e.g.
//[RenderSetup]
//other code
}// other passes
}// other subshaders
Fallback "name" // 当所有的SubShader 都无法使用时的最低级shader
}
参考链接
Unity ShaderLab 命令:ShaderLab:命令 - Unity 手册 Subshader Tags ref:ShaderLab:子着色器标签 (SubShader Tags) - Unity 手册
关于ShaderLab处理过程的一些理解
- 一个shader中可以有多个SubShader,每个SubShader中可以有多个Pass
- 当渲染一个物体时,Unity会遍历shader中的每个SubShader,找到第一个支持当前平台的SubShader,然后按照前后顺序执行其中的所有Pass,并将输出传递到同一个输出中(如ZBuffer, ColorBuffer等)。
- subshader和pass的Tags可以用来确定是否使用该subshader/pass(如LightMode = “ForwardBased”),以及在渲染队列中的位置(如Queue = “Transparent”,表示使用透明队列)
DirectX HLSL
在HLSL(High-Level Shading Language)中,语义(Semantics)是一种特殊的标记,用于告诉编译器某个变量的用途或者它应该如何被处理。
语义可以用于变量、结构体成员、函数参数和函数返回值。它们可以帮助编译器理解数据的用途,并进行相应的优化。例如,POSITION
语义告诉编译器该变量包含了顶点的位置信息,COLOR
语义表示该变量包含了颜色信息。
系统值语义(如SV_POSITION
和SV_Target
)是一种特殊的语义,它们在所有HLSL程序中都有特殊的含义。例如,SV_POSITION
表示顶点在屏幕空间中的位置,SV_Target
表示渲染目标(通常是一个像素的颜色)。
总的来说,语义在HLSL中起着非常重要的作用,它们帮助编译器理解和优化数据,确保数据在GPU中正确处理。
语义
reference:语义 - Win32 apps
Unity 应用阶段由unity传递给vertex着色器支持的语义
语义 | 描述 |
---|---|
POSITION | 模型空间中的顶点位置,通常是float4 类型 |
NORMAL | 顶点法线,通常是float3 类型 |
TANGENT | 顶点切线,通常是float4 类型 |
TEXCOORDn,如 TEXCOORD0 | 该顶点的纹理坐标,TEXCOORD0 表示第一组纹理坐标,依此类推,通常是 |
TEXCOORD1 | float2或 float4 类型 |
COLOR | 顶点颜色,通常是float4 类型 |
SV_VERTEXID: 顶点ID,从0开始递增,每个顶点都有一个唯一的ID; 在URP中,可以通过 float4 pos = GetFullScreenTriangleVertexPosition(input.vertexID); float2 uv = GetFullScreenTriangleTexCoord(input.vertexID);
获取顶点位置和纹理坐标
Unity 从vertex着色器传递给fragment着色器支持的语义
语义 | 描述 |
---|---|
SV POSTIION | 裁剪空间中的顶点坐标,结构体中必须包含一个用该距义修饰的变量。等同于 |
SV POSTIION | 我的空间中的顶点坐标,结构体中必须包含一个用该距又修饰的变量。等同于 DirectX 9 中的 POSTION, 但最好用 SV POSITION |
COLOR0 | 通常用于输出第一组顶点频色,但不是必需的 |
COLOR1 | 通常用于输出第二组顶点频色,但不是必需的 |
TEXCOORD0-TEXCOORD7 | 通常用于输出纹理坐标,但不是必需的 |
Unity 从fragment着色器输出支持的语义
语义 | 描述 |
---|---|
SV Target | 输出值将会存储到渲染目标(render target)中。等同于 DirectX 9 中的 COLOR,但最好使用 SV Target |
vertex/fragment 着色器
vertex 写在SubShader Pass 中, vertex着色器在几何处理阶段,一般用于传递位置, 法向顶点等信息。 着色器的输出通常是一个包含了各种顶点属性(如位置、颜色、纹理坐标等)的结构体。这些属性将被用于后续的图形管线阶段,如光栅化和片元着色。
在HLSL和Unity Shader中,顶点着色器的输出通常包含以下几个部分:
- 顶点在裁剪空间(Clip Space)中的位置,通常使用
SV_POSITION
语义标记。 - 顶点的颜色,如果有的话。
- 顶点的纹理坐标,如果有的话。
- 顶点的法线和切线,如果进行光照计算的话。
例如,一个顶点着色器的输出可能是这样的:
1 2 3 4 5 6 7 8
struct v2f { float4 pos : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; float3 normal : NORMAL; float3 tangent : TANGENT; };
在这个例子中,
v2f
结构体包含了顶点的位置、颜色、纹理坐标、法线和切线。这些属性将被传递到后续的图形管线阶段。- 顶点在裁剪空间(Clip Space)中的位置,通常使用
fragment 是对剪切空间(屏幕空间)的处理, 其输出一般是最终画面
Tips/Hints/坑
- vertex/fragment 着色器
一般都需要输入 - 输入输出建议使用struct, 且struct中需要补全个成员的语义,不然不支持
- 注意语义
- 常使用
#include "UnityCG.cginc"
- 当要传递多个坐标语义时, 可以使用语义
TEXCOORDn
,n为0-7的整数,表示第n组坐标 - 注意tangent为四位向量,其中tangent.w为切线的方向(副法向),通常为1或-1
- 注意颜色与法线的线性变换关系,颜色取值范围为
[0,1]
,法线取值范围为[-1,1]
, 故线性变换为normal = color*2-1
- 注意顶点着色器从应用阶段传递的顶点数据,一般是物体空间的数据,注意转换到世界空间或者切线空间
- 在创建半透明材质时,注意混合因子的设置,一般使用
Blend SrcAlpha OneMinusSrcAlpha
,即源颜色*源alpha + 目标颜色*(1-源alpha)
- 在多光源光照中,ambient总共只用计算一次
- frame debugger可以查看每一帧的渲染过程,对于调试很有帮助,The Frame Debugger window - Unity 手册
- 一般UV的坐标范围为[0,1],但在一些情况下,如立方体贴图,UV坐标可能为三维向量,不需要归一化处理
- vertex/fragment 着色器
补充:切线空间
ref:https://zhuanlan.zhihu.com/p/361417740
切线空间是一个局部坐标系,它的原点位于顶点的位置,x轴指向切线方向(Normal, N),y轴指向副法线方向(binomarl, B),z轴指向法线方向(tangent, T)。其变换矩阵互为转置矩阵(由于是正交矩阵,所以其逆矩阵等于其转置矩阵)
物体坐标->切线坐标:(L代表物体空间中坐标) \(\left.\left(\begin{array}{ccc}Tx&Ty&Tz\\Bx&By&Bz\\Nx&Ny&Nz\end{array}\right.\right)\cdot\left(\begin{array}{c}Lx\\Ly\\Ly\end{array}\right)\)
切线坐标 -> 世界坐标:TBN矩阵(注:也可以扩展到其它坐标变换中) \(\left.\left(\begin{array}{lll}worldTx&worldBx&worldNx\\worldTy&worldBy&worldNy\\worldTz&worldBz&worldNz\end{array}\right.\right)\cdot\left(\begin{array}{ll}tangentNx\\tangentNy\\tangentNz\end{array}\right)\) code:
1 2 3 4 5 6 7 8
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
切线空间:
目前使用过的内置函数/变量
一般Unity内置变量以
_
或unity_
或UNITY_
开头
refrence:
常用库:
1 2
#include "UnityCG.cginc" #include "Lighting.cginc" // 光照计算
常用变量:
1 2 3 4 5 6 7 8 9 10
UNITY_LIGHTMODEL_AMBIENT // 光照模型的环境光, 位于Lighting.cginc _LightColor0 // 第一个光源的颜色 _WorldSpaceLightPos0 // 第一个光源的世界空间位置 _WorldSapceCameraPos // 相机的世界空间位置 unity_ObjectToWorld// 物体空间到世界空间的变换矩阵 _LightTexture0 // 光照贴图, 可以使用宏.UNITY_ATTEN_CHANNEL通过光源坐标查找光照衰减 unity_WorldToLight // 世界空间到光源空间的变换矩阵
常用函数: Unity Shader内置函数列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间 UnityObjectToWorldNormal(v.normal); // 将法线从对象空间转换到世界空间, 注意为三维向量,注意转换后的法向未归一化! TRANSFORM_TEX(tex, name); // 将纹理坐标转换为纹理的uv坐标, tex.xy*name_ST.xy+name_ST.zw tex2D(name, uv); // 从纹理name中获取uv坐标处的颜色 texCUBE(name, uv); // 从立方体纹理name中获取uv坐标处的颜色,注意该uv为三维向量,但无需做归一化处理 UnpackNormal(normal); // 将法线从压缩格式转换为三维向量 ObjSpaceViewDir(v.vertex); // 物体空间的视线方向 ObjSpaceLightDir(v.vertex); // 物体空间的光照方向 UNITYWorldSpaceViewDir(world_pos); // 世界空间的视线方向 UNITYWorldSpaceLightDir(world_pos); // 世界空间的光照方向 ComputeScreenPos(pos); // 计算屏幕空间的坐标, pos为世界空间的坐标 ComputeGrabScreenPos(pos); // 计算捕捉屏幕空间的坐标 clip(floatx x); // HLSL内置函数, 如果x中有任一分量小于0, 则discard discard; // 丢弃当前片元, HLSL内置函数 reflect(i, n); // 计算i关于n的反射向量, i为入射向量(指向反射点), n为法线向量, HLSL内置函数 refract(i, n, eta); // 计算i关于n的折射向量, i为入射向量(指向折射点), n为法线向量, eta为当前介质和目标介质的折射率比值, HLSL内置函数 saturate(x); // 将x中的每个分量限制在[0,1]之间, HLSL内置函数 lerp(a, b, t); // 线性插值, a和b为两个向量, t为插值因子, HLSL内置函数 step(edge, x); // 如果x小于edge, 则返回0, 否则返回1, HLSL内置函数,常用于替代if语句 smoothstep(a, b, x); // 平滑插值,用于产生0~1之间的平滑过渡值,a和b为插值范围(在a和b之外的值将被截断) // 定义 float smoothstep(float a, float b, float x) { x = clamp((x - a) / (b- a), 0.0, 1.0); return x * x * (3 - 2 * x); } ddx(x_) // 计算x_对x的x方向的偏导数 ref:https://tyson-wu.github.io/blogs/2021/07/08/Ronja_Partial_Derivatives/ ddy(y_) // 计算y_对y的y方向的偏导数 fwidth(x_) // 计算x_的梯度
常用宏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
TANGENT_SPACE_ROTATION // 物体空间到切线空间的旋转矩阵(3*3) 为宏定义 USING_DIRECTIONAL_LIGHT // 宏定义, 判断是否使用平行光 POINT // 宏定义, 判断是否使用点光源 SPOT // 宏定义, 判断是否使用聚光灯 fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; // 通过光源坐标查找光照衰减 SHADOW_COORDS(i); // 用于声明一个阴影坐标, i为阴影贴图索引, 位于"AutoLight.cginc" #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; TRANSFER_SHADOW(o); // 用于计算阴影坐标, o为输出结构体, 位于"AutoLight.cginc",定义如下 #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos); SHADOW_ATTENUATION(i); // 用于计算阴影衰减, i为阴影贴图索引, 位于"AutoLight.cginc" #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) UNITY_LIGHT_ATTENUATION(); // 可以统一计算光照衰减和阴影衰减,即无需调用SHADOW_ATTENUATION(i)和UNITY_ATTEN_CHANNEL, 位于"AutoLight.cginc"
透明效果(对应第8章)
- 定义shader的渲染队列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
SubShader { Tags{"Queue" = "xxx"} Pass{ // other code } } SubShader { Tags{"Queue" = "Transparent"} // 使用透明队列 Pass{ ZWrite Off // 可选, 关闭深度写入 // other code } }
- Blend混合模式:
1
Unity Blend ref : [ShaderLab 命令:Blend - Unity 手册](https://docs.unity.cn/cn/2020.3/Manual/SL-Blend.html)
渲染路径
用于实现多灯光渲染,包含ForwardBase, ForwardAdd, Deferred, LegacyDeferredLighting, LegacyDeferredShading等; ref: 前向渲染路径 - Unity 手册 内置渲染管线中的渲染路径 - Unity 手册
阴影
Unity的阴影实现主要有两种方式:ShadowMap和ScreenSpaceShadow。
- unity中实现阴影的步骤:
- 在v2f结构体中声明阴影坐标,常用宏
SHADOW_COORDS(i)
,i为阴影贴图索引 - 在vertex shader中计算阴影坐标,常用宏
TRANSFER_SHADOW(o)
,o为输出结构体 - 在fragment shader中计算阴影衰减,常用宏
SHADOW_ATTENUATION(i)
,i为阴影贴图索引 - 在fragment shader中使用阴影衰减计算阴影效果
- 在v2f结构体中声明阴影坐标,常用宏
为获得阴影贴图,需要
LightMode = ShadowCaster
的Pass来实现,这一步一般由Fallback
指定的shader中的Pass来实现(即VertexLit中的Pass) 举例:1 2 3 4 5 6 7 8 9 10 11 12 13
Shader "xx" { //other code SubShader { //other code Pass { //other code } } Fallback "Reflective/VetexLit" }
高级纹理
菲涅耳(fresnel)
近似等式: \(F_{schlick}(\mathbf{v},\mathbf{n})=F_0+(1-F_0)(1-\mathbf{v}\cdot\mathbf{n})^5\) 或 \(F_{Empricial}(\mathbf{v},\mathbf{n})=max(0,min(1,bias+scale\times(1-\mathbf{v}\cdot\mathbf{n})^{power}))\)
镜面效果
这里所指的镜面效果是非光线追踪的,而是使用渲染纹理实现
思路:为镜面制作一个简单的显示纹理的shader, 然后放一摄像机在镜面位置,将摄像机的渲染结果作为纹理(render texture)传递给镜面shader,注意镜面shader的渲染队列应该在摄像机的shader之后,以便摄像机的渲染结果能够传递给镜面shader,同时需要注意纹理x坐标的反转。
玻璃效果
使用GrabPass实现
- GrabPass: 用于捕获屏幕上的图像,然后将其作为纹理传递给shader,以实现一些特殊效果,如玻璃效果,水面效果等
- 使用:
1 2 3 4 5 6 7 8
GrabPass { "TexName" } // 或者 GrabPass {} // 默认TexName为"_GrabTexture" // 可以通过ComputeGrabScreenPos(pos)计算屏幕空间的坐标 Coordinate = ComputeGrabScreenPos(pos); float4 _TexName_TexelSize // 纹理的像素大小, 用于计算纹理坐标
内置时间变量
1
2
3
4
float4 _Time; // t是自场景加载开始所经过的时间,4分量分别是(t/20, t, 2t, 3t)
float4 _SinTime; // t'是t的正弦值,4分量分别是(sin(t'/8), sin(t'/4), sin(t'/2), sin(t'))
float4 _CosTime; // t'是t的余弦值,4分量分别是(cos(t'/8), cos(t'/4), cos(t'/2), cos(t'))
float4 unity_DeltaTime; // dt为时间增量,4分量分别是(dt, 1/dt, soomth_dt, 1/soomth_dt)
NPR效果
- 描边效果
- 注意,法线扩展最好在view space中进行,而非world space
顶点动画
广告牌效果
- 变换在Object Space中进行