3.2.8. | Cg / HLSL Pragmas.
在我们的着色器中,我们至少可以找到三个默认编译指示。 这些是处理器指令,包含在 Cg 或 HLSL 中。 它们的功能是帮助我们的着色器识别和编译某些原本无法识别的函数。
#pragma vertex vert 允许名为 vert 的顶点着色器阶段作为顶点着色器编译到 GPU。 这是至关重要的,因为如果没有这行代码,GPU 将无法将“vert”函数识别为顶点着色器阶段,因此,我们将无法使用对象中的信息,也无法将信息传递到片段着色器阶段 投影到屏幕上。
如果我们查看 USB_simple_color 着色器中包含的pass,我们会发现以下与前面解释的概念相关的代码行。
// 允许我们将“vert”函数编译为顶点着色器
#pragma vertex vert
// use "vert" as vertex shader
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o, o.vertex);
return o;
}
#pragma 片段 frag 指令与 pragma 顶点具有相同的功能,不同之处在于它允许名为“frag”的片段着色器阶段在代码中编译为片段着色器。
// 允许我们将“frag”函数编译为片段着色器。
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoords, col);
return col;
}
与之前的指令不同,#pragma multi_compile_fog 有一个 double 函数。 首先,multi_compile 指的是着色器变体,它允许我们在着色器中生成具有不同功能的变体。 其次,“_fog”一词启用了 Unity 中“照明”窗口的雾功能,这意味着,如果我们转到“环境/其他设置”选项卡,我们可以激活或停用着色器的雾选项。
3.2.9. | Cg / HLSL Include.
指令“.cginc”(Cg include)包含几个文件,可以在我们的着色器中使用这些文件来引入预定义的变量和辅助函数。
如果我们查看 USB_simple_color 着色器,我们会发现pass中声明了以下指令:
• UNITY_FOG_COORDS(texcoordindex).
• UnityObjectToClipPos(inputVertex) .
• TRANSFORM_TEX(tex, name).
• UNITY_TRANSFER_FOG(outputStruct, clipSpacePos).
• UNITY_APPLY_FOG(inputCoords, colorOutput).
所有这些函数都属于“UnityCG.cginc”。 如果我们删除该指令,我们的着色器将无法编译,因为它将没有其引用。
我们可以在UnityCG.cginc中找到另一个定义的函数是UNITY_PI,它等于3.14159265359f。 后者不包含在我们的默认着色器中,因为它仅在特定情况下使用(例如,在计算三角形或球体时)。 如果我们想要查看 .cginc 文件中的变量和函数,我们可以遵循以下路径:
Windows: {unity install path}/Data/CGIncludes/UnityCG.cginc
Mac: /Applications/Unity/Unity.app/Contents/CGIncludes/UnityCG.cginc
除了 Unity 默认指令之外,我们还可以使用扩展名“.cginc”创建指令,为此,我们只需创建一个新文档,使用该扩展名保存它,然后开始定义我们的变量和函数。 在本书的后面部分,我们将开始使用一些自定义指令来处理光照、阴影和体积。
3.3.0. | Cg / HLSL vertex input & vertex output.
我们在创建着色器时经常使用的一种数据类型是“struct”。 对于了解 C 语言的人来说,结构体是一种复合数据类型声明,它定义了多个相同类型元素的分组列表,并允许通过单个指针访问不同的变量。 我们将使用结构体来定义着色器中的输入和输出。 其语法如下:
struct name
{
vector[n] name : SEMANTIC[n];
};
首先,我们声明结构,然后声明它的名称。 然后我们将向量语义存储在结构体字段中以供以后使用。 结构“名称”对应于结构的名称,“向量”对应于我们将使用的向量类型(例如 float2、half4)来分配语义。 最后,“语义”对应于我们将作为输入或输出传递的语义。
默认情况下,Unity添加了两个结构体函数,分别是:appdata和v2f。 Appdata对应“vertex input”,v2f指“vertex output”。
顶点输入将是我们存储对象属性(例如顶点位置、法线等)的地方,作为将它们带到“顶点着色器阶段”的“入口”。 然而,顶点输出将是我们存储光栅化属性以将它们带到“片段着色器阶段”的地方。
我们可以将语义视为对象的“访问属性”。 根据微软官方文档:
semantic(语义)是连接到着色器输入或输出的链,用于传输参数预期用途的使用信息。
我们将使用 POSITION[n] 语义进行举例。
在前面的几页中,我们讨论了基元的属性。 我们已经知道,图元有其顶点位置、切线、法线、UV 坐标和顶点颜色。 语义允许单独访问这些属性,也就是说,如果我们声明一个四维向量,并将 POSITION[n] 语义传递给它,那么该向量将包含原始顶点位置。 假设我们声明以下向量:
float4 pos : POSITION;
这意味着在称为“pos”的四维向量内,我们将对象顶点位置存储在对象空间中。
我们使用的最常见的语义是:
• POSITION[n].
• TEXCOORD[n].
• TANGENT[n].
• NORMAL[n].
• COLOR[n].
struct vertexInput (e.g. appdata)
{
float4 vertPos : POSITION;
float2 texCoord : TEXCOORD0;
float3 normal : NORMAL0;
float3 tangent : TANGENT0;
float3 vertColor: COLOR0;
};
struct vertexOutput (e.g. v2f)
{
float4 vertPos : SV_POSITION;
float2 texCoord : TEXCOORD0;
float3 tangentWorld : TEXCOORD1;
float3 binormalWorld : TEXCOORD2;
float3 normalWorld : TEXCOORD3;
float3 vertColor: COLOR0;
};
TEXCOORD[n] 允许访问图元的 UV 坐标,并且最多具有四个维度(x、y、z、w)。
TANGENT[n] 可以访问我们原语的切线。 如果我们想要创建法线贴图,则需要使用最多四个维度的语义。
通过 NORMAL[n] 我们可以访问图元的法线,它最多有四个维度。 如果我们想在着色器中使用光照,则必须使用此语义。
最后,COLOR[n] 允许我们访问图元顶点的颜色,并且与其他维度一样最多有四个维度。 通常,顶点颜色对应于白色(1,1,1,1)。
为了理解这个概念,我们将查看在 USB_simple_color 着色器中自动声明的结构。 我们将从 appdata 开始。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
正如我们所看到的,该结构中有两个向量:vertex 和 uv。 “Vertex”具有 POSITION 语义; 这意味着在向量内部,我们存储对象空间中对象顶点的位置。 这些顶点随后在顶点着色器阶段通过 UnityObjectToClipPos(v.vertex) 函数转换为剪辑空间。
向量 uv 具有语义 TEXCOORD0,它可以访问纹理的 UV 坐标。
为什么顶点向量有四个维度(float4)? 因为在向量中,我们存储值 XYZW,其中 W 等于“一”,顶点对应于空间中的位置。
在 v2f 结构中,我们可以找到与 appdata 中相同的向量,只是 SV_POSITION 语义略有不同,它实现与 POSITION[n] 相同的功能,但具有前缀“SV_”(System Value)。
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
请注意,这些向量在顶点着色器阶段连接如下:
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
…
}
“o.vertex”等于顶点输出,即v2f结构中已声明的顶点向量,而“v.vertex”等于顶点输入,即已声明的向量顶点 在应用程序数据结构中。 同样的逻辑也适用于 uv 向量。
3.3.1. | Cg / HLSL 变量和连接向量。
继续使用 USB_simple_color 着色器,请注意,在我们的程序中,有一个 Sampler2D 类型的变量和一个用于定义 _ MainTex 纹理的四维向量。
sampler2D _MainTex;
float4 _MainTex_ST;
这些连接变量已在 Cg 程序中声明为“全局”,并对应于先前包含在着色器属性中的 _MainTex 属性引用。
连接变量用于将属性的值或参数与我们的程序变量或内部向量连接起来。 对于 _MainTex,我们可以从 Unity Inspector 分配纹理并将其用作着色器中的纹理。
为了更好地理解这个概念,我们假设我们想要创建一个可以改变颜色的着色器。 为此,我们必须转到属性,创建一个“Color”类型,然后在 CGPROGRAM 字段中生成一个连接变量,这样我们就可以生成它们之间的链接
Properties
{
// first we declare the property
_Color ("Tint", Color) = (1, 1, 1 , 1)
}
…
CGPROGRAM
…
// then the connection variable or vector
sampler2D _MainTex;
float4 _Color;
…
ENDCG
通常,Cg 或 HLSL 中的全局变量使用“uniform”一词进行声明(例如,uniform float4 _Color),但是 Unity 会忽略此步骤,因为此声明包含在着色器中。
3.3.2. | Cg / HLSL 顶点着色器阶段.
顶点着色器对应于渲染管道的可编程阶段,其中顶点从 3D 空间转换为屏幕上的二维投影。 它的最小计算单位对应于一个独立的顶点。
在 USB_simple_color 着色器内部,有一个名为“vert”的函数,它对应于我们的顶点着色器阶段。 我们之所以知道它是我们的顶点着色器,是因为它是在 #pragma 顶点中声明的。
#pragma vertex vert
…
v2f vert (appdata v)
{
…
}
在继续解释此阶段之前,我们必须记住我们的着色器 USB_simple_color 是 Unlit 类型(它没有光),这就是为什么它包含一个用于顶点着色器的函数和另一个用于片段着色器的函数(#pragmafragment frag)。 值得一提的是,Unity 提供了一种以“Surface Shader”(surf)形式编写着色器的快速方法,可以自动生成 Cg 代码,专门用于受光照影响的材质。 这可以优化开发时间,但无助于我们理解它,因为许多函数和计算发生在程序内部。 这就是我们在本书开头创建 Unlit 着色器的原因; 来详细了解其运作。
我们将分析顶点着色器阶段的结构。 我们的函数以“v2f”一词开头,意思是“顶点到片段”。 当我们了解程序内部发生的内部流程时,这个名称就很有意义。 V2f 稍后将在片段着色器阶段用作参数,因此得名。 因此,由于我们的函数以 v2f 开头,这意味着它是顶点输出类型,因此我们必须返回与该数据类型关联的值。
继续使用“vert”,这是我们顶点着色器阶段的名称,然后是括号中的参数,其中 appdata 实现顶点输入的功能。
v2f vert (appdata v) { … }
如果我们继续分析,我们会注意到结构体 v2f 已在函数内用字母“o”初始化。 因此,在这个变量中,我们将找到之前在 v2f 中声明的所有属性。
v2f o;
o.vertex …
o.uv …
return o;
因此,顶点着色器阶段中发生的第一个操作是通过“UnityObjectToClipPos”方法将对象顶点从对象空间转换为剪辑空间。 让我们记住,我们的对象位于场景中的三维空间内,我们必须将这些坐标转换为屏幕上像素的二维投影。 该转换恰好发生在“UnityObjectToClipPos”函数中。 该函数的作用是将当前模型的矩阵(unity_ObjectToWorld)乘以当前视图与投影矩阵(UNITY_MATRIX_VP)之间的乘积因子
UnityObjectToClipPos(float3 pos).
{
return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos , 1.0)));
}
此操作在“UnityShaderUtilities.cginc”文件中声明,该文件已作为依赖项包含在 UnityCG.cginc 中,这就是我们可以在着色器中使用它的原因。 因此,我们从对象 (v.vertex) 获取顶点输入,将矩阵从对象空间转换到剪辑空间 (UnityObjectToClipPos) 并将结果保存在顶点输出 (o.vertex) 中。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
}
在处理输入或输出时我们必须考虑的一个因素是两个属性必须具有相同的维度数,例如,如果我们转到顶点输入 appdata,我们将看到向量“float4 vertex”具有相同的维度数 作为顶点输出 (v2f) 中的“float4 vertex”。
如果一个属性的类型为 float4,另一个属性的类型为 float3,则 Unity 可能会返回错误,因为在大多数情况下,您无法从四维向量转换为三维向量或更少。
然后我们可以找到TRANSFORM_TEX函数。 该函数需要两个参数,它们是:
- 对象的输入 UV 坐标 (v.uv)。
- 以及我们要放置在这些坐标上的纹理 (_MainTex)。 它实现了控制纹理UV坐标中“平铺和偏移”的功能。
最后,我们将这些值传递给 UV 输出 (o.uv),因为稍后它们将在片段着色器阶段使用。
3.3.3. | Cg/HLSL 片段着色器阶段。
Pass 中的下一个也是最后一个函数对应于着色器中出现的名为“frag”的片段着色器阶段。 我们之所以知道frag是片段着色器阶段的函数,是因为它已在#pragma片段中如此声明。
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
// fragment shader functions here
}
“片段”一词指的是屏幕上的一个像素; 到单个片段或到一起覆盖对象区域的组。 这意味着片段着色器阶段将处理计算机屏幕上与我们正在查看的对象相关的每个像素。
我们将分析frag函数的结构。 它以数据类型“fixed4”开头,这意味着我们必须返回四个维度的向量。
另一方面,我们要记住,这个着色器目前是用 Cg 编写的,因此它只能在Built-in RP 中编译。 如果我们希望着色器在Universal RP 和High Definition RP 中进行编译,我们必须将此数据类型更改为“half4 或 float4”,否则我们的程序可能会生成错误。
// Cg language
fixed4 frag (v2f i) : SV_Target { … }
// HLSL language
half4 frag (v2f i) : SV_Target { … }
在数据类型之后,名称以“frag”继续,并且作为参数,它有一个名为“i”的“v2f”类型变量。
与顶点着色器阶段不同,该函数有一个名为“SV_Target”的输出,它允许我们在中间缓冲区(渲染目标)中渲染场景,而不是将数据发送到帧缓冲区。 在 Direct3D 的早期版本(版本 9 及更低版本)中,片段着色器中的颜色输出以 COLOR 语义出现。 然而,在现代 GPU(版本 10 及以上)中,此语义由 更新,这意味着“系统价值目标”。 它可以在将图像投影到计算机屏幕上之前对其应用附加效果。 在片段着色器阶段内,我们可以找到一个名为“col”的固定向量类型,它与 tex2D 函数相同,其中作为参数,它接收 _MainTex 纹理和 UV 坐标输入。 基本上,此操作的作用是将纹理存储在 col 向量中。
这个向量之所以有四个维度是由于两个条件:第一个是因为 frag 函数是一个四维向量,所以我们必须返回一个具有相同维度的向量,第二个是因为在 col 内 我们将把纹理的颜色存储在它的 RGBA 通道中,因此,如果我们使用的纹理有一个 alpha 通道(透明度),那么它将反映在对象中。
fixed4 frag (v2f i) : SV_Target
{
// store the texture in the col vector
fixed4 col = tex2D(_MainTex, i.uv);
// return the texture color
return col;
}
3.3.4. | ShaderLab Fallback.
如上所述,此属性允许我们将着色器类型对象分配给该对象,以防我们的 SubShader 生成错误或与目标硬件不兼容。
其语法如下:
Fallback "shaderPath"
“Shader Path”对应于我们要在 SubShader 失败时使用的着色器的名称和路径。
回退也可以不声明,将空格留空或使用“Fallback Off”,尽管如此,建议使用Unity中已包含的着色器的路径和名称,例如“Mobile/Diffuse”。 这样,我们就可以确保我们的着色器在失败时能够继续编译。
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader { … }
Fallback "Mobile/Unlit"
}
在前面的示例中,如果 SubShader 生成错误,Fallback 将返回属于“Mobile”类别的“Unlit”类型着色器。
如果我们正在开发一个多平台游戏,建议为Fallback声明一个特定的路径,这样我们就可以确保我们的程序可以在大多数设备上运行。