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
        }
    }
}
相关推荐
nnsix40 分钟前
Unity API 兼容的 .NET Standard 2.1 和 .NET Framework 区别
unity·游戏引擎·.net
mxwin1 小时前
Unity Shader 制作半透明物体 使用多Pass提前写入深度的方式 避免穿模
unity·游戏引擎
IT里的交易员1 小时前
【系统】Windows 安装 uv
windows·uv
nnsix2 小时前
Unity HybridCLR 笔记
笔记·unity·游戏引擎
nnsix4 小时前
Unity Addressables 笔记
unity·游戏引擎
RReality4 小时前
【Unity Shader URP】视差贴图 实战教程
ui·平面·unity·游戏引擎·图形渲染·贴图
CG_MAGIC7 小时前
3ds Max FloorGenerator 插件:快速生成地板木纹
3d·贴图·uv·建模教程·渲云渲染
深耕AI13 小时前
【VS Code避坑指南】点击Python图标提示“没有Python环境”,选择安装uv后这堆输出到底是什么意思?
开发语言·python·uv
小清兔19 小时前
Addressable的设置打包流程
笔记·游戏·unity·c#
3D霸霸21 小时前
Sourcetree 拉取新工程
数据仓库·unity