【Shader基础】CG/HLSL 基础语法

【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:

  1. x, y, z, w:用于坐标 / 位置向量(最通用)
  2. r, g, b, a:用于颜色向量(红、绿、蓝、透明度)
  3. 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 多重采样时插值点落在图元内部,避免片元边界伪影

待更

相关推荐
垂葛酒肝汤4 小时前
Unity的UGUI的坐标
unity
winlife_4 小时前
让 AI 写敌人状态机,并用脚本化场景验证状态转换正确:funplay-unity-mcp 实战
人工智能·unity·游戏引擎·ai编程·状态机·mcp
tealcwu4 小时前
【Unity实战】Unity IAP 5.3 中实现 Windows Custom Store 实战教程
windows·unity·游戏引擎
unityのkiven4 小时前
工作分享1(26.5.27):基于栈实现全局返回逻辑通用架构设计(适配异步 + 确认弹窗)
游戏·unity·c#·客户端架构
winlife_18 小时前
在 Unity 里用 AI 做游戏:funplay-unity-mcp 从安装到第一次让 AI 改场景
人工智能·游戏·unity·ai编程·claude·mcp
qq_2052790518 小时前
Unity 运行时候会时不时卡顿一下,哪怕是空场景
unity·游戏引擎
美团骑手阿豪1 天前
Unity UGUI自适应分辨率
unity·游戏引擎
LONGZETECH1 天前
软硬协同+故障注入:无人机仿真维修与操控仿真底层算法逻辑拆解
大数据·c语言·算法·3d·unity·无人机
winlife_1 天前
让 AI 跑通“调跳跃手感“的完整闭环:funplay-unity-mcp 实战案例
人工智能·unity·游戏引擎·ai编程·mcp·游戏手感