顶点程序与插值器:语义、appdata→v2f、插值规则

Unity 知识 · Shader 区域 · 语言与结构 · 第 2 篇 更新于 2026-10-15

顶点程序接收什么、交出什么,完全由「语义」(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/):

  1. 网格导入:模型空间法线进 NORMAL 语义。
  2. 顶点程序:UnityObjectToWorldNormal(内部是逆转置,见 /notes/unity/principles-5/)→ 世界空间法线 → 塞进 TEXCOORD0 插值器。
  3. 光栅化:逐像素透视校正插值。
  4. 片元程序:normalize 后参与 dot(N, L)(见 /notes/unity/lighting-1/)。

每一步的空间都要写注释标明(// WS),跨空间混用是光照 bug 的头号来源。

自检三问

  • 输出结构里 float3 worldPos : TEXCOORD1 和 UV 有关系吗?
  • 为什么片元里对插值来的法线必须再 normalize?
  • 一个 v2f 塞了 9 个 4 分量插值器,可能出什么问题?