一、顶点着色器的介绍
顶点着色器 (Vertex Shader) 是 Unity Shader 流水线中的第一个可编程阶段,主要负责处理顶点数据,如位置、法线、UV 坐标等。它为每个顶点执行一次,可用于实现顶点变换、动画、变形等效果。
Shader "Custom/VertexShaderExample" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 顶点输入结构
struct appdata {
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 法线方向
float4 tangent : TANGENT; // 切线方向
float2 uv : TEXCOORD0; // 第一组UV坐标
};
// 顶点输出结构(片元输入)
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置
float3 worldPos : TEXCOORD0; // 世界空间位置
float3 normal : TEXCOORD1; // 世界空间法线
float2 uv : TEXCOORD2; // UV坐标
};
// 顶点着色器
v2f vert (appdata v) {
v2f o;
// 将顶点从模型空间转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 计算世界空间位置
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 计算世界空间法线
o.normal = UnityObjectToWorldNormal(v.normal);
// 传递UV坐标
o.uv = v.uv;
return o;
}
// 片元着色器
fixed4 frag (v2f i) : SV_Target {
// 简单的漫反射光照计算
float3 lightDir = _WorldSpaceLightPos0.xyz;
float ndotl = saturate(dot(i.normal, lightDir));
fixed4 col = tex2D(_MainTex, i.uv) * _Color;
col.rgb *= ndotl; // 应用光照
return col;
}
ENDCG
}
}
}
1. 顶点着色器输入数据
顶点着色器的输入通常通过appdata
结构体定义,常用的语义有:
POSITION
:顶点位置(模型空间)NORMAL
:法线方向(模型空间)TANGENT
:切线方向(模型空间)TEXCOORD0-7
:纹理坐标COLOR
:顶点颜色
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
2. 顶点着色器输出数据
顶点着色器的输出通常是一个传递到片元着色器的结构体,常用语义有:
SV_POSITION
:裁剪空间位置(必需)TEXCOORD0-7
:自定义数据(如 UV、法线、世界位置)COLOR
:顶点颜色
struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
fixed4 color : COLOR;
};
二、图形编程的语义
1. 输入语义
POSITION
:模型空间顶点位置(float3/float4)
SV_POSITION
:裁剪空间顶点位置(顶点着色器输出必选)
2. 法线与切线相关语义
NORMAL
:模型空间法线方向(float3)
TANGENT
:模型空间切线方向(float4,w 分量用于确定副切线方向)
3. 纹理坐标相关语义
TEXCOORD0
、TEXCOORD1
...TEXCOORD7
:纹理坐标(float2/float3/float4)
4. 颜色相关语义
COLOR
:顶点颜色(fixed4/float4)
5. 语义定义汇总
三、顶点着色器应用
顶点着色器是图形渲染管线中的关键阶段,主要负责处理顶点数据。其核心功能可以分为以下几类:
1. 坐标空间变换
这是顶点着色器最基本的功能,将顶点从模型空间转换到裁剪空间。
v2f vert (appdata v) {
v2f o;
// 模型空间 → 裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 计算世界空间位置用于光照
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 传递UV坐标
o.uv = v.uv;
return o;
}
2. 顶点动画
通过修改顶点位置实现各种动画效果。
案例:波浪动画
v2f vert (appdata v) {
v2f o;
// 计算波浪偏移
float wave = _Amplitude * sin(v.vertex.x * _Frequency + _Time.y * _Speed) *
sin(v.vertex.z * _Frequency * 0.5 + _Time.y * _Speed * 0.7);
// 应用偏移
v.vertex.y += wave;
// 转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
3. 法线修改
修改法线方向以影响光照计算,常用于创建特殊视觉效果。
案例:膨胀效果(修改法线方向)
v2f vert (appdata v) {
v2f o;
// 沿法线方向膨胀
v.vertex.xyz += v.normal * _Amount * sin(_Time.y);
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = UnityObjectToWorldNormal(v.normal);
o.uv = v.uv;
return o;
}
4. 光照预处理
在顶点着色器中进行部分光照计算,减轻片元着色器负担。
案例 :顶点光照(简化版)
v2f vert (appdata v) {
v2f o;
// 计算世界空间位置和法线
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算光照方向(简化,假设为方向光)
float3 lightDir = _WorldSpaceLightPos0.xyz;
// 计算漫反射光照
float ndotl = saturate(dot(worldNormal, lightDir));
fixed4 diffuse = _LightColor0 * ndotl * _Color;
// 传递光照结果到片元着色器
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.color = diffuse;
return o;
}
5. 几何变形
基于各种算法修改顶点位置,创建复杂几何效果.
案例:基于噪声的变形
// 需要包含噪声函数库
#include "Noise.cginc"
v2f vert (appdata v) {
v2f o;
// 计算噪声值
float noise = snoise(float3(v.vertex.x * _Frequency, v.vertex.y * _Frequency, _Time.y * _Speed));
// 应用变形
v.vertex.xyz += v.normal * noise * _Amplitude;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
核心功能分类表:
四、顶点的动态变化
顶点动态变化日常会使用到的功能如下:
五、顶点着色器性能优化
顶点着色器是图形渲染管线中的关键环节,其性能优化对于提升游戏或应用的帧率至关重要。由于顶点着色器为每个顶点执行一次,当处理大量顶点时,低效的代码会成为性能瓶颈。以下从多个角度详细介绍顶点着色器的性能优化方法。
1. 减少计算复杂度
避免昂贵的数学函数
- 昂贵操作:三角函数 (sin/cos/tan)、幂运算 (pow)、开方 (sqrt)、倒数 (rcp) 等
- 优化方法:
- 预先计算常量值(如在 CPU 端计算或使用 #define)
- 使用近似函数(如快速近似 sin/cos)
- 减少重复计算(使用临时变量存储中间结果)
// 优化前
float angle = v.vertex.x * _Frequency + _Time.y * _Speed;
float wave = _Amplitude * sin(angle) * sin(v.vertex.z * _Frequency * 0.5 + _Time.y * _Speed * 0.7);
// 优化后(减少三角函数调用)
float angleX = v.vertex.x * _Frequency;
float angleZ = v.vertex.z * _Frequency * 0.5;
float timeFactor = _Time.y * _Speed;
float waveX = _Amplitude * sin(angleX + timeFactor);
float waveZ = sin(angleZ + timeFactor * 0.7);
float wave = waveX * waveZ;
2. 压缩传递数据
- 使用更小的数据类型:
fixed
(11 位浮点数,适合颜色)代替float
half
(16 位浮点数)代替float
(精度要求不高时)
- 合并数据到更少的寄存器:
- 使用
float4
存储多个值(如位置和颜色) - 利用向量的多个分量(如用 w 分量存储额外数据)
- 使用
// 优化前
float3 worldPos : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
// 优化后(合并到TEXCOORD0和TEXCOORD1)
float4 posAndUV : TEXCOORD0; // .xyz = worldPos, .w = uv.x
float4 normalAndUV : TEXCOORD1; // .xyz = normal, .w = uv.y
3. 空间变换优化
避免不必要的变换
- 只计算必要的空间坐标(如不需要世界空间位置则不计算)
- 复用已有的变换矩阵(如利用
UNITY_MATRIX_MVP
)
// 优化前(分步变换)
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
float4 clipPos = mul(UNITY_MATRIX_P, viewPos);
// 优化后(直接使用MVP矩阵)
float4 clipPos = mul(UNITY_MATRIX_MVP, v.vertex);