Origin
zhuanlan.zhihu.com
Tags
简悦
项目
收藏夹
创建时间
收藏类型
Cubox 深度链接
更新时间
原链接
描述

前言

目前许多教程主要是旨于 Unity 内建管线(BRP)的,比较完整的 URP 教程相相对比较少一些,本篇则旨于 URP。
Unity 的渲染管线主要分为内置渲染管线(BRP)和可编程渲染管线(即 SRP , 包含 URP 和 HDRP)。自 Unity2019.3 版本开始,Unity 将轻量级渲染管线修改为了通用渲染管线(URP),这是一种快速、可扩展的渲染管线,支持所有的移动设备,适用于 2D、3D、虚拟现实 (VR) 和增强现实 (AR) 项目,相比旧的 BRP(内建管线) ,URP 拥有更好的渲染效果、更快的性能以及更高的扩展能力,URP 几乎是碾压姿态,在未来更将全面取代掉内建管线。所以,不管是美术人员还是程序开发,了解 URP 都很有必要。
如果你是初学者,你也可以尝试使用 Shader Graph ,可能更容易上手。当然如果你喜欢撸代码,那本篇就挺合适,毕竟 Shader Graph 无法像代码那般灵活,并且可以访问所有内容。
当然,如果你已经是个 Shader 老手,并希望从 BRP 升级到 URP,那你可以直接跳过 ShaderLab 的内容,查看最后一部分关于内置管线与 URP 的差异总结。

ShaderLab

ShaderLab 是一种专门用于 Unity 的着色器语言,用于描述着色器的结构和属性,包括渲染类型、材质属性、渲染状态等。通过 ShaderLab,用户可以轻松地创建多个子着色器和通道,以适应不同的渲染需求。同时,ShaderLab 也提供了方便的语法和编译器来生成最终的着色器代码。
在 ShaderLab 所有着色器都以 Shader 块开头,其中包含一个路径和名称,用于确定在检查器窗口中更改材质上的着色器时它在下拉列表中的显示方式。语法结构如下:
其他块将放在大括号内,包括属性块和各种子着色器块。

Properties(属性)

属性块用于将需要暴露给材质检视面板(Inspector)的任何值,以便我们可以在具有不同纹理 / 颜色的材料中使用相同的着色器。
我们也可以从 C# 脚本中更改这些属性(例如使用 material.SetColor / SetFloat / SetVector 等)。如果每个材质的属性都不同,则必须在 Properties 块以及 UnityPerMaterial CBUFFER 中包含它们,以正确支持 SRP Batcher,这将在后面解释。
如果所有着色器都应共享相同的值,则我们不必在此处公开它们。相反,我们仅在 HLSL 代码中稍后定义它们。我们仍然可以使用 Shader.SetGlobalColor / SetGlobalFloat / SetGlobalVector / 等方法在 C# 中设置它们。

SubShader(子着色器)

我们的 Shader 块可以包含多个 SubShaders。Unity 将使用 GPU 支持的第一个子着色器块。
如果不支持子着色器,我们还可以定义一个回退。如果不使用回退,那么它将显示洋红色以表示异常。
稍后我们将在每个 SubShader 中定义通道,其中可以包含 HLSL 代码。在这里面我们可以指定一个 Shader Compile Target。更高的 target 版本支持更多的 GPU 功能,但可能无法兼容所有平台。
在 v10 之前的 Unity URP 版本中,所有通道都会使用以下内容:
您可以在 URP/Lit 着色器 (v8.3.1) 中看到这方面的示例。
从 v10 + 开始,URP 开始支持延迟渲染,因此提供的着色器使用了两个子着色器。第一个子着色器在每个通道中使用以下内容:
基本上意思是 “将它用于除 OpenGL 之外的所有平台”。第二个 SubShader 使用:
其实这两个子着色器是相同的,除了 target 和第二个子着色器排除了 UniversalGBuffer 通道,用于延迟渲染,可能是因为这些平台目前不支持(请注意,这里说的是内置管线的延迟渲染,但技术是相通的)。对于这篇帖子 / 教程,我不包括这些 target 的内容,但如果您支持延迟渲染并针对 OpenGL 平台进行 target 设置,则将其拆分为像 URP/Lit 着色器(v10.5.0)一样的两个子着色器可能就有必要。

Render Pipeline

RenderPipeline 标签应该防止使用 SubShader,除非它旨在用于当前正在使用的渲染管线。该标签对应于 Shader.globalRenderPipeline 值,使用 Scriptable Render Pipeline 时会设置该值。
该值可以设置为 “UniversalPipeline”(或旧的 “LightweightPipeline”)和 “HDRenderPipeline”。虽然我没有测试过,但使用不同的值可能意味着除非使用自定义渲染管线并设置 Shader.globalRenderPipeline 字符串,否则该 SubShader 将始终被忽略。
完全排除标签意味着任何管线都可以使用它。我不确定如果标签值设置为空字符串(“”)会发生什么行为,但可能是相同的。对于内置的 RP 没有一个值,所以如果您想针对它进行定位,我建议使用最后一个没有 RenderPipeline 标签的 SubShader,类似于 Fallback。例如:
需要注意的是:之前我在 Unity 2019.3 和 URP 7.x 中进行了 RenderPipeline 标签测试时发现,如果着色器只包括一个 SubShader,无论标签设置为什么,它都会尝试使用它。不确定是否已更改。
另外,如果您在任何地方看到标签 “UniversalRenderPipeline”,那么这是不正确的,请不要使用它!这只是因为上面提到的问题而起作用。它实际上甚至出现在官方文档中,但是在我提到它后很快就被修复了。<3
Unity 2018 版本似乎总是使用第一个通道中的 SceneSelectionPass 和 Picking 通道,而不管标签如何。Unity 2019+ 修复了这个问题,不确定是否已经反向移植,但如果进行任何自定义场景选择渲染,则需要注意此问题。

Queue(队列)

Queue 标签很重要,用于确定对象何时呈现,但也可以通过材质(通过检查器中的 Render Queue)进行覆盖。
标签必须设置为以下预定义名称之一,每个名称对应一个 Render Queue 值:
  • “Background” (1000)
  • “Geometry”(2000)
  • “AlphaTest”(2450)
  • “Transparent” (3000)
  • “Overlay”(4000)
我们还可以在名称后附加 +N 或 -N 来更改着色器使用的队列值。例如,“Geometry+1” 将是 2001,因此在使用 2000 的其他对象之后呈现。“Transparent-1” 将是 2999,所以将在使用 3000 的其他透明对象之前呈现。
2500 以下的值被认为是不透明的,因此使用相同队列值的对象从前向后呈现(更接近相机的对象先呈现)。这是为了优化渲染,以便如果它们未通过深度测试,则可以丢弃后面的片段(稍后将详细解释)。
2501 及以上是透明的,从后向前呈现(更远的对象先呈现)。因为透明着色器往往不使用深度测试 / 写入,所以更改队列将改变着色器与其他透明对象排序的方式。
您还可以在 Unity SubShaderTags 文档中找到其他可用标签。

Pass(通道)

Pass 块在每个 SubShader 中被定义。可以有多个 Pass,其中每个 Pass 应包括一个名为 LightMode 的特定标签,该标签确定 Pass 何时 / 如何被使用(在下一节中进一步解释)。
您还可以为它们指定一个可选的 Name,以便在不同的着色器中使用 UsePass。在 URP Lit 着色器中使用 ShadowCaster pass 的示例已被注释掉。这是因为不建议使用 UsePass。为了保持 SRP 批处理器的兼容性,着色器中的所有 pass 必须具有相同的 UnityPerMaterial CBUFFER,而 UsePass 当前可能会破坏这一点,因为它使用先前着色器中定义的 CBUFFER。相反,您应该自己编写每个 pass 或手动复制它。我们将在稍后的部分介绍一些这些 pass。
根据着色器的用途,您甚至可能不需要额外的 pass。例如,用于应用全屏图像效果的 Blit 渲染特性的着色器只需要一个 pass,其中 LightMode 标记可以完全省略。

