Unity Shader 渲染管线深度解析 — Shader 三阶段

Unity Shader 渲染管线深度解析 — Shader 三阶段 深入理解顶点着色器、几何着色器与片元着色器的结构、职责与执行顺序掌握 GPU 渲染管线的核心机制。渲染管线总览GPU 渲染管线Rendering Pipeline是将 3D 场景中的几何数据逐步转化为屏幕像素颜色的完整流程。 在 Unity 的 ShaderLab / HLSL 体系中开发者可以编写顶点着色器、几何着色器可选以及片元着色器像素着色器三个可编程阶段 精确控制物体的形变、几何生成与最终像素颜色。可编程阶段Vertex / Geometry / Fragment是 Shader 开发者的主战场而光栅化与输出合并属于固定功能管线Fixed-Function 开发者通过参数配置如ZTest / Blend语句控制其行为而非直接编程。Vertex Shader · 顶点着色器顶点着色器是渲染管线中第一个可编程阶段。 GPU 对模型的每一个顶点并行执行一次顶点着色器其核心职责是执行坐标空间变换—— 将顶点从模型空间Model Space转换到裁剪空间Clip Space即所谓的MVP 变换。输入与输出结构输入 · appdata来自网格的每顶点数据positionPOSITIONnormalNORMALtexcoordTEXCOORD0colorCOLOR等语义输出 · v2f传递给后续阶段posSV_POSITION必须uvTEXCOORD0worldNormalTEXCOORD1worldPosTEXCOORD2等Unity HLSL 代码示例// 顶点着色器输入结构来自网格 struct appdata { float4 vertex : POSITION; // 顶点位置模型空间 float3 normal : NORMAL; // 法线模型空间 float2 uv : TEXCOORD0; // 纹理坐标 }; // 顶点→片元传递结构 struct v2f { float4 pos : SV_POSITION; // 必须裁剪空间位置 float2 uv : TEXCOORD0; float3 worldNormal: TEXCOORD1; float3 worldPos : TEXCOORD2; }; // 顶点着色器主函数 v2f vert(appdata v) { v2f o; // MVP 变换模型空间 → 裁剪空间 o.pos UnityObjectToClipPos(v.vertex); // 传递 UV可加偏移/缩放 o.uv TRANSFORM_TEX(v.uv, _MainTex); // 法线转换到世界空间用于光照计算 o.worldNormal UnityObjectToWorldNormal(v.normal); // 顶点世界坐标 o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; }SV_POSITION是顶点着色器唯一必须输出的系统语义代表裁剪空间坐标Clip Space。 硬件随后执行透视除法÷w将其转换为 NDC 坐标再映射到屏幕像素坐标。 其他输出字段由开发者自由定义全部会在光栅化阶段被重心插值后传给片元着色器。Geometry Shader · 几何着色器几何着色器是渲染管线中唯一能够动态生成或销毁图元的阶段 且该阶段完全可选——在 Unity ShaderLab 中只需增加#pragma geometry geom指令即可启用。 它位于顶点着色器之后、光栅化之前。几何着色器以完整图元一个三角形 3 个顶点一条线 2 个顶点一个点 1 个顶点 为输入单位可以向输出流TriangleStream / LineStream / PointStream写入任意数量的新顶点 从而生成 0 到数个新图元。Unity HLSL 代码示例// 几何着色器将每个三角形沿法线方向挤出生成法线可视化线段 // 指定输入输出图元类型和最大顶点数 [maxvertexcount(6)] // 最多输出 6 个顶点2 条线每条 2 顶点 × 3 6 void geom( triangle v2f input[3], // 输入一个三角形3 个顶点 inout LineStreamv2f stream // 输出线段流 ) { // 遍历三角形每个顶点 for (int i 0; i 3; i) { v2f v0 input[i]; // 顶点原始位置起点 stream.Append(v0); // 沿法线偏移一段距离终点 v2f v1 v0; v1.pos v0.pos float4(v0.worldNormal * 0.1, 0); v1.pos mul(UNITY_MATRIX_VP, v1.pos); stream.Append(v1); // RestartStrip() 结束当前图元开始下一条线 stream.RestartStrip(); } }⚠️性能注意事项几何着色器在 GPU 上并非总是高效——动态输出顶点数量会阻碍硬件流水线并行性 导致占用率下降。现代渲染方案如 GPU Instancing、Compute Shader往往能更高效地实现类似效果。 在移动平台Metal/Vulkan中应谨慎使用或替代方案。Fragment Shader · 片元着色器 / 像素着色器片元着色器HLSL 中也称 Pixel Shader是渲染管线最后一个可编程阶段 也是视觉效果表现力最丰富的阶段。 光栅化阶段将每个三角形覆盖的屏幕像素转换为片元Fragment 并对顶点着色器输出的属性进行重心插值后传入片元着色器。 片元着色器对每个片元独立计算最终输出颜色以及可选的深度值。输入与输出输入 · v2f插值后来自顶点着色器经过光栅化重心插值屏幕坐标、UV、世界法线、世界坐标…以及内置SV_IsFrontFace正/背面输出 · SV_Target最终像素颜色 float4(r,g,b,a)可输出到多个 Render TargetMRT可选输出深度 SV_Depth 覆盖默认值Unity HLSL 代码示例// 片元着色器Blinn-Phong 光照 纹理采样 sampler2D _MainTex; float4 _Color; float _Glossiness; fixed4 frag(v2f i) : SV_Target { // 1. 纹理采样 fixed4 texColor tex2D(_MainTex, i.uv) * _Color; // 2. 准备光照向量归一化 float3 N normalize(i.worldNormal); float3 L normalize(_WorldSpaceLightPos0.xyz); float3 V normalize(_WorldSpaceCameraPos - i.worldPos); float3 H normalize(L V); // 半程向量Blinn-Phong // 3. 计算各光照分量 fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.rgb * texColor.rgb; fixed3 diffuse _LightColor0.rgb * texColor.rgb * max(0, dot(N, L)); fixed3 specular _LightColor0.rgb * pow(max(0, dot(N, H)), _Glossiness * 128); // 4. 合并所有光照分量返回最终颜色 fixed3 finalColor ambient diffuse specular; return fixed4(finalColor, texColor.a); }片元 ≠ 像素片元是一个候选像素它携带位置、深度、插值属性等信息 但最终不一定会写入帧缓冲。在片元着色器输出后还需经过深度测试ZTest、模板测试Stencil、Alpha 混合Blend等操作 才可能成为真正的屏幕像素。三大着色器对比总结属性顶点着色器几何着色器片元着色器执行单位每顶点 ×1每图元 ×1每片元 ×N可编程✅ 必须⚪ 可选✅ 必须输入类型单个顶点完整图元3/2/1顶点插值后的片元能否改变顶点数❌ 不能✅ 可以生成/丢弃❌ 不能主要输出SV_POSITION 自定义属性向 Stream 写入新顶点SV_Target 颜色可读纹理⚠️ 有限支持⚠️ 有限支持✅ 完全支持并行度高中受图元数限制极高Unity 指令#pragma vertex vert#pragma geometry geom#pragma fragment frag典型用途MVP 变换、顶点位移粒子 Billboard、法线可视化光照、纹理、PBR完整 Unity Shader 结构Shader Custom/PipelineDemo { Properties { _MainTex (Albedo, 2D) white {} _Color (Color Tint, Color) (1,1,1,1) _Glossiness(Glossiness, Range(0,1)) 0.5 } SubShader { Tags { RenderTypeOpaque } Pass { HLSLPROGRAM // ① 声明三个阶段的函数 #pragma vertex vert // 必须 #pragma geometry geom // 可选 #pragma fragment frag // 必须 #include UnityCG.cginc #include Lighting.cginc // ─── 结构体 ─────────────────────────────── struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Glossiness; // ─── ② 顶点着色器 ───────────────────────── v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } // ─── ③ 几何着色器可选法线可视化────── [maxvertexcount(6)] void geom(triangle v2f input[3], inout LineStreamv2f stream) { for(int i0;i3;i){ stream.Append(input[i]); v2f einput[i]; e.posfloat4(input[i].worldNormal*.15,0); stream.Append(e); stream.RestartStrip(); } } // ─── ④ 片元着色器 ───────────────────────── fixed4 frag(v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * _Color; float3 N normalize(i.worldNormal); float3 L normalize(_WorldSpaceLightPos0.xyz); float3 V normalize(_WorldSpaceCameraPos-i.worldPos); float3 H normalize(LV); fixed3 diff _LightColor0.rgb * col.rgb * max(0,dot(N,L)); fixed3 spec _LightColor0.rgb * pow(max(0,dot(N,H)), _Glossiness*128); return fixed4(UNITY_LIGHTMODEL_AMBIENT.rgb*col.rgbdiffspec, col.a); } ENDHLSL } } }