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

来自网格的每顶点数据:

position(POSITION)

normal(NORMAL)

texcoord(TEXCOORD0)

color(COLOR)等语义

输出 · v2f

传递给后续阶段:

pos(SV_POSITION,必须)

uv(TEXCOORD0)

worldNormal(TEXCOORD1)

worldPos(TEXCOORD2)等

Unity HLSL 代码示例

cs 复制代码
// 顶点着色器输入结构(来自网格)
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 代码示例

cs 复制代码
// 几何着色器:将每个三角形沿法线方向挤出,生成法线可视化线段

// 指定输入输出图元类型和最大顶点数
[maxvertexcount(6)]   // 最多输出 6 个顶点(2 条线,每条 2 顶点 × 3 = 6)
void geom(
    triangle v2f input[3],         // 输入:一个三角形(3 个顶点)
    inout LineStream<v2f> 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 Target(MRT)

可选输出深度 SV_Depth 覆盖默认值

Unity HLSL 代码示例

cs 复制代码
// 片元着色器: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 结构

cs 复制代码
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 { "RenderType"="Opaque" }

        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 LineStream<v2f> stream) {
                for(int i=0;i<3;i++){
                    stream.Append(input[i]);
                    v2f e=input[i];
                    e.pos+=float4(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(L+V);
                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.rgb+diff+spec, col.a);
            }
            ENDHLSL
        }
    }
}
相关推荐
mxwin2 小时前
Unity Shader 数学与几何变换 深入理解渲染管线中的坐标系转换:从模型空间到屏幕空间的完整变换链
unity·游戏引擎·shader
心前阳光4 小时前
Unity使用Luban之Luban配置
unity
mxwin4 小时前
Unity ShaderLab 完全指南深入了解 Unity 特有的声明式语法,用于定义材质面板、渲染回退、细节层次等核心功能
unity·游戏引擎·材质·shader
qq1315306246 小时前
Unity 渲染优化核心总结(Draw Call / SetPass / Batch 全体系)
unity·游戏引擎·batch
美团骑手阿豪7 小时前
C#语法:HashSet与List对比,适合场景
unity·c#
crossoverJie9 小时前
OpenAI 收购 Python 工具链 uv 和 Ruff
开发语言·人工智能·python·uv
筱顾大牛9 小时前
黑马点评---用户签到、UV统计
android·服务器·uv
平行云PVT19 小时前
数字孪生信创云渲染技术解析:从混合信创到全国产化架构
linux·unity·云原生·ue5·图形渲染·webgl·gpu算力
小小数媒成员1 天前
Unity的包含文件
unity·游戏引擎