LightMode Tag

如前所述,每个 pass 都包括一个名为 LightMode 的标签,它描述了如何在 Unity 中使用该 pass。通用渲染管线使用以下模式:
  • “UniversalForward” - 用于在 Forward 渲染路径中渲染对象。使用照明渲染几何体。
  • “ShadowCaster” - 用于投射阴影
  • “DepthOnly” - 用于深度预处理以创建深度纹理 (_CameraDepthTexture),如果启用了 MSAA 或平台不支持复制深度缓冲区。
  • “DepthNormals” - 由深度法线预处理使用,如果渲染器功能请求(通过在 ScriptableRenderPass 中的 ConfigureInput(ScriptableRenderPassInput.Normal);,例如 SSAO 功能)则创建深度纹理 (_CameraDepthTexture) 和法线纹理(_CameraNormalsTexture)。
  • “Meta” - 在 Lightmap 烘焙期间使用
  • “Universal2D” - 在启用 2D Renderer 时使用的渲染
  • “SRPDefaultUnlit” - 如果在 Pass 中未包含 LightMode 标签,则为默认值。可用于绘制额外的 Pass(在正向 / 延迟渲染中都可以),但这可能会破坏 SRP 批处理的兼容性。请参阅下面的多通道部分。
未来的更改还将添加这些(v12+?):
  • “UniversalGBuffer” - 用于在延迟渲染路径中渲染对象。将几何体渲染到多个缓冲区而不带照明。照明在路径的后面处理。
  • “UniversalForwardOnly” - 类似于 “UniversalForward”,但可用于即使在延迟路径中也将对象渲染为正向,这对于着色器具有无法适合 GBuffer 的数据,如清除涂层法线,非常有用。
我目前没有包含有关 UniversalGBuffer pass 的部分,因为它尚未得到适当的发布。我可能会在将来更新文章(但不保证!)
像 “Always”,“ForwardAdd”,“PrepassBase”,“PrepassFinal”,“Vertex”,“VertexLMRGBM”,“VertexLM” 这样的标签适用于内置 RP,并且不受 URP 支持。
您还可以使用自定义 LightMode 标签值,您可以通过自定义 Renderer 功能或 URP 提供的 RenderObjects 功能触发其呈现。

Cull

每个 pass 块都可以包含 Cull 指令,用于控制哪些三角形的面应该被渲染。
哪些面对应于 “正面” 或“背面”取决于每个三角形顶点的绕序方式。在 Blender 中,这由法线决定。

Depth Test/Write(深度测试 / 写入)

每个通道都可以包括深度测试 ( ZTest ) 和深度写入 ( ZWrite ) 操作。
深度测试决定了如何根据一个片段的深度值与深度缓冲区中的值进行比较来确定渲染。例如,LEqual 是默认值,只有当片段深度小于或等于缓冲区中的值时才会被渲染。
深度写入则决定了当测试通过时,片段的深度值是否替换深度缓冲区中的值。使用 ZWrite Off 可以使缓冲区中的值保持不变。这主要用于透明对象,以便实现正确的混合,但这也是为什么对它们进行排序很困难,它们有时可能会以错误的顺序进行渲染。
另外,偏移操作允许您使用两个参数(因子、单位)来偏移深度值。
因子缩放多边形相对于 X 或 Y 的最大 Z 斜率,单位缩放最小可解析的深度缓冲值。这使您可以强制将一个多边形绘制在另一个多边形的顶部,尽管它们实际上位于相同的位置。例如,Offset 0,-1 会将多边形拉近到相机位置,忽略多边形的斜率,而 Offset -1,-1 会在观察的梯度角度时将多边形拉得更近。

Blend & Transparency

为了支持透明度,需要定义混合(Blend)模式。这决定了片段结果如何与相机颜色目标 / 缓冲区中现有的值相结合。语法如下:
其中,着色器颜色结果与 SrcFactor 相乘,而现有的颜色目标 / 缓冲像素与 DstFactor 相乘。每个值都基于单独的 BlendOp 操作进行组合(默认为 Add),以产生最终的颜色结果,该结果替换缓冲区中的值。
因子可以是以下之一:
  • One
  • Zero
  • SrcColor
  • SrcAlpha
  • DstColor
  • DstAlpha
  • OneMinusSrcColor
  • OneMinusSrcAlpha
  • OneMinusDstColor
  • OneMinusDstAlpha
另外请参见 Blend 文档页面,以获取支持的 BlendOp 操作列表,如果您想选择不同的操作。
最常见的混合模式包括:
  • Blend SrcAlpha OneMinusSrcAlpha - 传统透明度
  • Blend One OneMinusSrcAlpha - 预乘透明度
  • Blend One One - 加法混合
  • Blend OneMinusDstColor One - 柔和的加法混合
  • Blend DstColor Zero - 乘法混合
  • Blend DstColor SrcColor - 2 倍乘法混合
以下是一些例子:

Multi-Pass(多通道)

如果您有额外的渲染通道,但没有使用 LightMode 标签(或使用 SRPDefaultUnlit),则它将与渲染主要的 UniversalForward 一起使用。这通常被称为 “多通道”。但是,尽管这在 URP 中可能有效,但不建议这样做,因为它会破坏 SRP 批处理器的兼容性,这意味着使用该着色器渲染对象会更昂贵。
相反,建议使用以下方法之一来实现多通道:
  1. 应用作第二个材质的单独着色器。如果使用子网格,则可以添加更多材质并回路。
  1. 在 Forward Renderer 上使用 RenderObjects 功能,使用覆盖材质(使用单独的着色器)重新渲染特定 unity 图层上的所有不透明或透明对象。如果您想渲染许多具有此第二通道的对象,则此方法非常有用。不要在单个对象上浪费整个图层。使用覆盖材质也不会保留前一个着色器的属性 / 纹理。
  1. 再次使用 RenderObjects 功能,但不是使用覆盖材质,而是在着色器中使用自定义 LightMode 标记的 Pass,并在该功能的 Shader Tag ID 设置上使用它来渲染它。由于它仍然是相同的着色器,因此此方法将保留属性 / 纹理,但仅适用于编写的着色器,因为 Shader Graph 没有提供注入自定义 Pass 的方法。

HLSL

Shader 代码是使用 Unity 中的高级着色语言(HLSL)编写的。

HLSL 程序和 HLSLINCLUDE

在每个 ShaderLab Pass 中,我们使用 HLSLPROGRAM 和 ENDHLSL 标记定义 HLSL 代码块。每个块必须包含顶点和片段着色器。我们使用 #pragma vertex/fragment 设置要使用的函数。
对于内置管线着色器,“vert”和 “frag” 是最常见的名称,但它们可以是任何名称。对于 URP,它倾向于使用像 “UnlitPassVertex” 和“UnlitPassFragment”这样的函数,这对于描述着色器通道正在执行的操作更为描述性。
在 SubShader 中,我们还可以使用 HLSLINCLUDE 将代码包含在该 SubShader 中的每个 Pass 中。这在 URP 中编写着色器非常有用,因为每个 pass 都需要使用相同的 UnityPerMaterial CBUFFER 以与 SRP 批处理器兼容,这有助于我们重复使用相同的代码而不需要单独定义它。我们也可以使用单独的 include 文件。
我们将在以后讨论这些代码块的内容。现在,我们需要了解一些 HLSL 基础知识,以便能够理解后面的部分。

Variables(变量)

在 HLSL 中,我们有几种不同的变量类型,其中最常见的是标量、向量和矩阵。还有一些特殊对象用于纹理 / 采样器。数组和缓冲区也存在用于将更多的数据传递到着色器中。

Scalar(标量)

