深入理解顶点着色器、几何着色器与片元着色器的结构、职责与执行顺序,掌握 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
}
}
}
