4.3 着色器编程 (Shader Graph, HLSL)


文档摘要

4.3 着色器编程 (Shader Graph, HLSL) 4.3 着色器编程 (Shader Graph, HLSL) 着色器(Shader)是现代图形渲染管线中不可或缺的一部分,它们决定了3D模型在屏幕上呈现的外观。在Unity3D中,我们有两种主要的着色器编程方式:Shader Graph 和 HLSL (High-Level Shading Language)。Shader Graph 提供了一种可视化的、节点式的编程方式,而 HLSL 则是一种更底层、基于代码的编程语言,两者各有优势,并且在Unity开发中常常结合使用。 4.3.1 着色器概述与重要性 在深入 Shader Graph 和 HLSL 之前,理解着色器的基本概念至关重要。

4.3 着色器编程 (Shader Graph, HLSL)

4.3 着色器编程 (Shader Graph, HLSL)

着色器(Shader)是现代图形渲染管线中不可或缺的一部分,它们决定了3D模型在屏幕上呈现的外观。在Unity3D中,我们有两种主要的着色器编程方式:Shader Graph 和 HLSL (High-Level Shading Language)。Shader Graph 提供了一种可视化的、节点式的编程方式,而 HLSL 则是一种更底层、基于代码的编程语言,两者各有优势,并且在Unity开发中常常结合使用。

4.3.1 着色器概述与重要性

在深入 Shader Graph 和 HLSL 之前,理解着色器的基本概念至关重要。简单来说,着色器是一段运行在GPU(图形处理器)上的小程序,它负责处理渲染管线中的顶点和像素数据,最终决定屏幕上每个像素的颜色。

着色器的重要性体现在以下几个方面:

  • 视觉表现力: 着色器是实现各种视觉效果的核心。无论是简单的颜色填充、纹理贴图,还是复杂的光照模型、材质效果(如金属、玻璃、水面)、后期处理特效,都离不开着色器的编写。

  • 性能优化: 高效的着色器能够显著提升渲染性能。通过精细地控制GPU的计算过程,可以减少不必要的计算,提高帧率,尤其是在移动平台或性能受限的设备上,着色器的优化至关重要。

  • 艺术创作的自由度: 着色器赋予艺术家和开发者极大的创作自由。通过自定义着色器,可以突破标准渲染管线的限制,实现独特的视觉风格和效果,创造出更具吸引力和个性化的游戏或应用。

渲染管线中的着色器阶段:

现代渲染管线通常包含多个着色器阶段,其中最核心的是:

  • 顶点着色器 (Vertex Shader): 处理模型的顶点数据。它负责顶点变换(模型空间到世界空间、世界空间到观察空间、观察空间到裁剪空间),以及进行一些基于顶点的计算,例如顶点动画、法线变换等。顶点着色器的输出是裁剪空间中的顶点位置,以及传递给后续阶段的数据(例如法线、UV坐标、颜色等)。

  • 片元着色器 (Fragment Shader / Pixel Shader): 处理光栅化后的片元(可以理解为像素的候选者)。它负责计算每个片元的最终颜色。片元着色器接收来自顶点着色器插值后的数据,并进行纹理采样、光照计算、材质属性应用等操作,最终输出该片元的颜色值。

理解了顶点着色器和片元着色器的作用,就为后续学习 Shader Graph 和 HLSL 打下了基础。

4.3.2 Shader Graph:可视化着色器编程

Shader Graph 是 Unity 官方推出的可视化着色器编辑器,它使用节点连接的方式来创建和编辑着色器,无需编写代码。Shader Graph 的出现极大地降低了着色器编程的门槛,使得艺术家、设计师以及不熟悉代码的开发者也能轻松创建自定义着色器。