标量类型包括:
  • bool-true 或 false。
  • float-32 位浮点数。通常用于世界空间位置、纹理坐标或涉及复杂函数的标量计算,例如三角函数或幂 / 指数。
  • half-16 位浮点数。通常用于短向量、方向、对象空间位置、颜色。
  • double-64 位浮点数。不能用作输入 / 输出,请参见此处的说明。
  • real - 在 URP/HDRP 中使用,当函数支持 half 或 float 时。默认为 half(假定它们在平台上受支持),除非着色器指定 “#define PREFER_HALF 0”,否则它将使用 float 精度。 ShaderLibrary 中的许多常用数学函数使用此类型。 int-32 位带符号整数
  • uint-32 位无符号整数(除了 GLES2,其中不支持此功能,并将其定义为 int)。 还值得注意的是:
  • fixed - 带有 - 2 到 2 范围的 11(左右)位定点数。通常用于 LDR 颜色。这是来自旧的 CG 语法的一些东西,尽管所有平台似乎现在都将其转换为 half,即使在 CGPROGRAM 中也是如此。 HLSL 不支持此功能,但我认为将 “fixed” 类型用于内置 RP 的着色器中很重要,改为使用 half!

Vector(向量)

通过将一个组件大小(整数从 1 到 4)附加到其中一个标量数据类型上,可以创建一个向量。例如:
  • float4-(包含 4 个浮点数的浮点向量)
  • half3-(半向量,3 个分量)
  • int2
  • 从技术上讲,float1 也将是一个一维向量,但据我所知,它等同于 float。 为了获取向量的一个组件,我们可以使用. x、.y、.z 或. w(或者. r、.g、.b、.a,这在使用颜色时更有意义)。我们还可以使用. xy 从高维向量中获取向量 2 和. xyz 获取向量 3。
我们甚至可以进一步采取这个并返回一个重新排列组件的向量,这被称为 swizzling。
Swizzling 是一种在计算机图形学中使用的操作,它可以让我们通过重新排列向量的元素来创建新的向量,而无需进行显式的拷贝或重复。Swizzling 通常用于访问和操作向量的子集,例如将向量的三个元素作为颜色值进行传递(例如 RGBA),或将向量的前两个元素作为 2D 纹理坐标进行使用。
下面是一些例子:

Matrix(矩阵)

在 Shader 中通过将两个大小(1 到 4 之间的整数)附加到标量来表示一个矩阵,用 “x” 分隔。第一个整数是行数第二个是矩阵的列数。例如 :
  • float4x4– 4 行,4 列
  • int4x3– 4 行,3 列
  • half2x1– 2 行,1 列
  • float1x4– 1 行,4 列
矩阵用于不同空间之间的转换。如果您对它们不是很熟悉,我建议您查看 CatlikeCoding 的教程
Unity 内置了变换矩阵,用于公共空间之间的变换,例如:
  • UNITY_MATRIX_M(or Unity_ObjectToWorld) - 模型矩阵,从对象空间转换为世界空间
  • UNITY_MATRIX_V视图矩阵,从世界空间转换为视图空间
  • UNITY_MATRIX_P投影矩阵,从视图空间转换为剪辑空间
  • UNITY_MATRIX_VP查看投影矩阵,从世界空间转换为剪辑空间
还有反转版本:
  • UNITY_MATRIX_I_M(or unity_WorldToObject) - 逆模型矩阵,从世界空间转换为对象空间
  • UNITY_MATRIX_I_V逆视图矩阵,从视图空间转换为世界空间
  • UNITY_MATRIX_I_P逆投影矩阵,从剪辑空间转换为视图空间
  • UNITY_MATRIX_I_VP逆视图投影矩阵,从剪辑空间转换为世界空间
虽然您可以使用这些矩阵通过矩阵乘法(例如mul(matrix, float4(position.xyz, 1)))在空间之间进行转换,但 SRP Core ShaderLibrary SpaceTransforms.hlsl 中也有辅助函数。
需要注意的是,在处理矩阵乘法时,顺序很重要。通常矩阵将在第一个输入中,向量在第二个输入中。第二个输入中的向量被视为最多包含 4 行(取决于向量的大小)和单列的矩阵。第一个输入中的向量被视为由 1 行和最多 4 列组成的矩阵。
也可以使用以下任一方法访问矩阵中的每个组件:从零开始的行列位置:
  • ._m00, ._m01, ._m02, ._m03
  • ._m10, ._m11, ._m12, ._m13
  • ._m20, ._m21, ._m22, ._m23
  • ._m30, ._m31, ._m32, ._m33
以一为基础的行列位置:
  • ._11, ._12, ._13, ._14
  • ._21, ._22, ._23, ._24
  • ._31, ._32, ._33, ._34
  • ._41, ._42, ._43, ._44
从零开始的数组访问符号:
  • [0][0]、[0][1]、[0][2]、[0][3]
  • [1][0]、[1][1]、[1][2]、[1][3]
  • [2][0]、[2][1]、[2][2]、[2][3]
  • [3][0]、[3][1]、[3][2]、[3][3]
通过前两个选项,您还可以使用 swizzling。例如._m00_m11._11_22
值得注意的是,._m03_m13_m23对应于每个矩阵的翻译部分。所以UNITY_MATRIX_M._m03_m13_m23给你游戏对象起源的世界空间位置,(假设没有涉及静态 / 动态批处理,原因在我的着色器介绍帖子中解释过)。

Texture Objects(纹理对象)

纹理为每个纹素存储颜色 - 基本上与像素相同,但在提及纹理时它们被称为纹素(纹理元素的缩写),并且它们也不限于二维。
片段着色器阶段在每个片段 / 像素的基础上运行,我们可以在其中访问具有给定坐标的纹素的颜色。纹理可以具有不同的大小(宽度 / 高度 / 深度),但用于对纹理进行采样的坐标被标准化为 0-1 范围。这些被称为纹理坐标或 UV。(其中 U 对应于纹理的水平轴,而 V 是垂直轴。有时您会看到 UVW,其中 W 是纹理的三维 / 深度切片)。
最常见的纹理是 2D 纹理,可以在 URP 中使用以下宏在全局范围内(在任何函数之外)定义:
对于每个纹理对象,我们还定义了一个 SamplerState,其中包含来自纹理导入设置的环绕和过滤模式。或者,我们可以定义一个内联采样器,例如SAMPLER(sampler_linear_repeat).
Filter Modes
  • Point(或 Nearest-Point):颜色取自最近的纹素。结果是块状 / 像素化,但如果您正在采样像素艺术,您可能会想要使用它。
  • Linear / Bilinear:颜色被视为接近纹素的加权平均值,基于它们的距离。
  • Trilinear:与 Linear/Bilinear 相同,但它也在 mipmap 级别之间混合。
Wrap Modes
  • Repeat:0-1 之外的 UV 值将导致纹理平铺 / 重复。
  • Clamp:0-1 之外的 UV 值被夹紧,导致纹理的边缘伸展。
  • Mirror:纹理平铺 / 重复,同时也在每个整数边界处进行镜像。
  • Mirror Once:纹理被镜像一次,然后限制低于 -1 和高于 2 的 UV 值。
稍后在片段着色器中,我们使用另一个宏来使用 uv 坐标对 Texture2D 进行采样,该坐标也将从顶点着色器传递:
其他一些纹理类型包括:Texture2DArray、Texture3D、TextureCube(在着色器之外称为 Cubemap)和 TextureCubeArray,每种都使用以下宏:

Array(数组)

也可以定义数组,并使用 for 循环遍历。例如 :
如果循环的大小是固定的(即不基于变量)并且循环不会提前退出,则 “展开” 循环可能会更高效,这就像使用索引多次复制粘贴相同的代码变了。
技术上也可以使用其他类型的数组,但 Unity 只能从 C# 脚本设置 Vector (float4) 和 Float 数组。
我还建议始终全局设置它们,使用Shader.SetGlobalVectorArray和 / 或Shader.SetGlobalFloatArray而不是使用material.SetVector/FloatArray版本。这样做的原因是数组不能正确包含在 UnityPerMaterial CBUFFER 中(因为它要求它也被定义在 ShaderLab 属性中,并且那里不支持数组)。如果使用 SRP Batcher 对对象进行批处理,则尝试使用不同数组的多种材质会导致故障行为,其中所有对象的值都会根据屏幕上呈现的内容而改变。通过全局设置它们,只能使用一个数组来避免这种情况。
请注意,这些 SetXArray 方法也限制为最大数组大小 1023。如果您需要更大的数组,您可能需要尝试替代解决方案,例如计算缓冲区 (StructuredBuffer),假设它们在目标平台上受支持。

