4.0.1. | 着色器和材质之间的类比.
根据其术语,
材质是表面渲染方式的定义,包括对纹理、平铺、偏移、颜色等的引用。 材质的选项取决于所使用的着色器。
我们如何将上述定义转化为实际水平? 让我们将材质视为“着色器的容器”,这意味着我们拥有执行表面计算(着色器)的程序和能够读取这些计算的容器(材质)。
单独的材料无法执行任何操作。 如果它没有着色器,它将不知道应该如何渲染它,同样,如果不通过材质,着色器就无法应用于对象,因此,这个比喻之间材质和着色器是一个"预览的图形数学计算"。
4.0.2. | 我们的第一个 Cg 或 HLSL 着色器.
我们将继续使用我们在本章开头创建的“USB_simple_color”着色器。
我们已经知道,我们的默认着色器有一个名为 _MainTex 的纹理,它是在属性中配置的。 接下来我们要做的是添加颜色来改变纹理的色调。
在开始之前,我们必须记住,我们不能直接将着色器应用于场景中的对象。 为此,我们需要创建一个材质并将着色器分配给它,以便可以以图形方式渲染它。 要创建材质,我们必须转到我们的项目,右键单击我们正在其中工作的文件夹,转到创建并选择材质。 材质的默认配置取决于我们刚刚创建的渲染管道。 一般来说,当我们在内置 RP 中创建材质时,它会附带标准表面着色器类型,使我们能够可视化照明、阴影和与其相关的其他计算的方向。
因此,我们进入层次结构并创建一个 3D 对象。 我们将着色器 USB_simple_color 分配给我们最近创建的材质,然后将该材质应用于场景中的对象。 此时,我们可以为要投影到对象上的材质分配纹理。
返回我们的 USB_simple_color 着色器并创建一个颜色属性,正如我们将在以下示例中看到的:
Shader "USB/USB_simple_color"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Texture Color", Color) = (1, 1, 1, 1)
}
SubShader { … }
}
我们刚刚所做的是为我们的着色器声明一个颜色属性。 如果我们保存并返回到 Unity,我们可以看到这个属性出现在材质检查器中,但是,它还没有工作,因为我们还没有在 CGPROGRAM 中声明它。 接下来我们需要做的是添加 _Color 连接变量,以便我们可以在程序中使用它。 为此,我们进入顶点着色器阶段; 其中 _MainTex 已声明为 Sampler2D 并按以下方式创建连接变量:
uniform sampler2D _Maintex;
uniform float4 _MainTex_ST;
uniform float4 _Color; // 连接变量
正如我们所看到的,我们已经在 CGPROGRAM 或 HLSLPROGRAM 中声明了 _Color 的全局变量,但是,我们仍然没有使用它。 要更改纹理的色调,我们将进入片段着色器阶段并使用乘号将 col(纹理颜色)乘以 _Color。 操作应该如下所示:
// CGPROGRAM
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col * _Color;
}
// HLSLPROGRAM
half4 frag (v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
return col * _Color;
}
如果我们保存着色器并返回到 Unity,我们现在可以从材质检查器更改色调纹理。
4.0.3. | 在 Cg 或 HLSL 中添加透明度
在本节中,我们将添加混合,以便我们的着色器具有定义的 Alpha 通道。 在之前的 USB_simple_color 配置中,我们添加了颜色来更改纹理的色调。 现在,值得一提的是,颜色属性有四个通道(RGBA),但是,如果我们从 Inspector 中修改颜色的 Alpha 通道,我们会发现这并没有产生任何变化,这是为什么呢? 主要是因为它不具有 Blending 属性。 为了使我们的着色器受到透明度的影响,我们必须:将 RenderType 配置为透明,添加具有透明度的队列并添加我们要使用的混合类型。
Shader "USB/USB_simple_color"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Texture Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
LOD 100
Pass { … }
}
}
如果我们保存着色器并返回到 Unity,我们现在可以更改 Alpha 通道颜色,因此,这将影响材质纹理的透明度。
4.0.4 | HLSL 中函数的结构
与 C# 中一样,在 HLSL 中我们可以声明空函数 (void) 或返回值 (return) 的函数。
我们必须使用依赖于函数类型的“声明”; 它们确定一个值是否对应于输入(in)、输出(out)、全局变量(uniform)或常量值(const)。
我们将首先使用以下语法分析一个空函数:
void functionName_precision (declaration type arg)
{
float value = 0;
arg = value;
}
要声明空函数,我们从 void 命名法开始,然后是函数名称以及精度和最终参数。 正如我们在前面的示例中看到的,在函数字段内我们编写要执行的算法或操作。 一般来说,在参数中,我们必须定义这些是输入还是输出。
我们如何知道他们是否有声明? 一切都取决于我们想要作为参数传递的函数。 为了理解这个概念,假设我们想要创建一个函数来计算对象中的照明,为此,我们需要的属性之一是法线,通过它我们可以识别光源将从哪个方向照亮我们的物体。 目的。 因此,法线将是我们空函数内的“计算输入”。
void FakeLight_float (in float3 Normal, out float3 Out)
{
float[n] operation = Normal;
Out = operation;
}
该函数不实现任何特定功能,但我们将使用它来理解之前提到的概念。
该函数称为“FakeLight”,“_float”对应于其精度,可以是 float 或 half 类型,因为我们知道,这些是 HLSL 兼容格式。
我们必须始终为空函数添加精度,否则,它无法在我们的程序中编译。 然后在参数中,我们可以看到对象的法线(float3 Normal)已通过“in”声明声明为输入,同样,有一个名为“out”的输出,它将成为操作的最终值。
要在另一个函数中使用这种类型的函数,我们必须在函数本身之前在代码中声明输入和输出。
让我们在片段着色器阶段模拟 FakeLight 函数,就像它实际运行一样
// 创建我们的函数
void FakeLight_float (in float3 Normal, out float3 Out)
{
float[n] operation = Normal;
Out = operation;
}
half4 frag (v2f i) : SV_Target
{
// 声明法线。
float3 n = i.normal;
// 声明输出。
float3 col = 0;
// 将两个值作为参数传递。
FakeLight_float (n, col);
return float4(col.rgb, 1);
}
在上面的示例中,发生了几种情况。 首先,“FakeLight”函数在“frag”函数之前声明,因为 GPU 从上到下读取我们的代码。 然后,在片段着色器阶段,我们创建了一个名为“n”的三维向量和另一个名为“col”的三维向量。 在本例中,两者都是三维向量类型,这是因为我们将在 FakeLight_float 函数中使用这两个向量作为参数,该函数需要三维输入和输出向量。 然后,第一个参数对应于对象的正常输入,第二个参数对应于 FakeLight_float 函数内执行的操作的结果。
col 向量从“零”开始,这意味着它的红、绿、蓝 (RGB) 颜色为“0”,默认情况下对应于黑色,但是,由于它已被声明为输出,因此它是 现在发生在 FakeLight_float 函数内部。
最后,我们返回一个四维向量,其中前三个值对应于 RGB 中的 col 向量,“one”对应于 Alpha。
为什么我们要返回一个四维向量? 这是因为函数frag是half4类型,即四维向量。
现在我们将分析返回值的函数的结构。 本质上,它们非常相似,不同之处在于,在这种情况下,不需要添加精度函数。 为了说明这一点,我们将使用相同的 FakeLight 函数,但这次它将返回一个值。
// create our function
half3 FakeLight (float3 Normal)
{
float[n] operation = Normal;
return operation;
}
half4 frag (v2f i) : SV_Target
{
// declare normals
float3 n = i.normal;
float3 col = FakeLight_float (n);
return float4(col.rgb, 1);
}
与空函数不同,我们只添加 Normal 参数,因为它不需要输出,同样,在片段着色器阶段,我们使向量“col”等于该函数,因为它返回与该向量拥有的相同维数。
4.0.5. | 调试着色器
当我们用C#编写脚本时,在Unity中我们可以使用Debug.Log(对象消息)函数来调试我们的程序。 该函数允许我们在控制台上打印代码的操作,但是,该函数在 Cg 或 HLSL 中不可用。
那么我们如何调试着色器呢? 为此,我们必须考虑几个因素,包括:“颜色”。 在着色器中,我们经常看到三种重要的颜色,它们是白色 (1, 1, 1, 1)、黑色 (0, 0, 0, 1) 和洋红色 (1, 0, 1, 1)。 白色代表默认值,黑色代表初始化值。 洋红色代表图形错误,事实上,将洋红色资源导入 Unity 到我们的场景中是很常见的。
(图4.0.5a)
为了解决这个概念,让我们回顾一下 ShaderLab 中的属性声明,为此,我们将重新创建一个颜色属性。
_Color ("Tint", Color) = (1, 1, 1, 1)
在上面的例子中,属性默认被声明为“白色”,我们可以在四维向量中证实这一点; 操作结束时 (1, 1, 1, 1)。 该属性将在 Unity Inspector 中生成一个“颜色选择器”,其颜色和/或初始化值将为白色。
我们来分析一下另一个属性。
_MainTex ("Texture", 2D) = "white"{}
同样,这个属性将在 Unity Inspector 中生成一个“纹理选择器”,但是如果我们不使用任何纹理,它的默认颜色是什么? 它的颜色为白色,我们可以在该属性的“white”声明中确认这一点。
我们在代码中的内部向量和变量的声明中经常看到黑色,事实上,将向量初始化为“0”然后添加一些操作是很常见的,例如四维向量“float4 col = 0” 已初始化为“0”,这意味着其所有通道(RGBA)的默认值都为零。
注意,白色代表一个像素的最大照度值,黑色代表其最小照度。 记住这一点很重要,因为随着我们的前进,我们将有一些超出最大 (x > 1) 和最小 (x < 0) 值的操作。 在这些情况下,会出现颜色饱和,为此,我们将使用一些功能,例如“clamp”,它允许我们将值限制在两个数字(最小值和最大值)之间。
为什么我们的物体有时会呈现洋红色? 我们已经知道,这种颜色代表“图形错误”。 基本上,当 GPU 无法执行着色器中的操作时,它会返回默认的洋红色。 在Unity中,出现这种颜色的原因主要有两个因素:
- 渲染管道尚未配置。
- 或者着色器的代码中有错误。
当我们将资产导入软件时,我们首先需要知道的是,我们正在使用哪个渲染管道? 请记住,Unity 中存在三种类型的渲染管道,每种类型都有不同的配置。
假设我们在 Universal RP 中导入一个包含具有标准表面着色器材质的资源,那么我们的资源将以洋红色显示,因为这种类型的着色器仅在Built-in RP 中受支持。 为了解决这个图形错误,我们必须选择材质,进入 inspector 并将着色器类型更改为“Shader Graphs”中找到的类型。
一旦我们确定了正在使用的渲染管道的类型,我们就必须继续进行着色器评估。
由于对象获得洋红色,因此着色器中的错误在图形上很明显。 如果我们注意控制台,我们可以找到它的定义。 为了解决它们,我们必须考虑一些因素:在HLSL和ShaderLab中完成一条指令或函数。 大多数错误是由拼写错误或语法错误产生的。 下面,我们将回顾一个相当常见的:
我们该如何解读上面的陈述呢? 在这种情况下,“Unlit”菜单中找到的名为“USB_simple_color”的着色器在代码行 60 中出现错误,无法在 Direct3D 11 中编译。
现在,这是着色器问题吗? 为此,我们将分析生成错误的代码行。
58 fixed4 frag (v2f i) : SV_Target
59 {
60 fixed4 col = tex2D(_MainTex, i.uv);
61 return col // (;)
62 }
根据控制台,错误是在第 60 行代码中生成的,但正如我们所看到的,那里没有问题。 那么我们必须问自己,到底发生了什么? 错误是因为我们忘记关闭第 61 行代码中的操作。如果我们查看返回结果,我们会发现 col 向量缺少分号 (😉,因此 GPU 认为操作继续,因此无法继续编译它。
以下是我们经常看到的另一个错误:
half4 frag (v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
half2 uv = 0;
col = uv; // cannot be converted
return col;
}
在本例中,由于我们试图将四维向量 (col) 转换为二维向量 (uv),因此产生了错误。 col 向量是四维的,具有 RGBA 或 XYZW 通道,而 uv 向量是二维的,仅具有两个通道(RG、RB、GB 等)的组合,因此它不能从二维向量转换为四维向量。
4.0.6. | 添加 URP 兼容性
到目前为止,我们已经实现的许多变量、函数和向量都适用于 Cg 和 HLSL,但是,在某些情况下,我们必须添加 URP 支持,以便我们的着色器可以编译。
对于 Shader Graph,如果我们想使用 Universal RP 通过代码创建着色器,我们必须添加一些依赖项,以便 GPU 可以读取此渲染管道。 这种依赖关系可以在不同的路径中找到,其中我们可以提到:
Packages / Core RP Library / ShaderLibrary
Packages / Universal RP / ShaderLibrary
当我们在项目中安装 Shader Graph 时,会自动包含“Core RP Library”,同样,当我们选择此渲染管道作为渲染引擎时,也会包含“Universal RP”包。
这两个包都有扩展名为“.hlsl”的文件,我们的程序需要这些文件来编译 HLSL 中的着色器。
为了理解这个概念,让我们复制着色器 USB_simple_color 并将其重命名为 USB_simple_color_URP。 值得一提的是,着色器 USB_simple_color (Cg) 作为基本颜色模型,已经在 Universal RP 中具有兼容性,但是,我们将生成其副本并对其进行修改,以详细查看其在 HLSL 中的实现。
我们将首先添加标签“RenderPipeline”并使其等于“UniversalRenderPipeline”,这样GPU就会知道该着色器在此渲染引擎中具有兼容性。
Tags
{
"RenderType"="Transparent"
"Queue"="Transparent"
"RenderPipeline"="UniversalRenderPipeline"
}
正如本书中提到的,Universal RP 使用 HLSL 语言,因此 CGPROGRAM/ENDCG 块必须分别替换为 HLSLPROGRAM 和 ENDHLSL。
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
…
ENDHLSL
}
为了让我们的着色器完美编译并使其过程更加高效,我们必须包含依赖项“Core.hlsl”,它包含函数、结构和其他内容,从而允许程序在 Universal RP 中正常运行。 就其本身而言,Core.hlsl 取代了“UnityCg.cginc”,这意味着我们必须消除着色器的这种依赖性。
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// #pragma multi_compile_fog
// #include "UnityCg.cginc"
#include “Package/com.unity.render-pipelines.universal/ShaderLibrary/Core.
hlsl”
…
ENDHLSL
与 Unity 安装中包含的 UnityCg.cginc 不同,一旦我们在项目中安装 Universal RP,Core.hlsl 就会作为包添加,因此,我们必须在项目中写入其位置的完整路径。 一旦我们替换了这些依赖项,就会发生两件事。
- GPU 将无法编译雾坐标(UNITY_FOG_COORDS、UNITY_TRANSFER_FOR 和 UNITY_APPLY_FOR),因为这些坐标包含在 UnityCg.cginc 中。
- 出于同样的原因,您也无法编译 UnityObjectToClipPos 函数。
相反,我们必须使用函数 TransformObjectToHClip(v.vertex),它包含在依赖项“SpaceTransforms.hlsl”中,而“SpaceTransforms.hlsl”又包含在“Core.hlsl”中。
TransformObjectToHClip 函数执行与 UnityObjectToClipPos 相同的操作,即将顶点位置从对象空间变换到剪辑空间,但前者的执行过程更高效。
v2f vert (appdata v)
{
v2f o;
// o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex = TransformObjectToHClip(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
如果此时我们保存着色器并返回Unity,我们可以看到控制台中生成了“无法识别的标识符”类型的错误,这是什么原因? 请记住,默认情况下,片段着色器阶段和 col 向量的类型均为固定4。 所以我们可以在函数中检查它。
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
// UNITY_APPLY_FOG(i.fogcoord, col);
return col * _Color;
}
我们已经知道,Universal RP 无法编译“fixed”类型的变量或向量,而我们必须使用“half or float”。 因此,此时我们可以做两件事。
- 手动替换 fixed 类型的变量和向量。
- 或者包含“HLSLSupport.cginc”依赖项,为着色器编译添加帮助器宏和跨平台定义。
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// #pragma multi_compile_fog
// #include "UnityCg.cginc"
#include "HLSLSupport.cginc"
#include "Package/com.unity.render-pipelines.universal/ShaderLibrary/Core.
hlsl"
…
ENDHLSL
一旦包含这种依赖性,我们就可以使用 fixed 类型的变量和向量,因为现在我们的程序将根据其精度识别这种类型的数据,并自动将它们替换为 half 或 float。