27.Shader语法基础总结
27.1 核心要点速览
ShaderLab
材质与 Unity Shader
| 概念 | 在工程里指什么 | 类比(仅帮助记忆) |
|---|---|---|
| Shader(泛指) | 决定像素怎么算、怎么跟光和后处理打交道 | 菜谱 |
| Unity Shader | ShaderLab 外壳 + 若干 Pass,Pass 里写 CG/HLSL |
把菜谱写进固定格式的单子 |
| Material | 选定某个 Unity Shader,再填一组建模面板参数 | 按单子炒出来的一盘菜 |
| 日常工作流 | 建材质 → 选 Shader → 赋给 Renderer → 在 Inspector 里调参 | 换口味多半只改调料,不必重写厨房 |
- 工作流:
Create → Material→ Inspector 选 Shader → 调面板参数。新建.shader时按管线和平台选模板(Surface / Unlit / Compute 等) - Inspector 要扫的项:
Keywords(牵动变体数量)、Render Queue、Cast Shadows、LOD、Batching 兼容性 - Shader vs 材质:Shader 定义算法,Material 填参数;
Properties声明 + CG 同名变量 = 面板到 GPU 的通道。快速试效果改材质实例即可,不必每次动.shader
文件结构
ShaderLab 是 Unity 用来描述「资源名、面板参数、渲染状态、多 Pass、兜底」的格式本身;具体数学和光照怎么写,仍然在 Pass 的 CGPROGRAM … ENDCG 或 HLSLPROGRAM 里完成。
| 块 | 职责 |
|---|---|
Shader "路径/名字" |
资源在工程里的逻辑名,材质面板里按路径分级显示 |
Properties { } |
把颜色、标量、向量、纹理等暴露给 Inspector 和脚本 |
SubShader { }(可多个) |
Unity 选用第一个当前 GPU 能跑通的;内部写 Tags、渲染状态、若干 Pass |
Fallback "..." |
所有 SubShader 都失败时的备用 Shader;Fallback Off 表示不兜底 |
Surface Shader 用 #pragma surface surf Standard 走内置光照,Unity 会帮你展开大量代码和变体。Inspector 里 Compile and show code 可查看展开结果——移动端尤其注意 Pass 数量和变体规模。
首行 Shader "路径/名字" 的字符串决定材质面板里的层级路径。工程里习惯文件名和 Shader 字符串保持一致,搜索、比对、合并冲突都省事;避免中文和奇怪符号。要改显示路径,直接改 .shader 第一行即可。
Properties 是面板和 GPU 之间的契约:写名字和类型,CG 里用同名、类型兼容的变量接住。
_Name("Display Name", type) = defaultValue[{options}]
- 数值:
Int、Float、Range(min, max) - 颜色 / 向量:
Color(RGBA)、Vector(XYZW) - 纹理:
2D、2DArray、Cube、3D等
纹理默认值常写 white、black、gray、bump、red 等,后面跟 {} 是固定语法。贴图的 Tiling / Offset 会出现在 *_ST(例如 _MainTex_ST:xy 为缩放,zw 为偏移),顶点里配合 TRANSFORM_TEX 很常见。完整类型对照见后文表格。
SubShader:Tags、渲染状态、Pass
Unity 从上到下尝试 SubShader,命中第一个能完整编译通过的;该 SubShader 里的 Pass 按顺序执行。每多一个 Pass,通常就多画一遍几何,开销直接累加,所以 Pass 数量从来都是性能敏感指标。
- Tags:渲染队列、材质分类、批处理等”策略”层面的键值对
- 渲染状态:
Cull/ZWrite/ZTest/Blend/LOD/ColorMask— 决定与缓冲区的默认交互 - Pass:一帧里一步实际绘制;每多一个 Pass 就多画一遍几何,开销直接累加。高低端可写多个 SubShader,用能力检测做降级
Tags(子着色器级)
这里写的是键值对,用来告诉引擎大致什么时候画、属于哪类材质、要不要为了正确性牺牲一点批处理。注意:SubShader 上的 Tags 和 Pass 里的 Tags(例如 LightMode)不是一回事,不要抄错层级。
示例:
Tags { "Queue"="Geometry" "RenderType"="Opaque" }
| 键 | 用途摘要 |
|---|---|
Queue |
控制绘制顺序的大类(不透明 / 裁切透明 / 半透明等) |
RenderType |
给 Shader 分类,替换 Shader、后处理、按类型抓物体时会用到 |
DisableBatching |
是否在特定情况下关掉 Dynamic Batching 等 |
ForceNoShadowCasting |
是否禁止投射阴影 |
IgnoreProjector |
是否不受 Projector 影响(半透明常用) |
内置队列大致为:Background 1000 → Geometry 2000 → AlphaTest 2450 → Transparent 3000 → Overlay 4000;也可以写成 "Geometry+1"、"Transparent-1" 做微调。RenderType 常见 Opaque、Transparent、TransparentCutout 等。DisableBatching 可取 "True"、"False"、"LODFading"(LOD 渐变时禁用批处理)。精灵、预览类型等还有 CanUseSpriteAtlas、PreviewType 等标签,用到再查文档即可。
渲染状态
控制面剔除、深度写入与测试、混合、以及 LOD、ColorMask 等。写在 SubShader 级则作用于其下所有 Pass;写在某个 Pass 里则只影响该 Pass。
Cull:Back(默认背面剔除)、Front、Off(双面)ZWrite:透明物体常关深度写入,避免错误遮挡ZTest:与深度缓冲比较,常用LEqual(默认)、Less、Always等Blend:例如SrcAlpha OneMinusSrcAlpha做常规 Alpha 混合,One One做加法混合;要和Queue、排序方式一起考虑
Pass
Pass 是管线里最小的一步绘制。一个 SubShader 里可以堆多个 Pass:多光源附加、阴影投射、描边、后处理多步等都会用到。Pass 里可以再起 Name 供 UsePass 引用(名字会被转成大写)、可以写自己的 Tags(例如前向里的 LightMode)、可以覆写局部渲染状态,最后才是 CGPROGRAM。
UsePass "其它Shader/大写PASS名" 用来复用别人写好的通道;GrabPass 抓屏给后续 Pass 做折射、扭曲等,效果直观但带宽和额外绘制都贵,移动项目要慎用。
Fallback
当前硬件跑不了所有 SubShader 里的 Pass 时,Unity 会退到 Fallback 指定的简单 Shader,至少保证物体还在画面上。写成 Fallback Off 则关闭兜底,有可能什么都不渲染。Fallback 写在所有 SubShader 后面。
Shader 编写形式
| 写法 | 你在写什么 | 典型取舍 |
|---|---|---|
Surface(#pragma surface) |
Unity 帮你展开多 Pass,拼光照和阴影 | 写得快;展开结果黑盒、变体多,移动端要盯编译结果 |
| 顶点 / 片元 | 自己在 Pass 里写 vert / frag、LightMode、混合与深度 |
控制面最大;样板代码和状态都要自己管 |
| 固定函数(ShaderLab 老命令) | 只用 Lighting、SetTexture 等声明式语句 |
早期教材常见;硬件固定管线早已不存在,Unity 会编译成可编程 Shader,新项目不必新写 |
Surface:在 SubShader 的 CGPROGRAM 里写 #pragma surface,用 surf 填 SurfaceOutputStandard 的 Albedo、Metallic、Smoothness、Alpha 等,适合 PC/主机上快速出 Standard 光照效果;移动平台务必看展开后的 Pass 数和 Keyword。
顶点/片元:#pragma vertex / #pragma fragment 指明入口,自己定义 appdata、v2f、雾效宏等。自定义管线、移动优化、特殊混合和非标准光照,多半走这条。
固定函数:例如 Lighting On、SetTexture[_MainTex]{ Combine texture },多见于旧资料;弄懂历史即可,新代码用 Surface 或手写 Pass。
CG 语法
代码组织与数据流
可执行代码必须放在 Pass 的 CGPROGRAM … ENDCG(或等价 HLSL 块)里。入口用 #pragma vertex / #pragma fragment 绑定函数名。
数据从 CPU 到屏幕的流转:
- 应用阶段填充顶点缓冲
- 顶点着色器读
POSITION/NORMAL/TEXCOORD等,输出必须带SV_POSITION(裁剪空间) - 光栅化插值后进入片元着色器 → 输出
SV_Target - 多路数据(位置、法线、UV)传到片元时,用自定义结构体(
a2v、v2f)加语义标注
常见坑:片元着色器看不到未经传递的顶点数据 — 模型空间坐标若片元要用,必须在顶点着色器里算好写进 v2f 的 TEXCOORD 槽。
数据类型与 Swizzle
- 标量:
int、uint、float、half、fixed、bool。移动端常用half存颜色和中间量减带宽;fixed在不同后端语义略有差异 - 采样器:
sampler2D、samplerCUBE、sampler3D、sampler2DArray,与纹理采样函数配套 - 数组:语法接近 C,没有
.Length,长度自己记 - 结构体:写法接近 C#,无访问修饰符,定义末尾要分号
- 向量:
float2~float4;比较运算逐分量,产生bool2、bool3等 - 矩阵:
float3x3、float4x4;用[行][列]取下标 - Swizzle:
.xyzw/.rgba重排分量;高维到低维赋值有截断规则
lerp 与数学上的线性插值一致:
\[
\text{lerp}(a,b,t) = (1-t)a + tb
\]
颜色过渡、动画混合、雾效混合等场景几乎都离不开 lerp。
运算符与控制流
比较、?: 三元运算符与 C# 类似。要特别记住两点:一是 && 和 || 不会像 C# 那样短路,左右两边都会算,别用「左边为假右边不算」来写防护;二是 % 主要给整数用,浮点余数用 fmod 等函数(依平台而定)。
if / switch / 各类循环都能写,但 GPU 上动态分支和大步长循环都贵,能展开成少量指令或查表就尽量别硬循环。
参数修饰符:
in(默认):只读传入out:输出参数,函数返回前必须赋值inout:可读可写,相当于引用
顶点/片元函数通常用返回值带结果,out 仅在确实需要多返回值时使用。
语义与结构体
语义告诉编译器:这个变量从哪个阶段来、写到哪个寄存器。顶点输入常见 POSITION(模型空间顶点)、NORMAL、TANGENT、TEXCOORDn、COLOR;顶点输出到片元阶段必须有 SV_POSITION(裁剪空间),其余数据走 TEXCOORD0~TEXCOORD7、COLOR0 等插值槽。片元输出到渲染目标用 SV_Target(个别文档写作 SV_TARGET,以模板为准)。
最简思路:顶点里用 UnityObjectToClipPos 把顶点变到裁剪空间;片元里返回 fixed4 / float4 颜色。字段一多,就用结构体包起来。
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 position : SV_POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
只有顶点/片元入口以及和管线对接的结构体成员需要带语义;普通工具函数按普通 C 风格写参数即可。
ShaderLab 属性与 CG 变量
| ShaderLab | CG / HLSL 常见写法 | 注意 |
|---|---|---|
Color、Vector |
float4 / half4 / fixed4 |
Color 面板返回 [0,1],手动传值可能超范围 |
Range、Float、Int |
float / half / fixed |
Int 在 GPU 端仍是浮点,仅面板约束整数 |
2D |
sampler2D |
默认值写 "white" {},别漏花括号 |
Cube |
samplerCUBE |
管线不同时类型名可能变(如 URP 的 TextureCube) |
3D |
sampler3D |
体积纹理,移动端支持有限 |
2DArray |
sampler2DArray |
需 Shader Model 3.5+,注意平台兼容 |
Properties 里写 _MyColor,CG 里就声明 float4 _MyColor,材质一改,着色器里读到的就是新值。
.cginc 与常用函数
安装目录 Editor/Data/CGIncludes 下自带一批 .cginc,工程里用 #include 引用。下面这张表记个大概,真写 Shader 时以当前版本文件为准:
| 文件 | 内容侧重 | 何时 include |
|---|---|---|
UnityCG.cginc |
appdata_*、v2f_img、矩阵与向量辅助函数 |
几乎所有自定义 Shader 都引,起手模板标配 |
Lighting.cginc |
内置光照模型 | 写 Surface Shader 时常被自动包含 |
AutoLight.cginc |
阴影坐标、ATTENUATION 等 |
前向多光源 / 需要接收阴影时 |
UnityShaderVariables.cginc |
_Time、各类矩阵与全局量 |
一般被 UnityCG 间接包含,无需手动 |
HLSLSupport.cginc |
平台与 API 差异相关宏 | 需要跨平台宏或条件编译时 |
UnityCG.cginc 里常点到名的符号可以按下表记;矩阵多在 UnityShaderVariables.cginc 里与宏一起出现。
| 符号 | 作用 |
|---|---|
WorldSpaceViewDir |
顶点 → 相机的世界空间方向(未归一化) |
ObjSpaceViewDir |
同上,但结果在模型空间 |
WorldSpaceLightDir |
顶点 → 前向主光源方向(仅 ForwardBase) |
UnityObjectToWorldNormal |
法线从模型空间变换到世界空间(含非统一缩放修正) |
UnityObjectToWorldDir |
方向向量从模型空间变换到世界空间 |
unity_ObjectToWorld |
模型 → 世界矩阵(4×4) |
unity_WorldToObject |
世界 → 模型矩阵(4×4),法线变换常用其转置 |
采样、向量与标量运算里,下面这类内置函数出现频率很高;是否可用仍以目标 API、shader model 和编译报错为准。
| 函数 | 作用 |
|---|---|
tex2D(sampler, uv) |
2D 纹理采样,片元最常用 |
texCUBE(sampler, dir) |
立方体贴图采样,反射/天空盒 |
dot(a, b) |
点积,光照余弦、投影长度 |
cross(a, b) |
叉积,求法线或正交向量 |
normalize(v) |
归一化为单位向量 |
length(v) |
向量长度 |
distance(a, b) |
两点距离 |
reflect(i, n) |
入射方向关于法线的反射 |
refract(i, n, eta) |
折射方向,eta 为折射率比 |
mul(M, v) |
矩阵乘向量(或矩阵乘矩阵) |
lerp(a, b, t) |
线性插值,颜色过渡/动画混合常用 |
saturate(x) |
钳到 [0, 1] |
clamp(x, min, max) |
钳到 [min, max] |
smoothstep(edge0, edge1, x) |
Hermite 平滑过渡,0→1 无突变 |
pow(x, y) |
幂运算,高光指数常用 |
abs(x) |
绝对值 |
max(a, b) / min(a, b) |
取较大/较小值 |
sincos(x, out s, out c) |
同时算正弦和余弦,比分开调用快 |
atan2(y, x) |
反正切,返回 [-π, π] |
全局量如 _Time、_LightColor0 等定义在对应头文件 / 管线封装中,随 Unity 版本与渲染路径变化,以当前工程能编过的模板和官方文档为准。
头文件多的时候,可以按三条线记:几何与顶点输入输出在 UnityCG,矩阵时间在 UnityShaderVariables,光照与阴影辅助在 Lighting / AutoLight。上手时先从一个 Unlit、单 Pass、只引 UnityCG.cginc 的模板抄起,再按需加 include,比一次塞满一堆头文件好排错。
27.2 面试题精选
基础题
1. ShaderLab 由哪几部分组成
题目
Unity Shader 的 ShaderLab 结构包含哪几块?各自承担什么角色?
深入解析
Shader "路径/名称":资源在工程中的逻辑名,决定材质 Inspector 里的层级路径。Properties:把颜色、浮点、纹理等暴露到材质面板;CG/HLSL 里用同名变量接收。- 一个或多个
SubShader:Unity 选第一个 GPU 能跑通的;内部写 Tags、渲染状态、Pass。 Fallback:所有 SubShader 都不支持时的兜底,避免物体完全不画;Fallback Off则放弃兜底。
答题示例
四块:
Shader名字、Properties、SubShader(可多个)、Fallback。
Unity 用第一个能编译通过的 SubShader;都不行就走 Fallback。
参考文章
- 2.ShaderLab-ShaderLab基本结构
2. SubShader 与 Pass 的关系
题目
SubShader 和 Pass 分别是什么?多 Pass 一般用来做什么?
深入解析
- SubShader:一层「方案」,含标签、共享状态、若干 Pass;用于降级或多版本实现。
- Pass:最小绘制单位,一次 Pass 往往对应管线里一轮几何/光照/后处理步骤。
- 多 Pass 常见于:多光源附加通道、阴影投射/采集、描边、后处理多步、GrabPass 后再画等。
答题示例
SubShader 是一整包渲染方案;Pass 是其中一步。
需要多轮渲染效果时就要多个 Pass,每 Pass 可设自己的 LightMode 和状态。
参考文章
- 5.ShaderLab-ShaderLab语法规则-Shader的子着色器-基本构成
- 8.ShaderLab-ShaderLab语法规则-Shader的子着色器-Pass渲染通道
3. Properties 与 CG 变量如何对应
题目
在 Properties 里声明的属性,怎样在着色器代码里使用?
深入解析
- 在
CGPROGRAM内声明与属性同名、类型匹配的变量(如Color→float4,2D→sampler2D)。 - Unity 在材质赋值时把面板值注入这些 uniform。
- 常见坑:名字不一致、类型不匹配、在错误的 Pass 里未声明。
答题示例
Properties 里写
_MainTex,CG 里就声明sampler2D _MainTex,类型要和 ShaderLab 类型对上。
这样改材质面板就会传到 GPU。
参考文章
- 4.ShaderLab-ShaderLab语法规则-Shader的属性
- 24.CG-ShaderLab属性类型和CG变量类型的匹配关系
进阶题
1. Tags 中 Queue、RenderType、Pass 的 LightMode 各解决什么问题
题目
RenderQueue(Queue)、RenderType、Pass 里的 LightMode 分别作用于什么层级?各有什么典型用途?
深入解析
- Queue(SubShader Tags):决定绘制顺序的大类(Geometry、Transparent 等),半透明必须晚于不透明。
- RenderType:给 Shader 做分类(Opaque、Transparent 等),便于替换着色(如相机后期、URP 特性)。
- LightMode(Pass Tags):告诉管线这个 Pass 参与哪条光照/阴影路径(如 ForwardBase、ForwardAdd、ShadowCaster),选错会导致不照光或阴影缺失。
答题示例
Queue 管谁先画后画;RenderType 管「你是哪类材质」方便系统替换;LightMode 管这个 Pass 在光照管线里扮演什么角色。
透明材质通常 Queue 靠后,且 Pass 里要配对的混合与深度策略。
参考文章
- 6.ShaderLab-ShaderLab语法规则-Shader的子着色器-Tags渲染标签
- 8.ShaderLab-ShaderLab语法规则-Shader的子着色器-Pass渲染通道
2. 表面着色器与顶点/片元着色器的选型
题目
什么时候优先写 Surface Shader,什么时候必须上顶点/片元?各有什么代价?
深入解析
- Surface Shader:Unity 展开为多 Pass,自动处理前向光照、阴影等;适合快速 Standard 类效果,隐式 Pass 多、可控性弱。
- 顶点/片元:每个 Pass 手写,状态、数据结构、优化完全自控;移动平台、自定义光照、后处理、非标准管线通常选它。
- 工程上:原型与 PC 可用 Surface;要抠 DrawCall、Pass 数或写特殊管线时用 VF。
答题示例
要快出带多光源的 PBR 可以用 Surface;要控 Pass、做特殊混合或非标准光照就用顶点片元。
Surface 展开后往往更重,移动端要谨慎。
参考文章
- 10.ShaderLab-Shader编写形式-表面着色器
- 11.ShaderLab-Shader编写形式-顶点片元着色器
3. CG 中逻辑运算与取余和 C# 的差异
题目
CG/HLSL 里 &&、|| 与 C# 有何重要差异?% 能用于浮点吗?
深入解析
- 无短路:
&&/||两侧都会求值,不能像 C# 那样靠短路省掉副作用或除零保护。 %仅整数:浮点取余需用fmod等函数(依平台与语言子集而定)。- 写 Shader 时条件表达式要显式拆开,避免假设「左边假就不算右边」。
答题示例
CG 里逻辑与或没有短路,两边都会算。
取余只能用于整数,浮点要用别的 API。
参考文章
- 18.CG-运算符相关
深度题
1. 半透明与深度、队列、混合的协同
题目
为什么常见透明 Shader 会关 ZWrite、提高 RenderQueue,并配合 Blend?乱改会怎样?
深入解析
- ZWrite Off:透明片元不写入深度,否则后画的透明体会挡住后面应透过看到的内容;但排序错误仍会出现穿插错误。
- Queue 靠后:在不透明之后绘制,才能看到背后已固化的颜色与深度结果。
- Blend:按
SrcAlpha OneMinusSrcAlpha等方程与帧缓冲混合。 - 乱改后果:开 ZWrite 可能导致透明互相遮挡错误;Queue 不对会与 opaque 顺序错乱;Blend 与预乘、裁剪不匹配会发黑或边缘脏。
答题示例
透明一般后画、常关深度写入,用 Blend 做Alpha混合。
否则顺序一乱就会出现错误的遮挡或颜色。排序问题严重时还要排序物体或用更复杂技术。
参考文章
- 7.ShaderLab-ShaderLab语法规则-Shader的子着色器-States渲染状态
- 6.ShaderLab-ShaderLab语法规则-Shader的子着色器-Tags渲染标签
2. UsePass 与 GrabPass 的工程含义
题目
UsePass 和 GrabPass 分别适合什么需求?使用时要考虑哪些性能点?
深入解析
- UsePass:复用其他 Shader 中已命名 Pass,减少重复代码;Pass 名会转大写,路径要正确。
- GrabPass:抓取当前屏幕颜色供后续采样(折射、扭曲等);每帧额外复制/渲染成本大,移动项目要慎用或减少分辨率。
- 性能:GrabPass 常比多 Pass 纯几何更伤带宽;UsePass 主要注意 Shader 变体与包含关系是否清晰。
答题示例
UsePass 是引用别人写好的 Pass 复用逻辑。
GrabPass 是抓屏给后面用,效果强但贵,移动端要特别小心。
参考文章
- 8.ShaderLab-ShaderLab语法规则-Shader的子着色器-Pass渲染通道
3. float、half、fixed 的选用与语义
题目
在 Unity Shader 中 float、half、fixed 大致对应什么精度?移动端为何要区分?
深入解析
- float:32 位,全平台一致性好,用于位置、矩阵、需要高精的标量。
- half:16 位浮点,适合颜色、UV 插值、中间量,减少寄存器与带宽压力。
- fixed:历史上有低精度定点语义,现代平台常按最低精度实现;适合部分颜色、遮罩等,勿假设跨平台数值行为完全一致。
- 移动端:过多
floatvaryings 会增加插值与寄存器压力;在精度够用处用half有助于 ALU 与带宽。
答题示例
float 最高精度,half 常用在中间计算和颜色,fixed 更省但要留心平台差异。
移动上要控制 varying 精度和数量来省带宽和寄存器。
参考文章
- 15.CG-基础数据类型
- 16.CG-特殊数据类型
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com