Buffer(缓冲)

数组的替代方法是使用 Compute Buffers,它在 HLSL 中被称为 StructuredBuffer(它是只读的。或者有 RWStructuredBuffer 用于读取和写入,但仅在像素 / 片段和计算着色器中受支持)。
您至少还需要#pragma target 4.5使用这些。并非所有平台都支持计算缓冲区(有些平台可能不支持顶点着色器中的 StructuredBuffer)。您可以SystemInfo.supportsComputeShaders在运行时在 C# 中使用来检查平台是否支持它们。
并使用此 C# 进行设置,作为测试:
对于 StructuredBuffers 大家也可以到网上查找更多的补充。

Functions(函数)

在 HLSL 中声明函数与 C# 非常相似,但请务必注意,您只能调用已声明的函数。您不能在声明函数之前调用它,因此函数和文件的顺序#include很重要!(了解 c 和 c++ 的同学应该就很熟悉这个 #include) >> 武侠小说中,武功招式名先喊出来,再出招。
这里float3是返回类型,“example” 是函数名,括号内是传递给函数的参数。在没有返回类型的情况下,void使用。out您还可以在参数类型之前使用指定输出参数,或者inout如果您希望它成为可以编辑并传回的输入。(也有in但我们不需要写)
您可能还会inline在函数返回类型之前看到。这是函数实际可以拥有的默认且唯一的修饰符,因此指定它并不重要。这意味着编译器将为每次调用生成函数的副本。这样做是为了减少调用函数的开销。
您可能还会看到以下功能:
这称为。宏在编译着色器之前被处理,它们被定义替换,任何参数都被替换。例如 :
另一个宏示例是:
运算##符是一种特殊情况,宏在其中很有用。它允许我们连接名称和_ST部分,从而产生_MainTex_ST此输入。如果该##部分被遗漏,它只会产生name_ST,导致错误,因为它尚未定义。(当然,_MainTex_ST 仍然需要定义,但这是预期的行为。附加_ST到纹理名称是 Unity 处理纹理的平铺和偏移值的方式)。

UnityPerMaterial CBUFFER

继续实际创建着色器代码,我们应该首先在 SubShader 的 HLSLINCLUDE 块中指定 UnityPerMaterial CBUFFER 。这确保相同的 CBUFFER 用于所有通道,这对于着色器与 SRP Batcher 兼容很重要。
CBUFFER 必须包含所有公开的属性(与 Shaderlab Properties 块中相同),但纹理除外,尽管您仍然需要包含纹理平铺和偏移值(例如 _ExampleTexture_ST,其中 S 指缩放,T 指平移)和 TexelSize(例如 _ExampleTexture_TexelSize),如果它们被使用。
它不能包含未公开的其他变量。
注意:虽然不必通过 C#公开material.SetColor / SetFloat / SetVector / etc变量来设置它们,但如果多个材质实例具有不同的值,这可能会产生故障行为,因为 SRP Batcher 仍会在屏幕上将它们一起批处理。如果您有未公开的变量 - 始终使用Shader.SetGlobalX函数设置它们,以便它们对所有材质实例保持不变。如果它们需要因材质而异,您应该通过 Shaderlab Properties 块公开它们并将它们添加到 CBUFFER。
在上面的代码中,我们还使用 #include 从 URP ShaderLibrary 中包含 Core.hlsl,如上所示。这基本上是内置管道 UnityCG.cginc 的 URP 等价物。Core.hlsl(以及它自动包含的其他 ShaderLibrary 文件)包含一堆有用的函数和宏,包括 和宏本身,它们被替换为 “cbuffer name {” 和“};” 在支持它们的平台上,(我认为除了 GLES2 之外的所有平台,这是有道理的,因为该平台也不支持 SRP Batcher)。CBUFFER_STARTCBUFFER_END

Structs(结构体)

在定义顶点或片段着色器函数之前,我们需要定义一些用于将数据传入和传出的结构。在内置中,通常创建两个名为 “appdata” 和“v2f”(“vertex to fragment”的缩写),而 URP 着色器倾向于使用 “Attributes” 和“Varyings”。这些只是名称,通常不太重要,如果您愿意,可以将它们命名为 “VertexInput” 和“FragmentInput”。
URP ShaderLibrary 还使用一些结构来帮助组织某些功能所需的数据——例如用于照明 / 着色计算的 InputData 和 SurfaceData,我将在照明部分介绍它们。
由于这是一个相当简单的 Unlit 着色器,我们的属性和变量不会那么复杂:

Attributes(顶点输入)

Attributes 结构将作为顶点着色器输入。它允许我们使用被称为 semantics 的字符串(大部分都是大写的)从网格中获取每个顶点的数据。
可以通过该链接找到完整的语义列表,但这里是顶点输入中常用的一些语义:
  • POSITION: 顶点位置
  • COLOR: 顶点颜色
  • TEXCOORD0-7:UV(又名纹理坐标)。一个网格有 8 个不同的 UV 通道,值从 0 到 7。请注意,在 C# 中,Mesh.uv 对应于TEXCOORD0. Mesh.uv1 不存在,下一个通道是 uv2,它对应于TEXCOORD1等等直到 Mesh.uv8 和TEXCOORD7
  • NORMAL:顶点法线(用于光照计算。目前没有光照,因此不需要)
  • TANGENT:顶点切线(用于定义 “切线空间”,对法线贴图和视差效果很重要)
