第1章:语义与数据类型——着色器的“语法”和“词汇”

各位同学,欢迎来到《Unity着色器开发:从基础到进阶》的第一章。

今天咱们聊点最基础,但也最容易踩坑的东西——语义数据类型

你可能会想:“不就是几个关键字吗?有啥好讲的?”

嗯,我刚开始学的时候也这么想。结果呢?写出来的shader要么报错,要么效果完全不对。后来我才明白,这些看似简单的“语法”和“词汇”,其实是整个着色器世界的基石。

1.1 常用语义:着色器的“通信协议”

语义(Semantic),说白了就是告诉GPU:“嘿,这个变量是干啥用的”。

它就像快递单上的地址——没有它,数据就不知道该往哪儿送。

1.1.1 POSITION vs SV_POSITION

这两个长得像,但用途完全不同。

  • POSITION:从顶点缓冲区读取的原始顶点位置。它是在模型空间里的坐标。
  • SV_POSITION:经过顶点着色器变换后的位置。它是在裁剪空间里的坐标。

我个人习惯这样记:POSITION是“原材料”,SV_POSITION是“成品”。

重要提醒:在顶点着色器的输出中,SV_POSITION必须的。没有它,GPU不知道把像素画在哪儿。

来看个例子:

struct appdata
{
    float4 vertex : POSITION;    // 输入:模型空间顶点位置
};

struct v2f
{
    float4 vertex : SV_POSITION; // 输出:裁剪空间顶点位置
};

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex); // 变换!
    return o;
}

我曾经见过有人把SV_POSITION写成POSITION,结果shader编译通过了,但渲染出来的模型完全扭曲。嗯,这种bug最难找。

1.1.2 COLOR vs SV_Target

这两个也容易搞混。

  • COLOR:通常用于传递颜色数据,比如顶点颜色。它是个输入/输出语义。
  • SV_Target:片元着色器的输出语义,表示最终要写入渲染目标的颜色。

说白了:COLOR是“中间人”,SV_Target是“最终结果”。

小技巧:在Unity的Surface Shader中,你很少直接看到SV_Target,因为Unity帮你封装了。但在顶点/片元着色器中,它必须显式写出。

1.1.3 其他常用语义

语义 用途 阶段
NORMAL 法线方向 顶点输入
TEXCOORD0~7 纹理坐标(最多8组) 顶点/片元
TANGENT 切线方向 顶点输入
SV_VertexID 顶点索引编号 顶点输入

你想想看,为什么纹理坐标有8组?因为有些复杂的模型需要多层贴图叠加。我在做地形渲染时就用到过4组——分别控制颜色、法线、高度和细节。

1.2 数据类型:float、half、fixed

这部分是很多新手忽略的。他们一律用float,觉得省事。

但我要告诉你:在移动端,用对数据类型,性能能差好几倍。

1.2.1 三兄弟的区别

类型 精度 取值范围 适用场景
float 32位(高精度) ±3.4×10³⁸ 位置、矩阵运算
half 16位(中精度) ±6.55×10⁴ 纹理坐标、颜色
fixed 11位(低精度) ±2.0 颜色、光照系数

为什么会有这种区别?

因为GPU的ALU(算术逻辑单元)对不同精度的数据,处理速度不一样。精度越低,计算越快,功耗也越低。

避坑指南:我曾经在项目里用half存储世界坐标,结果模型在远处出现了明显的抖动。因为half的精度不够,导致位置误差被放大。从那以后,所有位置相关的数据我都用float

1.2.2 什么时候用哪个?

我总结了一个简单的原则:

  • 位置、方向、矩阵float(精度优先)
  • 纹理坐标、UVhalf(够用就行)
  • 颜色、简单的光照系数fixed(省性能)

但要注意:在PC上,halffixed通常会被自动提升为float。所以性能优化主要针对移动端。

1.2.3 向量和矩阵类型

除了基础类型,HLSL还提供了向量和矩阵:

float2 pos;      // 二维向量
float3 normal;   // 三维向量
float4 color;    // 四维向量(RGBA)
float4x4 world;  // 4x4矩阵

这些其实都是float/half/fixed的组合。比如half3就是三个half组成的向量。

小技巧:访问向量成员时,可以用.xyzw.rgba。比如color.rgbcolor.xyz是等价的。我个人习惯用.rgb处理颜色,用.xyz处理位置——这样代码更易读。

1.3 语义与数据类型的配合

语义和数据类型是配合使用的。你不能给POSITION语义配一个fixed类型——精度不够。

来看一个完整的例子:

struct v2f
{
    float4 pos : SV_POSITION;   // 位置:float
    half2 uv : TEXCOORD0;       // UV:half
    fixed3 color : COLOR;       // 颜色:fixed
};

这个结构体里,每个成员都用了最合适的类型。既保证了精度,又兼顾了性能。

嗯,这就是专业和业余的区别。

1.4 本章小结

今天的内容不多,但很重要。记住三点:

  1. 语义是通信协议——告诉GPU数据从哪来、到哪去
  2. 数据类型是精度选择——别一味用float,也别盲目用fixed
  3. 两者要匹配——位置用float,颜色用fixed,UV用half

下一章,我们会正式进入着色器的编写。到时候,这些基础概念会反复用到。

各位,先把今天的内容消化掉。有问题随时问我。