27.Shader语法基础总结
27.1 核心要点速览
材质与 Unity Shader
| 概念 | 在工程里指什么 | 类比(仅帮助记忆) |
|---|---|---|
| Shader(泛指) | 决定像素怎么算、怎么跟光和后处理打交道 | 菜谱 |
| Unity Shader | ShaderLab 外壳 + 若干 Pass,Pass 里写 CG/HLSL |
把菜谱写进固定格式的单子 |
| Material | 选定某个 Unity Shader,再填一组建模面板参数 | 按单子炒出来的一盘菜 |
| 日常工作流 | 建材质 → 选 Shader → 赋给 Renderer → 在 Inspector 里调参 | 换口味多半只改调料,不必重写厨房 |
日常操作一般是:Project 里 Create → Material,Inspector 的 Shader 下拉选中目标,下面出现的各项就是 Properties 里暴露出来的参数。新建 .shader 时,模板里常见 Surface、Unlit(学顶点/片元多半从这里下手)、Image Effect、Compute、Ray Tracing 等,按你用的是内置管线还是 URP/HDRP、以及目标平台来选。
Inspector 里和渲染行为强相关的几项建议扫一眼:Default Maps、Keywords(会牵动 shader 变体)、Properties、Cast Shadows、Render Queue、LOD,以及是否容易被打进同一批(Dynamic/Static Batching 等语境)。它们共同决定物体大概在什么队列画、会不会多一批编译出来的变体、深度/阴影是否参与。
Shader 源码解决「算法长什么样」,材质解决「这一份算法用哪组数」。面板上的颜色、浮点、贴图,会在运行时灌进 GPU 上的 uniform;你在 Properties 里声明、在 CG 里写同名变量,两边就对上了。想快速试效果时,优先改材质实例,不必每次动 .shader 文件。
ShaderLab 文件长什么样
ShaderLab 是 Unity 用来描述「资源名、面板参数、渲染状态、多 Pass、兜底」的格式本身;具体数学和光照怎么写,仍然在 Pass 的 CGPROGRAM … ENDCG 或 HLSLPROGRAM 里完成。
| 块 | 职责 |
|---|---|
Shader "路径/名字" |
资源在工程里的逻辑名,材质面板里按路径分级显示 |
Properties { } |
把颜色、标量、向量、纹理等暴露给 Inspector 和脚本 |
SubShader { }(可多个) |
Unity 选用第一个当前 GPU 能跑通的;内部写 Tags、渲染状态、若干 Pass |
Fallback "..." |
所有 SubShader 都失败时的备用 Shader;Fallback Off 表示不兜底 |
新建 Surface Shader 时,Properties 里常见 _Color、_MainTex、_Glossiness、_Metallic,子着色器里 #pragma surface surf Standard 走内置 Standard 光照;需要看 Unity 帮你展开了多少东西、生成了哪些变体,可以在 Inspector 里用 Compile and show code 看展开结果——移动端尤其值得扫一眼 Pass 数量和 #pragma 变体。
Shader "名字" 与文件名
首行字符串决定材质面板里的层级路径。工程里习惯文件名和 Shader "..." 字符串保持一致,搜索、比对、合并冲突都省事;避免中文和奇怪符号。要改显示路径,直接改 .shader 第一行即可。
Properties
面板和 GPU 之间的契约:Properties 里写名字和类型,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 数量从来都是性能敏感指标。
SubShader 里三块东西要分清:Tags 跟渲染队列、材质分类、批处理之类「策略」打交道;Cull / ZWrite / ZTest / Blend / LOD / ColorMask 等渲染状态决定这一档默认怎么跟缓冲交互;真正画一帧里的一步的是 Pass。高端与低端可以分多个 SubShader 写,用能力检测或简化版 Pass 做降级。
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 后面。
Surface、顶点/片元、固定函数:怎么选
| 写法 | 你在写什么 | 典型取舍 |
|---|---|---|
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 / HLSL 写在哪儿、怎么组织
可执行代码必须放在 Pass 的 CGPROGRAM … ENDCG(或等价 HLSL 块)里。入口用 #pragma vertex / #pragma fragment 绑定函数名,多数示例会 #include "UnityCG.cginc",雾效用 #pragma multi_compile_fog 等按需加。
数据从 CPU 侧进 GPU,大致路径是:应用阶段填充顶点缓冲 → 顶点着色器读 POSITION / NORMAL / TEXCOORD 等 → 输出里必须带 SV_POSITION(裁剪空间)→ 经光栅化插值进片元着色器 → 片元输出 SV_Target。需要把模型空间位置、法线、UV 等多路数据传到片元时,用自定义结构体(例如 a2v、v2f)加语义标注即可。
片元着色器看不到未经传递的顶点数据:模型空间坐标若在片元里要用,必须在顶点着色器里算好,写进 v2f 的某个 TEXCOORD 槽再插值下去。这是写 VF 时最常见的坑之一。
类型、向量、矩阵、Swizzle
标量与整型含 int、uint、float、half、fixed、bool 等。移动平台常在精度和带宽允许的前提下用 half 存颜色和中间量;fixed 在不同后端上语义略有差异,不要假设和桌面完全一致。
sampler2D、samplerCUBE、sampler3D、sampler2DArray 等与纹理采样函数配套使用,具体哪些符号在当前宏和管线下可用,以编译报错和官方文档为准。数组语法接近 C,没有 .Length 之类属性,长度要自己记。结构体写法接近 C#,没有访问修饰符,定义末尾要分号。
向量 float2~float4,矩阵 float3x3、float4x4 等;比较运算对向量逐分量作用时会产生 bool2、bool3 之类结果。Swizzle 用 .xyzw、.rgba 重排分量;矩阵用 [行][列] 取下标,高维到低维赋值时有截断规则,以目标 profile 为准。
lerp 与数学上的线性插值一致(Hexo 下块级公式需双反斜杠定界):
\[
\text{lerp}(a,b,t) = (1-t)a + tb
\]
运算符与控制流
比较、?: 三元运算符与 C# 类似。要特别记住两点:一是 && 和 || 不会像 C# 那样短路,左右两边都会算,别用「左边为假右边不算」来写防护;二是 % 主要给整数用,浮点余数用 fmod 等函数(依平台而定)。
if / switch / 各类循环都能写,但 GPU 上动态分支和大步长循环都贵,能展开成少量指令或查表就尽量别硬循环。
in、out、inout
in 只读传入;out 表示输出参数,函数返回前必须赋值;inout 可读可写。顶点函数和片元函数通常用返回值带结果,再额外堆一堆 out 可读性会下降,除非确实需要多返回值。
语义(Semantics)
语义告诉编译器:这个变量从哪个阶段来、写到哪个寄存器。顶点输入常见 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 |
Range、Float、Int |
float / half / fixed |
2D |
sampler2D |
Cube |
samplerCUBE(或当前管线下的立方体贴图类型) |
3D |
sampler3D |
2DArray |
sampler2DArray |
Properties 里写 _MyColor,CG 里就声明 float4 _MyColor,材质一改,着色器里读到的就是新值。
.cginc 与常用函数
安装目录 Editor/Data/CGIncludes 下自带一批 .cginc,工程里用 #include 引用。下面这张表记个大概,真写 Shader 时以当前版本文件为准:
| 文件 | 内容侧重 |
|---|---|
UnityCG.cginc |
常用 appdata_*、v2f_img、矩阵与向量辅助函数 |
Lighting.cginc |
内置光照相关(写 Surface 时常间接用到) |
AutoLight.cginc |
阴影坐标、ATTENUATION 等,前向光照很常用 |
UnityShaderVariables.cginc |
时间 _Time、各类矩阵与全局量,常被自动包含 |
HLSLSupport.cginc |
平台与 API 差异相关的宏 |
UnityCG.cginc 里常点到名的符号可以按下表记;矩阵多在 UnityShaderVariables.cginc 里与宏一起出现。
| 分类 | 符号 |
|---|---|
| 方向 / 法线辅助 | WorldSpaceViewDir、ObjSpaceViewDir、WorldSpaceLightDir(前向主光)、UnityObjectToWorldNormal、UnityObjectToWorldDir |
| 物体与世界矩阵 | unity_ObjectToWorld、unity_WorldToObject(及同文件中的相关宏) |
采样、向量与标量运算里,下面这类内置函数出现频率很高;是否可用仍以目标 API、shader model 和编译报错为准。
| 分类 | 函数 |
|---|---|
| 纹理采样 | tex2D、texCUBE |
| 向量与矩阵 | dot、cross、normalize、length、distance、reflect、refract、mul |
| 插值与分段 | lerp、saturate、clamp、smoothstep、pow |
| 数值与三角 | abs、max、min、sincos、atan2 |
全局量如 _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