顶点程序与插值器:语义、appdata→v2f、插值规则
顶点程序接收什么、交出什么,完全由「语义」(semantics)决定——不是变量名,不是顺序,是语义。而顶点程序交出的数据并不会原样到达片元:它们会被插值。这两件事理解了,TEXCOORD0 这类命名怪象就不再迷惑。
语义:按名字接线,不按顺序
顶点输入结构(惯例叫 appdata/Attributes)的每个字段必须绑语义,GPU 按语义把网格数据接进来:
| 语义(输入) | 接什么 | 来源 |
|---|---|---|
POSITION | 模型空间顶点位置 | 网格顶点缓冲 |
NORMAL / TANGENT | 法线 / 切线(w 是镜像符号) | 网格导入设置 |
TEXCOORD0..7 | 第 1..8 套 UV | 网格 UV 通道(UV2 常给 lightmap,见 /notes/unity/lighting-4/) |
COLOR | 顶点色 | 网格 |
SV_InstanceID 等 | 系统值(实例索引等) | GPU Instancing |
TEXCOORD0 在输入结构里是「第一套 UV」,在输出结构里却只是「第 0 号通用插值器」——同一个语义名,两个含义。输出结构(惯例叫 v2f)里你可以用 TEXCOORD0 装世界坐标、装视线方向,它和 UV 毫无关系,只是个编号插槽。这是读老 shader 时最常见的困惑点。
SV_POSITION 是特殊的输出语义(SV = system value):它是裁剪空间位置,光栅化器就靠它工作(见 /notes/unity/principles-3/)。每个顶点程序必须且只能有一个。
一个完整可跑的例子:顶点色渐变
Shader "MyGame/VertexGradient"
{
SubShader
{
Tags { "RenderType" = "Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct Attributes {
float4 vertex : POSITION;
float4 color : COLOR;
float3 normal : NORMAL;
};
struct V2F {
float4 pos : SV_POSITION;
float4 color : COLOR; // 插值器:顶点色逐像素插值
float3 normal : TEXCOORD0; // 通用插值器:装世界法线
};
V2F vert(Attributes v) {
V2F o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.color;
o.normal = UnityObjectToWorldNormal(v.normal);
return o;
}
float4 frag(V2F i) : SV_Target {
float ndl = max(0, dot(normalize(i.normal), float3(0.5, 0.8, -0.3)));
return i.color * (0.2 + 0.8 * ndl);
}
ENDCG
}
}
}
插值规则:片元拿到的是「插值后的」数据
光栅化器对 v2f 的每个字段按重心坐标逐像素插值(见 /notes/unity/principles-3/),且默认是透视校正插值——屏幕上均匀不是目的,「模型表面上均匀」才是,否则贴图在斜面上会拉伸。两条实用推论:
- 插值是免费的,但插值器数量有上限(各平台不同,移动端历史下限是 8 个 4 分量)。装不下降级或编译失败。能重算的不如重算:比如世界空间视线方向可以在片元里用
_WorldSpaceCameraPos - 插值来的世界位置得到,只传一个位置插值器。 - 插值后再 normalize:法线插完长度会缩,片元里必须重新归一化(上面例子里
normalize(i.normal)),否则光照偏暗。
特殊修饰:nointerpolation 让整三角形取同一值(不插值,适合分类 ID);CENTROID 牺牲一点精度换抗锯齿稳定(程序化纹理边缘抖动时用过)。
数据怎么「从模型空间到片元」的完整路线
以法线为例串一遍(空间链见 /notes/unity/principles-2/):
- 网格导入:模型空间法线进
NORMAL语义。 - 顶点程序:
UnityObjectToWorldNormal(内部是逆转置,见 /notes/unity/principles-5/)→ 世界空间法线 → 塞进TEXCOORD0插值器。 - 光栅化:逐像素透视校正插值。
- 片元程序:
normalize后参与dot(N, L)(见 /notes/unity/lighting-1/)。
每一步的空间都要写注释标明(// WS),跨空间混用是光照 bug 的头号来源。
自检三问
- 输出结构里
float3 worldPos : TEXCOORD1和 UV 有关系吗? - 为什么片元里对插值来的法线必须再 normalize?
- 一个 v2f 塞了 9 个 4 分量插值器,可能出什么问题?