还有一些更特殊的语义,如SV_VertexID(requires #pragma target 3.5),它允许您获得每个顶点的标识符(uint类型)。与 ComputeBuffer 一起使用时很有用。

Varyings(片段输入)

Varyings 结构将是片段着色器的输入和顶点着色器的输出(假设中间没有几何着色器,这可能需要另一个结构,但我们不会在本文中讨论它)
与之前的结构不同,我们使用SV_POSITIONinstead ofPOSITION来存储来自顶点着色器输出的裁剪空间位置。将几何图形转换为屏幕上正确位置的片段 / 像素非常重要。
我们还使用COLOR和 / 或TEXCOORDn(其中 n 是一个数字)语义,但与以前不同的是,根本不必对应于网格顶点颜色 / uvs。相反,它们用于在三角形中插入数据NORMAL/TANGENT通常不在 Varyings 结构中使用,虽然我已经看到它们仍然有效(以及完全自定义的语义,例如 Shader Graph 使用INTERPn),但它可能并非在所有平台上都受支持,因此为了TEXCOORDn安全起见,我会坚持使用。
根据平台和编译目标,可用插值器的数量可能会有所不同:
  • OpenGL ES 2.0 (Android)、Direct3D 11 9.x 级别 (Windows Phone) 和 Direct3D 9 Shader Model 2.0 ( #pragma target 2.0) 最多支持 8 个插值器 (eg TEXCOORD0-7)
  • Direct3D 9 Shader Model 3.0 ( #pragma target 3.0) 最多支持 10 个(例如TEXCOORD0-9
  • OpenGL ES 3.0 (Android) 和 Metal (iOS) 平台最多支持 16 个(例如TEXCOORD0-15
  • Direct3D 10 Shader Model 4.0 ( #pragma target 4.0) 支持多达 32 个(例如TEXCOORD0-31
与 , 一起使用的另一个有用的语义Cull OffVFACE(浮点类型,在 Direct3D 9 Shader Model 3 中可用)。负值表示它是背面,而正值表示它是正面。因此可以使用三元像(face > 0) ? _ColorFront : _ColorBack将颜色应用于不同的面。Direct3D 10 有一个类似的SV_IsFrontFace但是是 bool 类型而不是 float。

片段输出

片段着色器还可以提供输出结构。然而,通常不需要它,因为它通常只使用单个输出语义,SV_Target用于将片段 / 像素颜色写入当前渲染目标。在这种情况下,我们可以使用如下函数定义它:
着色器可以输出到多个渲染目标,称为多渲染目标 (MRT)。这由延迟渲染路径使用,例如,请参阅 UnityGBuffer.hlsl(URP 尚未完全支持)。
如果不使用延迟路径,使用 MRT 将需要在 C# 端进行设置,例如Graphics.SetRenderTargetRenderBuffer[]数组一起使用,或CommandBuffer.SetRenderTargetRenderTargetIdentifier[]数组一起使用。然而,并非所有平台都支持 MRT(例如 GLES2)
在着色器中,我们将像这样定义 MRT 输出:
SV_Depth也可以使用语义(或SV_DepthGreaterEqual/ )更改用于深度的值,SV_DepthLessEqual如我的深度文章中所述。

顶点着色器

我们的顶点着色器需要做的主要事情是将对象空间位置从网格转换为裁剪空间位置。这是为了在预期的屏幕位置正确渲染片段 / 像素所必需的。
在内置着色器中,您可以使用该函数执行此UnityObjectToClipPos操作,但它已重命名为TransformObjectToHClip(您可以在 SRP 核心 SpaceTransforms.hlsl 中找到)。也就是说,还有另一种方法可以处理 URP 中的转换,如下所示,它也可以更容易地转换到其他空间。
GetVertexPositionInputs 计算每个常用空间中的位置。它曾经是 Core.hlsl 的一部分,但在 URP v9 中被分离到它自己的文件 – ShaderVariablesFunctions.hlsl 中,但是当我们无论如何包含 Core.hlsl 时都会自动包含该文件。
该函数使用来自 的对象空间位置作为Attributes输入并返回一个VertexPositionInputs结构,其中包含:
  • positionWS: 在世界空间中的位置
  • positionVS: 在视图空间中的位置
  • positionCS: 在剪辑空间中的位置
  • positionNDC:标准化设备坐标中的位置,又名屏幕位置。(0,0) 在左下角,(w,w) 在右上角。值得注意的是,我们会将位置传递给片段阶段,然后处理透视划分 ( positionNDC.xy / positionNDC.w),因此 (1,1) 改为右上角。
对于我们当前的无光照着色器,我们不需要这些其他坐标空间,但是这个函数对于我们需要的着色器很有用。未使用的也不会包含在编译的着色器中,因此没有任何不必要的计算。
顶点着色器还负责将数据传递给片段,例如纹理坐标 (UV) 和顶点颜色。正如着色器介绍帖子中所讨论的,这些值会在三角形上进行插值。对于 UV,我们可以OUT.uv = IN.uv;假设两者都在结构中设置为float2,但通常包含 Tiling 和 Offset 值的纹理,Unity 将其传递到 a 中,float4纹理名称为 + _ST(s 指的是比例,t 指的是平移). 在这种情况下,_BaseMap_ST它也包含在我们之前的 UnityPerMaterial CBUFFER 中。为了将其应用于 UV,我们可以这样做:
TRANSFORM_TEX也可以使用宏来代替,它包含在 Built-in RP 和 URP 中。
虽然我们的 Unlit 着色器不需要任何法线 / 切线数据,但也GetVertexNormalInputs可以获取法线、切线和生成的双切线向量的世界空间位置。
稍后需要照明时,这将很有用。还有一个仅采用 的函数版本normalOS,它留下tangentWSas(1,0,0)bitangentWSas (0,1,0),或者您可以改用它positionWS = TransformObjectToWorldNormal(IN.normalOS),如果不需要切线 / 副切线(例如,没有法线 / 凹凸或视差贴图效果),这将很有用。

片段着色器

片段着色器负责确定像素输出的颜色(包括 alpha)。对于无光照着色器,这可以是相当简单的纯色或从输入纹理采样获得的颜色。对于光照着色器,它有点复杂,但 URP 提供了一些方便的函数,我将在光照部分介绍这些函数。
现在,因为我们的着色器是 Unlit,所以我们需要的是:
这将生成一个着色器,该着色器基于采样_BaseMap纹理输出 half4 颜色,该纹理也由_BaseColor属性和插值顶点颜色着色。该SAMPLE_TEXTURE2D宏由 ShaderLibrary 提供并返回给定 uv 坐标处的颜色,因为着色器按片段 / 像素运行。
如 FragmentOutput 部分所述,SV_Target用于将片段 / 像素颜色写入当前渲染目标。
我们可能还想做的事情是,如果像素的 alpha 值低于某个阈值,则丢弃像素,这样整个网格就不可见了——例如,四边形上的草 / 树叶纹理。这可以在不透明和透明着色器中完成,通常称为 Alpha Clip/Cutout/Cutoff。如果您熟悉 Shader Graph,它是用 Alpha Clip Threshold 处理的。在着色器代码中,这通常涉及一个名为 Float 的属性_Cutoff(添加到 Shaderlab 属性以及用于 SRP Batcher 兼容性的 UnityPerMaterial CBUFFER)。然后可以在片段着色器中使用它:
这基本上是 Unlit 着色器代码的完成。

关键字和着色器变体

在我们讨论光照之前,我们需要先谈谈关键字和着色器变体。在着色器中,我们可以指定#pragma multi_compile#pragma shader_feature指令,这些指令用于指定用于将着色器代码的某些部分 “打开” 或“关闭”切换的关键字。着色器实际上被编译成多个版本的着色器,称为着色器变体。在 Unity 中,我们可以启用和禁用每个材质的关键字来选择使用哪个变体。
这很有用,因为它允许我们编写单个着色器,但创建它的不同版本并关闭某些功能以节省性能。然而,这需要小心使用,因为不同的着色器变体不会一起批处理。URP 使用其中一些关键字来切换功能,如照明(即#pragma multi_compile _ _MAIN_LIGHT_SHADOWSv11 之前)和雾(使用稍微特殊的#pragma multi_compile_fog,与内置 RP 相同)。

Multi Compile

在此示例中,我们将生成着色器的三个变体,其中 _A、_B 和 _C 是关键字。然后我们可以使用#if defined(KEYWORD)/#ifdef KEYWORD来确定关键字切换了哪个代码。例如 :
URP 使用了一堆 multi_compiles,但这里是一些常见的。并非每个着色器都需要包含所有这些,但 ShaderLibrary 中的某些函数依赖于包含这些关键字,否则它们可能会跳过计算。

Shader Feature

Shader Features 类似于 Multi-Compile,但会生成一个附加变体,所有关键字都被禁用,任何未使用的变体都不会包含在最终构建中。这对于减少构建时间很有用,但在运行时启用 / 禁用这些关键字并不好,因为它需要的着色器可能不包含在构建中!如果您需要在运行时处理关键字,则应改用 multi_compile。
上面的代码生成三个变体,其中 _A 和 _B 是关键字。虽然只有两个关键字,但也会生成两个都被禁用的附加变体。使用 Multi-Compile 时,我们也可以这样做,方法是使用一个或多个下划线 ( _) 将第一个关键字指定为空白。例如

着色器变体

随着每个添加的 multi_compile 和 shader_feature,它会为启用 / 禁用关键字的每个可能组合生成越来越多的着色器变体。以下面的例子:
此处,第一行生成 3 个着色器变体。但是第二行,需要为那些已经启用了 _D 或 _E 的变体生成 2 个着色器变体。所以,A & D、A & E、B & D、B & E、C & D 和 C & E。现在有 6 个变体。
第三行,是这 6 个中的每一个的另外 2 个变体,所以我们现在总共有 12 个着色器变体。(虽然它只是一个关键字,但它有一个禁用的附加变体,因为该行是一个 shader_feature。其中一些变体可能也不会包含在构建中,具体取决于材料使用的内容)
每个添加带有 2 个关键字的 multi_compile 都会使产生的变体数量加倍,因此包含其中 10 个的着色器将产生 1024 个着色器变体!它需要编译每个需要包含在最终构建中的着色器变体,因此会增加构建时间以及构建的大小。
如果您想查看一个着色器产生了多少着色器变体,请单击该着色器,然后在检查器中有一个 “编译并显示代码” 按钮,旁边是一个小的下拉箭头,其中列出了包含的变体的数量。如果单击“跳过未使用的 shader_features”,则可以切换以查看变体总数。
为了帮助减少产生的变体数量,这些指令还有 “顶点” 和“片段”版本。例如 :
在此示例中,_A 和 _C 关键字仅用于顶点程序,而 _B 和 _D 仅用于片段。Unity 告诉我们这会产生 2 个着色器变体,尽管它更像是一个两者都被禁用的着色器变体和两个 “半” 变体,当您查看实际编译的代码时它似乎。
文档包含有关着色器变体的更多信息。

关键字限制

一个重要的注意事项是每个项目最多有 256 个全局关键字,因此最好遵循其他着色器的命名约定以确保重复使用相同的关键字而不是定义新关键字。
您还会注意到,对于许多 Multi-Compile,第一个关键字通常只保留为 “_”。通过将关键字留空,可以为 256 个最大值中的其他关键字留出更多可用空间。对于着色器功能,这是自动完成的。
我们还可以通过使用 and 的本地版本来避免用完最大关键字数。这些生成的关键字是该着色器的本地关键字,但每个着色器最多也有 64 个本地关键字。multi_compileshader_feature

灯光介绍

在内置管道中,需要照明 / 着色的自定义着色器通常由 Surface Shaders 处理。这些可以选择使用哪种照明模型,基于物理的标准 / 标准镜面Lambert(漫反射)和 BlinnPhong(镜面)模型。您还可以编写自定义照明模型,例如,如果您想生成卡通着色结果,您可以使用它。
Universal RP 不支持表面着色器,但是 ShaderLibrary 确实提供了帮助我们处理大量光照计算的函数。这些包含在 Lighting.hlsl 中——(它不会自动包含在 Core.hlsl 中,它必须单独包含)。
光照文件中甚至还有函数可以为我们完全处理光照,包括 UniversalFragmentPBRUniversalFragmentBlinnPhong。这些函数确实很有用,但仍然涉及一些设置,例如需要传递给函数的 InputData 和 SurfaceData 结构。
我们需要一堆公开的属性(也应该添加到 CBUFFER),以便能够将数据发送到着色器并根据材质更改它。您可以检查模板以了解使用的确切属性 - 例如 PBRLitTemplate
在包含 Lighting.hlsl 文件之前还需要定义关键字,以确保函数处理我们想要的所有计算,例如阴影和烘焙光照。着色器通常还包含一些着色器功能关键字(下面未包括但请参阅模板)以便能够切换功能,例如避免不必要的纹理样本并使着色器更便宜。

Surface Data & Input Data

这两个UniversalFragmentPBR/UniversalFragmentBlinnPhong函数都使用两个结构来传递数据:SurfaceDataInputData
SurfaceData 结构负责采样纹理并提供与您在 URP/Lit 着色器上找到的相同的输入具体包含以下内容:
请注意,您不需要包含此代码,因为此结构是 ShaderLibrary 的一部分,我们可以改为包含它所在的文件。在 v10 之前,该结构存在于 SurfaceInput.hlsl 中,但函数存在于 Lighting.hlsl 中并没有实际使用它。
虽然您仍然可以使用该结构,但您需要执行以下操作:
在 v10+ 中,结构移到了它自己的文件 SurfaceData.hlsl 中,并且UniversalFragmentPBR更新了函数,因此我们可以简单地传递两个结构(对于函数,UniversalFragmentBlinnPhong在 v12 中添加了 SurfaceData 版本,但当前版本需要拆分它。示例稍后展示)。
不过,我们仍然可以包含 SurfaceInput.hlsl ,因为 SurfaceData.hlsl 也将自动包含在该文件中,并且它还包含我们的_BaseMap_BumpMap和纹理定义以及一些有助于对它们进行采样的函数。_EmissionMap当然,我们仍然需要包含 Lighting.hlsl 才能访问这些功能。
InputData 结构用于传递光照计算所需的一些额外内容在 v10 中,包含以下内容:
同样,我们不需要包含此代码,因为它已经在 Input.hlsl 中,并且当我们 #include Core.hlsl 时它会自动包含在内。
由于照明功能使用这些结构,我们需要创建它们并设置它包含的每个变量。为了更有条理,我们应该在单独的函数中执行此操作,然后在片段着色器中调用它们。函数的确切内容可能会略有不同,具体取决于照明模型的实际需要。
现在我将函数留空,以便首先更好地了解文件的结构。接下来的几节将介绍InitializeSurfaceDataInitializeInputData函数的内容。
据我所知,这些功能是否无效也不太重要。我们可以改为返回结构本身。我有点喜欢这种方式,但我想我会尝试使其与 URP/Lit 着色器代码的外观更加一致。
如果您想进一步组织事情,我们还可以将所有功能移动到单独的 .hlsl 文件中并使用 a#include来实现。这还允许您为多个着色器重用该代码,如果需要支持 Meta pass(将在后面的部分中详细讨论)。至少,我建议有一个 hlsl 文件包含InitializeSurfaceData它是必需的函数 / 纹理定义。

InitializeInputData

如前所述,我们的InitializeInputData函数需要设置 InputData 结构中的每个变量,但这主要是获取从顶点阶段传递过来的数据并使用一些宏和函数(例如,为了处理空间之间的转换)。
该结构对于所有光照模型也可以相同,但我相信您可以省略一些部分,例如,如果您不支持烘焙光照或 shadowMask。重要的是要注意 InputData 结构中的所有内容都需要初始化,因此函数中的第一行将所有内容初始设置为 0 以避免错误。你需要小心,不要错过任何重要的事情。如果在未来对 ShaderLibrary 的更新中将额外的变量添加到结构中,它还有助于防止着色器中断。
了解这里的每个函数有点困难,所以我希望其中的大部分内容是不言自明的。唯一可能不太清楚的是 normalizedScreenSpaceUV,它目前仅用于稍后对 Screen Space Ambient Occlusion 纹理进行采样。如果您不需要支持,可以将其省略,但包含它也无妨。如果未使用,编译器可能无论如何都会将其删除。
另外,如果不清楚,bakedGI请参考 Baked Global Illumination(烘焙光照),shadowMask具体指的是何时将其设置为 Shadowmask 模式,因为随后会使用额外的阴影遮罩纹理。和宏会根据特定的关键字在编译时发生变化SAMPLE_GI您可以在 Lighting.hlsl(在 v12 中拆分 / 移动到 GlobalIllumination.hlsl)和 URP ShaderLibrary 的 Shadows.hlslSAMPLE_SHADOWMASK中找到这些函数。

简单的照明

URP / SimpleLit 着色器使用 Lighting.hlsl 中的 UniversalFragmentBlinnPhong 函数,它使用 Lambert 和 Blinn-Phong 光照模型。如果您不熟悉它们,我相信网上有更好的资源,但我会尝试快速解释一下:
Lambert 模型是一个完全漫反射表面,其中光在所有方向上反射。这涉及到光方向和法向量之间的点积(均归一化)。
Phong 模型是表面的镜面部分,其中当视线方向与由法线反射的光向量对齐时,光反射得更多。 Blinn-Phong 是一种轻微的改变,它不使用反射向量,而是使用光向量和视线方向之间的半向量,这更具计算效率。
虽然了解如何计算这些光照模型可能很有用,但它们可以由 URP ShaderLibrary 中的函数为我们处理。UniversalFragmentBlinnPhong 函数使用包含在 Lighting.hlsl 中的 LightingLambert 和 LightingSpecular(blinn-phong 模型)函数,它们是:
我们可以通过包含 Lighting.hlsl 来调用这些函数,或者将代码复制出来,但由于 UniversalFragmentBlinnPhong 为我们完成了这些操作,因此我们可以使用它。但是,我们仍需要传入这两个结构体。我们在前面的部分中已经讨论了 InitializeInputData 函数,但对于 InitializeSurfaceData 函数,它可能会略有不同,具体取决于我们需要支持的内容(例如 Blinn-Phong 不像 PBR 那样使用金属度)。我使用以下代码:
如前所述,在片段着色器中我们可以调用所有这些函数:
有关完整示例,请参阅 URP_SimpleLitTemplate

PBR 照明

URP / Lit 着色器使用更准确的物理渲染(PBR)模型,它基于 Lambert 和 Minimalist CookTorrance 模型。根据 ShaderLibrary,确切的实现略有不同。如果感兴趣,可以查看 Lighting.hlsl 中的 LightingPhysicallyBased 函数和 BRDF.hlsl 中的 DirectBRDFSpecular 函数来了解其实现方式。
但是,我们不一定需要了解其实现方式才能使用它,我们只需调用 UniversalFragmentPBR 函数即可。如正如 v10 + 版本中提到的,它需要两个结构体 InputData 和 SurfaceData。我们已经在前面的几节中讨论了创建 InitializeInputData 函数。对于 InitializeSurfaceData,我们可以使用:
然后在片段着色器中:

其他 Passes

Universal RP 使用的还有其他 Pass,例如 ShadowCaster、DepthOnly、DepthNormals(v10+)和 Meta Passes。我们还可以使用自定义的 LightMode 标签创建 Passes,这在之前的 Multi-Pass 部分中已经讨论过了。

ShadowCaster

标记为 “LightMode” =“ShadowCaster” 的 Pass 负责允许对象投射实时阴影。
在前面的部分中,我提到可以使用 UsePass 触发着色器使用来自不同着色器的 Pass,但由于这会破坏 SRP 批处理的兼容性,我们需要在着色器本身中定义该 Pass。
我发现最简单的处理方式是让 ShadowCasterPass.hlsl 为我们完成工作(由 URP / Lit 等着色器使用)。它包含 Attributes 和 Varyings 结构体和相当简单的 Vertex 和 Fragment 着色器,处理阴影偏差偏移和 alpha 剪切 / 切割。
URP/Lit 着色器通常包括 LitInput.hlsl,但是它定义了我们的着色器可能不会使用的许多纹理(无论如何都可能被忽略 / 编译出来)并且它还包括UnityPerMaterial CBUFFER我们已经在我们的HLSLINCLUDE. 这会导致重新定义错误,因此我改为包含 LitInput.hlsl 包含的一些 ShaderLibrary 文件,以确保传递仍然正常运行而不会出错。
包含 CommonMaterial.hlsl 主要是因为 Shadows.hlsl 在对阴影贴图进行采样时使用了 LerpWhiteTo 函数。SurfaceInput.hlsl 包含在内,因为 ShadowCasterPass.hlsl 使用_BaseMapSampleAlbedoAlpha函数来支持 alpha 裁剪 / 剪切。
使用此 ShadowCaster,我们的着色器还应包含_BaseMap_BaseColor_Cutoff属性。如果它们不包含在内,那么它不会出错,因为它将使用它们代替全局着色器属性。
如果我们的主着色器使用顶点位移,我们也需要在 ShadowCaster pass 中处理它,否则阴影不会移动。这涉及到将顶点着色器替换为自定义着色器,例如:

DepthOnly

标记为 “LightMode” =“DepthOnly” 的 Pass 负责将对象的深度写入相机深度纹理 - 特别是当深度缓冲区无法被复制或启用 MSAA 时。如果您的着色器是不透明的,并在主 Pass 中使用 ZWrite On,则应该包含一个 DepthOnly pass,无论它是否被光照 / 非光照。透明着色器也可以包含它,但由于深度纹理是在绘制透明对象之前生成的,它们不会出现在其中。
DepthOnly pass 与上面的 ShadowCaster 几乎相同,但它不在顶点着色器中使用阴影偏差偏移量(使用常规的 TransformObjectToHClip(IN.positionOS.xyz)而不是 GetShadowPositionHClip(input))。
与上面类似,我们可以使用 URP / Lit 等着色器使用的 DepthOnlyPass.hlsl 来为我们定义 Attributes 和 Varyings 结构体以及 Vertex 和 Fragment 着色器。
同样,如果我们想要支持顶点位移,我们需要一个自定义顶点着色器:

DepthNormals

标记为 “LightMode” =“DepthNormals” 的 Pass 负责将对象的深度写入相机深度纹理,并在摄像机的前向 / 通用渲染器上的 Renderer Feature 请求时将法线写入相机法线纹理。
例如,屏幕空间环境光遮蔽功能可以支持使用深度法线作为其来源,或者可以从深度重建法线(因此请改用 DepthOnly pass),这样可以避免创建一个额外的缓冲区 / 渲染纹理来存储_CameraNormalsTexture。
如果您确实确定不需要 SSAO 或其他可能使用它的功能,您可以排除该 pass,但我建议仍然支持它,以避免对象未出现在深度和法线纹理中时产生混淆!
与之前的 pass 类似,我们可以使用 DepthNormalsPass.hlsl 文件。
值得一提的是,较新版本的 URP (v12) 改用 LitDepthNormalsPass.hlsl,它支持使用法线贴图和细节法线贴图,以及视差 / 高度贴图(也需要在上述代码中注释的额外关键字)。

Meta

标记为的通道"LightMode"="Meta"在烘焙全局照明时使用。如果您不使用烘焙 GI,则可以忽略此过程。
对于 Unlit 着色器,您可能需要研究使用与上述过程类似的 UnlitMetaPass.hlsl 。
对于 Lit 着色器,我们可能会使用 LitMetaPass.hlsl,但它需要一个InitializeStandardLitSurfaceData与我们正在使用的函数不完全相同的函数,而且我的 PBR 示例还包括顶点颜色,因此我们也需要更改 Varyings。相反,我最终使用了这个:
PBRSurface.hlsl与着色器文件位于同一文件夹中的自定义 HLSL 文件在哪里。它包含InitializeSurfaceDataPBR 光照部分中使用的函数(以及 SurfaceInput.hlsl 包含、纹理 / 采样器定义和所需的函数,例如InitializeSurfaceDataSampleMetallicSpecGloss。UniversalForwardSampleOcclusion通道还包含该文件,而不是在着色器中包含该代码。
如果您已经读到这里,谢谢!最后一部分总结了 URP 和内置 RP 之间的所有区别——主要针对那些已经熟悉手撸着色器代码的人,但仍然是对已经讨论过的所有内容的有用总结。

内置管线(BRP)与 URP 差异总结

ShaderLab

  • URP 中的 SubShader 使用 “RenderPipeline” =“UniversalPipeline” 标签
  • URP 中的 Passes 使用的一些 “LightMode” 标签与 Built-In 不同。最常见的是 “UniversalForward” 或完全省略(默认为“SRPDefaultUnlit”)。请参阅 LightMode 标签部分以获取列表。
  • 仅渲染第一个 UniversalForward Pass。在 URP 中使用 SRPDefaultUnlit 支持多通道着色器的附加通道,但它会破坏 SRP Batcher 的兼容性,因此不建议使用。请参阅 Multi-Pass 部分以获取替代方法(例如第二个材质或 RenderObjects 功能)。
  • URP 不支持 GrabPass。相反,在渲染不透明和透明对象之间捕获相机不透明纹理,可用于透明队列中的着色器的某些扭曲 / 折射效果。#include DeclareOpaqueTexture.hlsl 文件并使用其 SampleSceneColor 函数,将 ScreenPos(positionNDC)用作 UV 输入。其他透明对象不会出现在纹理中。如果您需要它,另一种选择可能是使用 Custom Renderer 功能将加法扭曲对象渲染到屏幕缓冲区中,然后使用 CommandBuffer.Blit 扭曲最终屏幕结果。这个想法类似于这个 Makin’ Stuff Look Good in Unity 视频,但那里使用的代码仍然是针对 Built-In 的。

HLSL

  • 应始终使用 HLSLPROGRAM 和 ENDHLSL 而不是 CGPROGRAM/ENDCG。这是因为后者包含一些与 URP ShaderLibrary 冲突的附加文件,导致重定义错误。
  • “fixed”类型 / 精度在 HLSLPROGRAM 中不存在,请使用 “half” 代替。
  • URP 不支持表面着色器 ( #pragma surface),仅支持顶点 / 片段样式着色器。(也仍然支持几何和外壳 / 域)
  • 用于在顶点和片段着色器之间传递数据的结构在 URP 中通常称为 Attributes 和 Varyings,而不是 appdata 和 v2f。这主要是一个命名约定,但并不重要。
  • 不包含 UnityCG.cginc,而是使用 URP ShaderLibrary。要包括的主要内容是:
  • SRP Batcher 对绘制调用之间的设置进行批处理,因此使用同一个着色器渲染多个对象的成本更低。它甚至可以对具有不同材质但不具有不同着色器 / 着色器变体的对象进行批处理。为了使着色器与此兼容,它必须包含 URP ShaderLibrary 并包含一个 UnityPerMaterial CBUFFER,其中包含每个公开的 ShaderLab 属性(纹理除外)。它不能包含全局着色器变量或兼容性中断。您可以从检查器检查着色器是否兼容。CBUFFER 也必须对所有着色器通道保持不变,因此建议将其放在 SubShader 中的 HLSLINCLUDE 内。例如 :
  • 取而代之的是_MainTex,URP 倾向于使用_BaseMap。这主要只是命名约定的差异,并且不太重要,除非您包含为您定义反照率、凹凸和发射纹理的 SurfaceInput.hlsl。_MainTex仍然应该用于使用CommandBuffer.Blit(即 Blit Render Feature)和从 SpriteRenderer 组件获取精灵的图像效果。
  • URP 提供了定义纹理的宏,并使用 DX10+ 风格的语法,分别定义了一个纹理和采样器:
  • 并用于采样纹理:
  • 对于其他纹理类型(即 Texture2DArray、Texture3D、TextureCube、TextureCubeArray),请参阅纹理对象部分以获取其他宏。
  • URP 包含一个称为的函数GetVertexPositionInputs,可以在顶点着色器中使用它来轻松获得到其他空间的变换。任何未使用的都不会被计算,所以使用它非常方便。例如 :
  • 同样,有一个GetVertexNormalInputs,获取世界空间法线(normalWS),以及世界空间切线(tangentWS)和副切线(bitangentWS)。如果你只需要正常的,你TransformObjectToWorldNormal也可以使用。

Keywords (关键字)

URP 中的着色器通常将这些关键字用于 Lit 着色器:
如果是非光照着色器,则可能只需要雾和实例化。
还有一堆 shader_feature,着色器可以包含它们,你可以查看常见的模板(如_NORMALMAP), 但它们取决于着色器,除非支持关键字的功能,否则不应该总是包括它们。

常用函数 / 宏 - BRP/URP 对照

内置(BRP)
URP 对应
TRANSFORM_TEX(uv, textureName)
TRANSFORM_TEX(uv, textureName)
tex2D, tex2Dlod, 等
SAMPLE_TEXTURE2D, SAMPLE_TEXTURE2D_LOD, 等等 见上
UnityObjectToClipPos(positionOS)
TransformObjectToHClip(positionOS), 或使用 GetVertexPositionInputs().positionCS
UnityObjectToWorldNormal(normalOS)
TransformObjectToWorldNormal(normalOS), 或使用 GetVertexNormalInputs().normalWS
ComputeScreenPos(positionCS)
ComputeScreenPos(positionCS),尽管在 Unity 2021 / URP v11+ 中已弃用。应该 GetVertexPositionInputs().positionNDC 改用
ComputeGrabScreenPos(positionCS)
URP 不支持 GrabPass
WorldSpaceViewDir(positionOS)
计算 positionWS 并改用以下函数
UnityWorldSpaceViewDir(positionWS)
GetWorldSpaceViewDir(positionWS)(添加到 v9+ 中的 ShaderVariablesFunctions.hlsl)。对于之前的版本,请将其复制出来。如果你需要它归一化,可以改用 GetWorldSpaceNormalizeViewDir(positionWS)。
WorldSpaceLightDir(positionOS)
见下文
UnityWorldSpaceLightDir(positionWS)/_WorldSpaceLightPos0
对于 Main Directional Light,使用 GetMainLight().direction. 请参见 Lighting.hlsl
Shade4PointLights(…)
确实没有直接等效项,但内置将其用于 Forward 中的顶点照明,请参见下文。
ShadeVertexLights(vertex, normal)
VertexLighting(positionWS, normalWS) 在 Lighting.hlsl 中
ShadeSH9(half4(worldNormal,1))
SampleSH(normalWS),但在 Lighting.hlslSAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS) 中使用宏 / SampleSHVertex/SampleSHPixel 函数。例如,参见 LitForwardPass.hlsl
UNITY_FOG_COORDS(n)
float fogFactor : TEXCOORDn
UNITY_TRANSFER_FOG(o, positionCS)
OUT.fogFactor = ComputeFogFactor(positionCS.z)
UNITY_APPLY_FOG(fogCoord, color, fogColor)
color.rgb = MixFog(color.rgb, fogCoord)
UNITY_APPLY_FOG_COLOR(fogCoord, color)
color.rgb = MixFogColor(color.rgb, fogColor.rgb, fogCoord)
Linear01Depth(z)
Linear01Depth(z, _ZBufferParams)
LinearEyeDepth(z)
LinearEyeDepth(z, _ZBufferParams)
ParallaxOffset(h, height, viewDirTS)
ParallaxOffset1Step(h, amplitude, viewDirTS) 如果在 v10.1+ 中(对于之前的版本,复制功能)。请参见 ParallaxMapping.hlsl
Luminance(rgb)
Luminance(rgb), 请参见 Color.hlsl
V2F_SHADOW_CASTER
等价物大致只是 float4 positionCS : SV_POSITION; 但请参阅 ShadowCaster 部分。
TRANSFER_SHADOW_CASTER_NORMALOFFSET
请参见 ShadowCasterPass.hlslGetShadowPositionHClip(input) 中的示例,另请参见上文
SHADOW_CASTER_FRAGMENT
return 0;
SHADOW_COORDS(1)
float4 shadowCoord : TEXCOORD1;
TRANSFER_SHADOW(o)
TransformWorldToShadowCoord(inputData.positionWS)
SHADOW_ATTENUATION(i)
MainLightRealtimeShadow(shadowCoord),虽然 GetMainLight(shadowCoord) 也会处理它。请参见 Lighting.hlsl 和 Shadows.hlsl
(如果这里没有列出内置的任何常用功能,请告诉我,我会考虑添加它们!)

Templates(模版)

你可以通过访问 >> github 上的库来获取已写好的一些模版. 主要包括了 :
  • Opaque Unlit Shader Template
  • Transparent Unlit Shader Template
  • Opaque Unlit+ Shader Template
  • (includes optional Alpha Clipping and ShadowCaster, DepthOnly & DepthNormals passes)
  • Diffuse Lit Shader Template
  • (Ambient / Baked GI & Lambert Diffuse shading from Main Directional Light only)
  • Simple Lit Shader Template
  • (Lambert Diffuse & Blinn-Phong Specular. Uses UniversalFragmentBlinnPhong method from Lighting.hlsl, similar to URP/SimpleLit shader)
  • PBR Lit Shader Template
  • (Physically Based Rendering lighting model. Uses UniversalFragmentPBR method from Lighting.hlsl, similar to URP/Lit shader. Note, doesn’t include height/parallax mapping, detail maps or clear coat. Split into URP_PBRLitTemplate.shader, PBRSurface.hlsl and PBRInput.hlsl for organisation & Meta pass support)
以上就是本篇的全部内容,希望你有所收获。有错误的地方,还望海涵,也可以在评论区帮忙指正。感谢。

参考资料


▎本文由 简悦 SimpRead 转码。