Shader Graph 的优势:

  • 可视化编程: 通过拖拽、连接节点,直观地构建着色器逻辑,无需记忆复杂的语法和代码结构。

  • 实时预览: 在 Shader Graph 编辑器中,可以实时预览着色器的效果,方便快速迭代和调整。

  • 易于学习和上手: 对于初学者来说,Shader Graph 比 HLSL 更容易入门,可以快速理解着色器编程的基本概念和流程。

  • 快速原型设计: Shader Graph 非常适合快速原型设计和实验各种视觉效果。

  • 与 Unity 编辑器深度集成: Shader Graph 与 Unity 编辑器无缝集成,可以方便地在材质球中使用和管理。

Shader Graph 的基本操作和界面:

  1. 创建 Shader Graph 资源: 在 Project 窗口中,右键单击,选择 Create -> Shader Graph -> URP (或 Built-in / HDRP,取决于你的渲染管线) -> Unlit GraphLit Graph

    • Unlit Graph:用于创建不受光照影响的着色器,例如 UI 元素、特效等。

    • Lit Graph:用于创建受光照影响的着色器,适用于大部分 3D 模型材质。

  2. Shader Graph 编辑器界面: 双击创建的 Shader Graph 资源,打开 Shader Graph 编辑器。界面主要分为几个区域:

    • Blackboard (黑板): 用于声明和管理着色器的属性 (Properties),例如颜色、纹理、数值等。这些属性可以在材质球中进行调整。

    • Main Preview (主预览): 实时显示着色器效果的预览窗口。

    • Graph Inspector (图检查器): 显示当前选中节点的属性和设置。

    • Node Palette (节点面板): 包含各种着色器节点,例如纹理采样、数学运算、颜色操作、光照模型等。

    • Graph Editor (图编辑器): 主要的节点编辑区域,通过连接节点构建着色器逻辑。

    • Master Node (主节点): Shader Graph 的输出节点,决定着色器的最终输出类型(例如表面着色器、片元着色器等)。

  3. 添加和连接节点: 在 Node Palette 中搜索或浏览需要的节点,拖拽到 Graph Editor 中。通过拖拽节点上的连接点 (Sockets) 来建立节点之间的连接,数据流从左向右流动。

Shader Graph 常用节点类型:

  • Input (输入节点):

    • Texture Sample:采样纹理。

    • Color:颜色输入。

    • Vector:向量输入 (Vector1, Vector2, Vector3, Vector4)。

    • Slider / Float / Integer:数值输入。

    • Time:时间节点,用于创建动画效果。

    • Screen Position:屏幕坐标。

    • World Space / Object Space / View Space:不同坐标空间下的向量。

  • Math (数学运算节点):

    • Add / Subtract / Multiply / Divide:加减乘除。

    • Power / Square Root / Smoothstep / Lerp (线性插值) / Clamp (钳制)。

    • Sin / Cos / Tan / Abs / Floor / Ceil / Round

    • Dot Product (点积) / Cross Product (叉积) / Distance (距离) / Normalize (归一化)。

  • Channel (通道操作节点):

    • Combine:合并通道 (例如将 R, G, B 通道合并为 Color)。

    • Split:分离通道 (例如从 Color 中分离出 R, G, B, A 通道)。

    • Swizzle:通道重排 (例如 .rgb, .rgba, .grba 等)。

  • Artistic (艺术效果节点):

    • Posterize (海报化)。

    • Hue, Saturation, Value (色相、饱和度、亮度调整)。

    • Invert (反相)。

    • Contrast (对比度)。

  • Utility (实用工具节点):

    • If:条件判断。

    • Branch:分支选择。

    • Comment:添加注释。

    • Group:节点分组。

    • Sub Graph:创建子图,用于模块化和复用。

Shader Graph 代码实践示例 1:简单的纹理着色器

这个示例将创建一个简单的 Unlit Shader Graph,显示一张纹理。

