【Shader基础】CG/HLSL 基础
一、语言定位与历史渊源
CG (C for Graphics) 是 NVIDIA 于 2002 年提出的面向 GPU 的中级着色语言,其语法高度派生自 C 语言,加入了面向图形处理的向量/矩阵原生类型和语义绑定机制。HLSL (High-Level Shading Language) 是微软为 DirectX 平台设计的着色语言,两者语法高度同源(NVIDIA 曾与微软合作统一规范),在 Unity 中可以互换使用。
GLSL (OpenGL Shading Language) 是 Khronos 为 OpenGL 设计的着色语言,语法同样类 C 但语义体系不同,Unity 中较少直接使用。
在 Unity Built-in 管线中,CGPROGRAM 块内使用 CG 语法(兼容 HLSL);在 URP/HDRP 中,HLSLPROGRAM 块使用纯 HLSL 语法。两者的核心语法(类型、函数、语义)基本一致,主要差异在于包含文件和纹理采样方式。
二、标量类型与精度限定符 (Precision Qualifiers)
基础标量
HLSL/CG 的标量类型与 C 语言一致,但增加了精度限定符------这是 GPU 编程区别于 CPU 编程的核心特征之一。
cpp
float x = 1.0; // 32 位 IEEE 754 单精度浮点
half y = 0.5; // 16 位半精度浮点
fixed z = 0.25; // 11 位定点数 (deprecated)
int n = 10;
uint m = 5u;
bool flag = true;
精度限定符的意义
精度限定符的引入是为了解决 GPU 运算吞吐量与数值精度的权衡问题:
| 限定符 | 位宽 | 有效范围 | 显著位 (Significand) | 典型适用域 |
|---|---|---|---|---|
| float | 32bit | 1.18×10−38,3.40×10381.18×10\^{-38},3.40×10\^{38}1.18×10−38,3.40×1038 | 23 bit 尾数 | 坐标变换、深度值、UV、世界空间位置 |
| half | 16bit | 6.10×10−5,6.55×1046.10×10\^{-5},6.55×10\^{4}6.10×10−5,6.55×104 | 10 bit 尾数 | 颜色值、法线、切线、光照方向 |
| fixed | 11bit | −2.0,2.0-2.0,2.0−2.0,2.0 | 定点 (deprecated) | 简单颜色混合 |
关键工程原则:
- 坐标相关数据(位置、UV、MVP 变换)必须用 float,精度不足会导致几何畸变(vertex snapping、纹理抖动)
- 颜色和法线用 half 即可,人眼和插值计算对此精度不敏感
- 移动端 GPU 的 half 运算吞吐通常是 float 的 2-4 倍(FP16 ALU 管线),合理使用可显著提升性能
- URP/HDRP 中 fixed 已被废弃,编译器将其映射为 half
- real 类型是 Unity SRP 引入的自适应精度别名:#if SHADER_TARGET >= 2.5 时为 half,否则为 float
三、向量类型与 Swizzle 操作
声明与初始化
cpp
float3 position = float3(1.0, 2.0, 3.0); // 显式构造
float4 color = float4(1, 0, 0, 1); // 标量隐式转换为 float
half2 uv = half2(0.5, 0.5);
Swizzle(分量重排 )--- HLSL 最强大的特性之一
Swizzle 允许通过分量选择器 对向量的分量进行任意读取或重组,零开销(编译器生成的是寄存器级的 swizzle 指令,无额外 ALU 操作)。
HLSL 为向量提供了 3 套等价的分量标识(语义不同,不能混用),专门用于 Swizzle:
- x, y, z, w:用于坐标 / 位置向量(最通用)
- r, g, b, a:用于颜色向量(红、绿、蓝、透明度)
- i, j, k:极少用,等价于 x,y,z
关键特性
- 分量顺序可以任意颠倒
- 分量可以重复使用
- 提取的分量数量可以少于原向量(自动生成更小的向量)
- 三套标识绝对不能混用(如 xrg、zb 都是语法错误)
cpp
// 定义一个 4 维向量:x=1, y=2, z=3, w=4
float4 vec = float4(1.0, 2.0, 3.0, 4.0);
// 1. 读取分量(重组新向量)
float2 a = vec.xy; // 结果:float2(1,2) → 提取前两个分量
float3 b = vec.zyx; // 结果:float3(3,2,1) → 颠倒顺序
float4 c = vec.xxxx; // 结果:float4(1,1,1,1) → 重复分量
float2 d = vec.wz; // 结果:float2(4,3) → 跨分量提取
// 2. 颜色语义 Swizzle(等价于 xyzw)
float4 color = float4(1,0,0,1); // 红色
float3 rgb = color.rgb; // 结果:float3(1,0,0) → 去掉透明度
// 3. 直接赋值(修改原向量分量)
vec.xy = float2(5, 6); // vec 变为:float4(5,6,3,4)
vec.zzz = float3(9,9,9); // vec 变为:float4(5,6,9,4)
记法约定 :xyzw 用于几何/坐标 语义,rgba 用于颜色 语义。两者完全等价,可混合使用(如 v.xg),但不推荐这样做。
四、矩阵类型
声明
HLSL 的矩阵类型遵循 typeRxC 的命名范式:
cpp
float4x4 mvp; // 4x4 浮点矩阵 (16 个 float)
float3x3 normalMatrix; // 3x3 法线变换矩阵
float4x3 view; // 4 行 3 列
half4x4 tangentFrame; // 半精度 4x4
行主序 vs 列主序 (Row-Major vs Column-Major)
这是 CG/HLSL 中最容易混淆的问题:
- HLSL 语言层面 :书写是按行写,但内存打包仍是列主序
- Unity 的 C#→Shader 传递 :Matrix4x4 是列主序(column-major),但 Unity 通过 UNITY_MATRIX_MVP 等宏已处理好转换,UnityObjectToClipPos() 内部正确处理了矩阵乘法顺序
cpp
// Unity 封装的变换函数(内部处理了矩阵布局差异)
float4 clipPos = UnityObjectToClipPos(v.vertex);
// 等价于(不需要手动写)
float4 worldPos = mul(UNITY_MATRIX_M, float4(v.vertex, 1.0));
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
float4 clipPos = mul(UNITY_MATRIX_P, viewPos);
行主序:使用行向量
- 向量在左
- 矩阵顺序:M → V → P
- 数学书写 = 内存存储
列主序:使用列向量
- 向量在右
- 矩阵顺序:P ← V ← M
- 数学书写与内存转置关系
矩阵乘法 --- mul() 的方向性
cpp
// mul(M, v) --- 将 v 视为列向量: 结果 = M * v
float4 result = mul(unity_MatrixVP, worldPos);
// mul(v, M) --- 将 v 视为行向量: 结果 = v^T * M
float4 result = mul(worldPos, unity_MatrixVP);
// 矩阵 × 矩阵
float4x4 mvp = mul(UNITY_MATRIX_VP, UNITY_MATRIX_M);
五、语义 (Semantics) --- 着色器与管线的契约
语义是 HLSL 中连接着色器代码 与GPU 硬件数据通路的绑定机制。它告诉编译器:"这个变量对应的是顶点位置 / UV / 系统输出的哪个通道"。
语义的分类体系
顶点输入语义
从 CPU 端的 Mesh 顶点缓冲区读取数据,每个语义绑定到一个顶点属性:
| 语义 | 数据类型 | 语义描述 |
|---|---|---|
| POSITION | float3 | 顶点在模型空间 (Object Space) 中的坐标 |
| NORMAL | float3 | 顶点法线(模型空间,非归一化) |
| TANGENT | float4 | 切线向量(xyz = tangent, w = binormal 手性符号) |
| TEXCOORD0 ~ TEXCOORD7 | float2/4 | 纹理坐标(UV)或自定义逐顶点数据 |
| COLOR | float4 | 逐顶点颜色 (Vertex Color) |
| SV_InstanceID | uint | GPU Instancing 的实例索引(系统值语义) |
插值传递语义(顶点着色器 → 片元着色器)
经过光栅化阶段的透视校正插值(Perspective-Correct Interpolation)后传递给片元着色器:
| 语义 | 数据类型 | 语义描述 |
|---|---|---|
| SV_POSITION | float4 | 裁剪空间 (Clip Space) 位置,必须存在且唯一 |
| TEXCOORD0 ~ TEXCOORD7 | 任意向量 | 自定义插值数据(法线、世界坐标、UV 等) |
| COLOR0 ~ COLOR1 | float4 | 插值颜色 |
SV_POSITION 在顶点着色器输出时是裁剪空间坐标 ;在片元着色器中可读取其 .xy 获取屏幕空间像素坐标。
片元输出语义
| 语义 | 语义描述 |
|---|---|
| SV_Targetn (n=0~7) | 写入第 n 个渲染目标 (Render Target),SV_Target0 等价于旧语法 COLOR |
| SV_Depth | 输出自定义深度值覆盖深度缓冲 |
| SV_DepthLessEqual | 自定义深度 + 深度测试约束(小于等于当前值才写入) |
系统值语义 (System-Value Semantics)
所有以 SV_ 前缀 开头的语义属于系统值语义,由 GPU 固定管线解释。开发者不得将其用于自定义数据绑定。
cpp
// 错误:SV_POSITION 用于 TEXCOORD 通道
struct v2f {
float2 uv : SV_POSITION; // 编译错误!
};
// 正确
struct v2f {
float4 pos : SV_POSITION; // 系统值语义
float2 uv : TEXCOORD0; // 自定义数据
};
插值修饰符 (Interpolation Modifiers)
修饰符放在语义之后,控制光栅化阶段的插值方式:
cpp
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0; // 默认: 透视校正插值
float2 screenUV : TEXCOORD1 noperspective; // 屏幕空间线性插值
float3 normal : TEXCOORD2 centroid; // 质心采样 (MSAA 抗锯齿)
};
- linear(默认):透视校正插值,保证 3D 空间中的线性关系在屏幕上也成立
- noperspective:纯屏幕空间线性插值,用于屏幕后处理坐标
- centroid:质心采样,确保 MSAA 多重采样时插值点落在图元内部,避免片元边界伪影