第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(精度优先) - 纹理坐标、UV →
half(够用就行) - 颜色、简单的光照系数 →
fixed(省性能)
但要注意:在PC上,half和fixed通常会被自动提升为float。所以性能优化主要针对移动端。
1.2.3 向量和矩阵类型
除了基础类型,HLSL还提供了向量和矩阵:
float2 pos; // 二维向量
float3 normal; // 三维向量
float4 color; // 四维向量(RGBA)
float4x4 world; // 4x4矩阵
这些其实都是float/half/fixed的组合。比如half3就是三个half组成的向量。
小技巧:访问向量成员时,可以用.xyzw或.rgba。比如color.rgb和color.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 本章小结
今天的内容不多,但很重要。记住三点:
- 语义是通信协议——告诉GPU数据从哪来、到哪去
- 数据类型是精度选择——别一味用
float,也别盲目用fixed - 两者要匹配——位置用
float,颜色用fixed,UV用half
下一章,我们会正式进入着色器的编写。到时候,这些基础概念会反复用到。
各位,先把今天的内容消化掉。有问题随时问我。