步骤:

  1. 创建一个 Unlit Graph Shader Graph 资源,命名为 "SimpleTextureShader"。

  2. 打开 Shader Graph 编辑器。

  3. 在 Blackboard 中,点击 + 按钮,选择 Texture 2D,创建一个名为 "MainTex" 的纹理属性。将其 Reference 设置为 _MainTex (这是 HLSL 代码中访问该属性的变量名)。

  4. 在 Node Palette 中搜索 "Texture Sample" 节点,拖拽到 Graph Editor 中。

  5. 将 "MainTex" 属性节点从 Blackboard 拖拽到 "Texture Sample" 节点的 "Texture" 输入端。

  6. 在 Node Palette 中搜索 "Unlit Master" 节点,拖拽到 Graph Editor 中。

  7. 将 "Texture Sample" 节点的 "Color" 输出端连接到 "Unlit Master" 节点的 "Color" 输入端。

  8. 点击 Shader Graph 编辑器左上角的 "Save Asset" 保存着色器。

现在你可以创建一个材质球,选择 "SimpleTextureShader" 着色器,并将一张纹理赋值给 "MainTex" 属性,就可以看到模型被纹理着色了。

Shader Graph 代码实践示例 2:动画纹理偏移着色器

这个示例将在上一个示例的基础上,添加时间节点,实现纹理的水平方向动画偏移效果。

步骤:

  1. 在 "SimpleTextureShader" Shader Graph 中,添加一个 Time 节点。

  2. 添加一个 Multiply 节点。将 Time 节点的 "Time" 输出端连接到 Multiply 节点的 "A" 输入端,并在 Multiply 节点的 "B" 输入端创建一个 Float 节点,设置为一个较小的数值 (例如 0.1),控制动画速度。

  3. 添加一个 Vector2 节点,命名为 "TilingAndOffset",将其模式设置为 Property,并在 Blackboard 中创建同名属性 "TilingAndOffset"。将其默认值设置为 (1, 0)。

  4. 添加一个 Add 节点。将 "TilingAndOffset" 节点的 "Vector 2" 输出端连接到 Add 节点的 "A" 输入端,将 Multiply 节点的输出端连接到 Add 节点的 "B" 输入端的 X 分量(通过拖拽连线到输入端的小圆点选择分量)。

  5. 添加一个 "Tiling And Offset" 节点。将 Add 节点的输出端连接到 "Tiling And Offset" 节点的 "UV" 输入端,并将 "Tiling And Offset" 节点的输出端连接到 "Texture Sample" 节点的 "UV" 输入端。

  6. 保存 Shader Graph。

现在运行游戏,你会看到纹理在水平方向上循环滚动。你可以在材质球中调整 "TilingAndOffset" 属性来改变偏移量和动画效果。

4.3.3 HLSL:高级着色器编程语言

HLSL (High-Level Shading Language) 是微软开发的一种高级着色器语言,广泛应用于 DirectX 和 Unity 等平台。与 Shader Graph 的可视化编程不同,HLSL 是一种基于文本的编程语言,需要编写代码来定义着色器的行为。

HLSL 的优势:

  • 更强大的控制力: HLSL 提供了更底层的控制,可以实现 Shader Graph 无法实现的复杂效果和算法。

  • 更高的性能优化潜力: 对于性能敏感的应用,通过精细地编写 HLSL 代码,可以实现更极致的性能优化。

  • 更大的灵活性和扩展性: HLSL 可以自由地访问 GPU 的各种功能和资源,可以编写更复杂的着色器逻辑和算法。

  • 学习和理解渲染原理: 学习 HLSL 有助于深入理解图形渲染管线的原理和着色器的工作方式。

HLSL 着色器的基本结构:

一个基本的 HLSL 着色器文件通常包含以下几个部分:

  1. Properties (属性块): 声明可以在材质球中调整的属性,例如颜色、纹理、数值等。这些属性会暴露给 Unity 编辑器。

  2. SubShader (子着色器): 定义一个或多个子着色器块。一个着色器可以包含多个 SubShader,Unity 会根据平台和设备能力选择合适的 SubShader 执行。

  3. Pass (通道): 每个 SubShader 可以包含一个或多个 Pass。每个 Pass 定义了一次渲染过程,例如绘制阴影、应用光照、渲染透明物体等。

  4. CGPROGRAM / ENDCG (或 HLSLPROGRAM / ENDHLSL) 代码块: 在 Pass 内部,使用 CGPROGRAM (或 HLSLPROGRAM) 和 ENDCG (或 ENDHLSL) 标记 HLSL 代码块。

  5. #pragma 指令: 用于指定编译指令,例如指定着色器类型 (vertex, fragment)、包含文件、函数定义等。

  6. 结构体 (struct): 用于定义顶点着色器和片元着色器之间传递的数据结构 (例如 appdata_base, v2f_standard 等)。

  7. 变量声明和函数定义: 在 HLSL 代码块中,可以声明全局变量、定义函数 (例如顶点着色器函数 vert 和片元着色器函数 frag)。

  8. 顶点着色器函数 (vert): 处理顶点数据,输入通常是 appdata_base 结构体,输出通常是 v2f_standard 结构体。

  9. 片元着色器函数 (frag): 处理片元数据,输入通常是 v2f_standard 结构体,输出通常是 fixed4 (或 half4, float4) 类型的颜色值。

HLSL 代码实践示例 1:简单的颜色着色器

这个示例将创建一个最基本的 HLSL 着色器,输出一个固定的红色。

Shader "Custom/SimpleColorShader" { Properties { _Color ("Color", Color) = (1,0,0,1) // 红色默认值 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; fixed4 _Color; // 声明与 Properties 中 _Color 属性对应的变量 v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return _Color; // 输出 Properties 中定义的颜色 } ENDCG } } FallBack "Diffuse" }

代码详解:

  • Shader "Custom/SimpleColorShader":定义着色器路径和名称。

  • Properties { ... }:定义属性块,_Color ("Color", Color) = (1,0,0,1) 声明了一个名为 _Color 的颜色属性,显示名称为 "Color",默认值为红色。

  • SubShader { ... }:定义子着色器。

  • Tags { "RenderType"="Opaque" }:设置渲染类型为不透明。

  • LOD 100:设置 LOD 级别。

  • Pass { ... }:定义渲染通道。

  • CGPROGRAM ... ENDCG:HLSL 代码块。

  • #pragma vertex vert:指定顶点着色器函数为 vert

  • #pragma fragment frag:指定片元着色器函数为 frag

  • #include "UnityCG.cginc":包含 Unity 预定义的 CG 宏和结构体。

  • struct appdata { ... }:定义顶点着色器输入结构体 appdata,包含顶点位置 vertex

  • struct v2f { ... }:定义顶点着色器到片元着色器传递的结构体 v2f,包含裁剪空间顶点位置 vertex

  • fixed4 _Color;:声明与属性 _Color 对应的变量,类型为 fixed4 (低精度浮点数)。

  • v2f vert (appdata v) { ... }:顶点着色器函数 vert,将模型空间顶点坐标 v.vertex 转换为裁剪空间坐标 o.vertex

  • fixed4 frag (v2f i) : SV_Target { ... }:片元着色器函数 frag,直接返回属性 _Color 的值作为片元颜色。

  • FallBack "Diffuse":如果当前设备不支持该着色器,则回退到内置的 "Diffuse" 着色器。

HLSL 代码实践示例 2:菲涅尔效果着色器

菲涅尔效果 (Fresnel Effect) 描述了物体表面反射率随观察角度变化的现象,常用于模拟玻璃、水面、金属等材质的边缘高光效果。

Shader "Custom/FresnelShader" { Properties { _Color ("Color", Color) = (1,1,1,1) _FresnelColor ("Fresnel Color", Color) = (0,0,1,1) // 蓝色菲涅尔颜色 _FresnelPower ("Fresnel Power", Range(1, 10)) = 3 // 菲涅尔强度 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 viewDir : TEXCOORD1; }; fixed4 _Color; fixed4 _FresnelColor; float _FresnelPower; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); // 世界空间观察方向 return o; } fixed4 frag (v2f i) : SV_Target { float3 normalDir = normalize(i.worldNormal); float3 viewDir = normalize(i.viewDir); float fresnel = 1.0 - saturate(dot(viewDir, normalDir)); // 计算菲涅尔值 fresnel = pow(fresnel, _FresnelPower); // 菲涅尔强度控制 fixed4 baseColor = _Color; fixed4 fresnelEffect = _FresnelColor * fresnel; fixed4 finalColor = baseColor + fresnelEffect; // 基础颜色 + 菲涅尔效果 return finalColor; } ENDCG } } FallBack "Diffuse" }

代码详解:

  • 新增了 _FresnelColor (菲涅尔颜色) 和 _FresnelPower (菲涅尔强度) 属性。

  • appdata 结构体新增了法线 normal

  • v2f 结构体新增了世界空间法线 worldNormal 和世界空间观察方向 viewDir

  • 顶点着色器 vert 中计算了世界空间法线和观察方向,并传递给片元着色器。

  • 片元着色器 frag 中:

    • float fresnel = 1.0 - saturate(dot(viewDir, normalDir));:计算菲涅尔值,使用 1 减去观察方向和法线方向的点积的饱和值。点积越接近 1,表示视角越垂直于表面,菲涅尔值越小;点积越接近 0,表示视角越平行于表面,菲涅尔值越大。

    • fresnel = pow(fresnel, _FresnelPower);:使用 _FresnelPower 控制菲涅尔强度。

    • fixed4 finalColor = baseColor + fresnelEffect;:将基础颜色和菲涅尔效果颜色叠加,得到最终颜色。

将此着色器应用到材质球,并调整 _FresnelColor_FresnelPower 属性,即可看到菲涅尔边缘高光效果。

4.3.4 Shader Graph 与 HLSL 的结合

Shader Graph 和 HLSL 并非互相排斥,而是可以结合使用,发挥各自的优势。Shader Graph 适合快速搭建基础着色器结构和可视化调整参数,而 HLSL 适合编写复杂的自定义逻辑和算法。

结合方式:Custom Function 节点

Shader Graph 提供了 Custom Function 节点,允许用户在 Shader Graph 中调用自定义的 HLSL 函数。通过 Custom Function 节点,可以将复杂的 HLSL 代码集成到 Shader Graph 中,实现更高级的效果。

Shader Graph + HLSL 代码实践示例:菲涅尔效果 (Shader Graph + Custom Function)

我们将使用 Shader Graph 搭建基础结构,并使用 Custom Function 节点调用 HLSL 函数来计算菲涅尔效果。

  1. 创建 HLSL 函数文件: 在 Project 窗口中,右键单击,选择 Create -> Shader -> Standard Surface Shader (或任何其他 Shader 文件),将文件扩展名改为 .hlsl,例如 "FresnelFunction.hlsl"。

  2. 编写 HLSL 菲涅尔函数: 在 "FresnelFunction.hlsl" 文件中,编写以下 HLSL 函数:

float Fresnel(float3 normalDir, float3 viewDir, float power) { float fresnel = 1.0 - saturate(dot(viewDir, normalDir)); return pow(fresnel, power); }
  1. 创建 Shader Graph: 创建一个 Lit Graph Shader Graph 资源,命名为 "FresnelShaderGraph"。

  2. 在 Shader Graph 中使用 Custom Function 节点:

Shader Graph 步骤:

  • 创建 "BaseColor", "FresnelColor", "FresnelPower" 属性。

  • 添加 "View Direction (World Space)" 和 "Normal Vector (World Space)" 节点。

  • 添加 "Custom Function" 节点。

    • 在 "Node Settings" 面板中,选择 "File" 为 "FresnelFunction.hlsl","Function" 为 "Fresnel"。

    • 添加三个输入端口: "NormalDir" (Vector 3), "ViewDir" (Vector 3), "Power" (Float)。

    • 添加一个输出端口: "Out" (Float)。

  • 将 "Normal Vector (World Space)" 连接到 "Custom Function" 节点的 "NormalDir" 输入端。

  • 将 "View Direction (World Space)" 连接到 "Custom Function" 节点的 "ViewDir" 输入端。

  • 将 "FresnelPower" 属性连接到 "Custom Function" 节点的 "Power" 输入端。

  • 添加两个 "Multiply" 节点和一个 "Add" 节点,将 "BaseColor" 与 1 相乘,"FresnelColor" 与 "Custom Function" 的 "Out" 输出相乘,然后将两个 "Multiply" 节点的输出相加。

  • 将 "Add" 节点的输出连接到 "PBR Master" 节点的 "Albedo" 输入端。

  • 保存 Shader Graph。

现在,你创建了一个使用 Shader Graph 结构和 HLSL 函数计算菲涅尔效果的着色器。这种方式既利用了 Shader Graph 的可视化便利性,又扩展了 HLSL 的自定义能力。

4.3.5 着色器性能优化与调试

着色器的性能对于游戏帧率至关重要。以下是一些着色器性能优化的常用技巧:

  • 减少纹理采样次数: 纹理采样是 GPU 运算中相对耗时的操作。尽量减少不必要的纹理采样,例如使用纹理图集 (Texture Atlas)、程序化纹理等。

  • 简化数学运算: 避免复杂的数学运算,例如 pow, sqrt, sin, cos 等。尽量使用更简单的运算,例如 add, multiply, lerp 等。

  • 使用低精度数据类型: 对于颜色、法线等精度要求不高的数据,可以使用 fixedhalf 类型,而不是 float 类型,可以减少 GPU 运算量和内存带宽。

  • 避免分支语句: GPU 更适合并行计算,分支语句 (例如 if, else) 会降低并行效率。尽量使用 lerp, step, smoothstep 等函数来替代分支逻辑。

  • Shader LOD (Level of Detail): 为不同距离的模型使用不同 LOD 级别的着色器。距离远的模型可以使用更简单的着色器,减少性能开销。

  • 善用 Shader Graph 的 Sub Graph 和 Group 功能: 模块化着色器逻辑,提高代码复用率和可维护性。

着色器调试技巧:

  • Shader Graph 实时预览: Shader Graph 的实时预览功能可以快速查看着色器效果,方便调试。

  • Unity Frame Debugger (帧调试器): Unity Frame Debugger 可以逐帧查看渲染过程,包括每个 Draw Call 的着色器、材质属性、纹理等信息,有助于定位着色器问题。

  • RenderDoc (第三方 GPU 调试工具): RenderDoc 是一款强大的 GPU 调试工具,可以深入分析 GPU 的渲染过程,查看每个像素的着色器执行细节、变量值、纹理采样等信息,对于复杂着色器的调试非常有用。

  • Debug.Log (Shader 中的调试输出): 在 HLSL 代码中,可以使用 UNITY_OUTPUT_SEMANTIC(TEXCOORD)UNITY_DECLARE_TEXCOORD 宏,将数据输出到额外的纹理坐标通道,然后在片元着色器中使用 SV_Target 输出到颜色缓冲区,并通过 Frame Debugger 或 RenderDoc 查看这些输出值。例如:

// 顶点着色器 v2f vert (appdata v) { v2f o; // ... o.debugValue.x = someValue; // 将要调试的值赋值给 o.debugValue.x return o; } // 片元着色器 fixed4 frag (v2f i) : SV_Target { // ... return fixed4(i.debugValue.x, 0, 0, 1); // 将调试值输出到红色通道 }

4.3.6 总结与展望

本章节详细介绍了 Unity3D 中的着色器编程,包括 Shader Graph 和 HLSL 两种主要方式。Shader Graph 以其可视化、易用性,降低了着色器编程的门槛,适合快速原型设计和艺术家使用;HLSL 则提供了更强大的控制力和性能优化潜力,适合实现复杂效果和性能敏感的应用。两者可以结合使用,发挥各自的优势。


发布者: 作者: 转发
评论区 (0)
U