27.Shader语法基础总结

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 MapsKeywords(会牵动 shader 变体)、PropertiesCast ShadowsRender QueueLOD,以及是否容易被打进同一批(Dynamic/Static Batching 等语境)。它们共同决定物体大概在什么队列画、会不会多一批编译出来的变体、深度/阴影是否参与

Shader 源码解决「算法长什么样」,材质解决「这一份算法用哪组数」。面板上的颜色、浮点、贴图,会在运行时灌进 GPU 上的 uniform;你在 Properties 里声明、在 CG 里写同名变量,两边就对上了。想快速试效果时,优先改材质实例,不必每次动 .shader 文件。

ShaderLab 文件长什么样

ShaderLab 是 Unity 用来描述「资源名、面板参数、渲染状态、多 Pass、兜底」的格式本身;具体数学和光照怎么写,仍然在 Pass 的 CGPROGRAM … ENDCGHLSLPROGRAM 里完成。

职责
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}]
  • 数值:IntFloatRange(min, max)
  • 颜色 / 向量:Color(RGBA)、Vector(XYZW)
  • 纹理:2D2DArrayCube3D

纹理默认值常写 whiteblackgraybumpred 等,后面跟 {} 是固定语法。贴图的 Tiling / Offset 会出现在 *_ST(例如 _MainTex_STxy 为缩放,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 上的 TagsPass 里的 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 常见 OpaqueTransparentTransparentCutout 等。DisableBatching 可取 "True""False""LODFading"(LOD 渐变时禁用批处理)。精灵、预览类型等还有 CanUseSpriteAtlasPreviewType 等标签,用到再查文档即可。

渲染状态

控制面剔除、深度写入与测试、混合、以及 LODColorMask 等。写在 SubShader 级则作用于其下所有 Pass;写在某个 Pass 里则只影响该 Pass。

  • CullBack(默认背面剔除)、FrontOff(双面)
  • ZWrite:透明物体常关深度写入,避免错误遮挡
  • ZTest:与深度缓冲比较,常用 LEqual(默认)、LessAlways
  • Blend:例如 SrcAlpha OneMinusSrcAlpha 做常规 Alpha 混合,One One 做加法混合;要和 Queue、排序方式一起考虑

Pass

Pass 是管线里最小的一步绘制。一个 SubShader 里可以堆多个 Pass:多光源附加、阴影投射、描边、后处理多步等都会用到。Pass 里可以再起 NameUsePass 引用(名字会被转成大写)、可以写自己的 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 / fragLightMode、混合与深度 控制面最大;样板代码和状态都要自己管
固定函数(ShaderLab 老命令) 只用 LightingSetTexture 等声明式语句 早期教材常见;硬件固定管线早已不存在,Unity 会编译成可编程 Shader,新项目不必新写

Surface:在 SubShader 的 CGPROGRAM 里写 #pragma surface,用 surfSurfaceOutputStandard 的 Albedo、Metallic、Smoothness、Alpha 等,适合 PC/主机上快速出 Standard 光照效果;移动平台务必看展开后的 Pass 数和 Keyword。

顶点/片元:#pragma vertex / #pragma fragment 指明入口,自己定义 appdatav2f、雾效宏等。自定义管线、移动优化、特殊混合和非标准光照,多半走这条。

固定函数:例如 Lighting OnSetTexture[_MainTex]{ Combine texture },多见于旧资料;弄懂历史即可,新代码用 Surface 或手写 Pass。

CG / HLSL 写在哪儿、怎么组织

可执行代码必须放在 PassCGPROGRAM … ENDCG(或等价 HLSL 块)里。入口用 #pragma vertex / #pragma fragment 绑定函数名,多数示例会 #include "UnityCG.cginc",雾效用 #pragma multi_compile_fog 等按需加。

数据从 CPU 侧进 GPU,大致路径是:应用阶段填充顶点缓冲 → 顶点着色器读 POSITION / NORMAL / TEXCOORD 等 → 输出里必须带 SV_POSITION(裁剪空间)→ 经光栅化插值进片元着色器 → 片元输出 SV_Target。需要把模型空间位置、法线、UV 等多路数据传到片元时,用自定义结构体(例如 a2vv2f)加语义标注即可。

片元着色器看不到未经传递的顶点数据:模型空间坐标若在片元里要用,必须在顶点着色器里算好,写进 v2f 的某个 TEXCOORD 槽再插值下去。这是写 VF 时最常见的坑之一。

类型、向量、矩阵、Swizzle

标量与整型含 intuintfloathalffixedbool 等。移动平台常在精度和带宽允许的前提下用 half 存颜色和中间量;fixed 在不同后端上语义略有差异,不要假设和桌面完全一致。

sampler2DsamplerCUBEsampler3Dsampler2DArray 等与纹理采样函数配套使用,具体哪些符号在当前宏和管线下可用,以编译报错和官方文档为准。数组语法接近 C,没有 .Length 之类属性,长度要自己记。结构体写法接近 C#,没有访问修饰符,定义末尾要分号。

向量 float2float4,矩阵 float3x3float4x4 等;比较运算对向量逐分量作用时会产生 bool2bool3 之类结果。Swizzle 用 .xyzw.rgba 重排分量;矩阵用 [行][列] 取下标,高维到低维赋值时有截断规则,以目标 profile 为准。

lerp 与数学上的线性插值一致(Hexo 下块级公式需双反斜杠定界):

\[
\text{lerp}(a,b,t) = (1-t)a + tb
\]

运算符与控制流

比较、?: 三元运算符与 C# 类似。要特别记住两点:一是 &&|| 不会像 C# 那样短路,左右两边都会算,别用「左边为假右边不算」来写防护;二是 % 主要给整数用,浮点余数用 fmod 等函数(依平台而定)。

if / switch / 各类循环都能写,但 GPU 上动态分支和大步长循环都贵,能展开成少量指令或查表就尽量别硬循环。

inoutinout

in 只读传入;out 表示输出参数,函数返回前必须赋值;inout 可读可写。顶点函数和片元函数通常用返回值带结果,再额外堆一堆 out 可读性会下降,除非确实需要多返回值。

语义(Semantics)

语义告诉编译器:这个变量从哪个阶段来、写到哪个寄存器。顶点输入常见 POSITION(模型空间顶点)、NORMALTANGENTTEXCOORDnCOLOR;顶点输出到片元阶段必须有 SV_POSITION(裁剪空间),其余数据走 TEXCOORD0TEXCOORD7COLOR0 等插值槽。片元输出到渲染目标用 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 常见写法
ColorVector float4 / half4 / fixed4
RangeFloatInt 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 里与宏一起出现。

分类 符号
方向 / 法线辅助 WorldSpaceViewDirObjSpaceViewDirWorldSpaceLightDir(前向主光)、UnityObjectToWorldNormalUnityObjectToWorldDir
物体与世界矩阵 unity_ObjectToWorldunity_WorldToObject(及同文件中的相关宏)

采样、向量与标量运算里,下面这类内置函数出现频率很高;是否可用仍以目标 API、shader model 和编译报错为准。

分类 函数
纹理采样 tex2DtexCUBE
向量与矩阵 dotcrossnormalizelengthdistancereflectrefractmul
插值与分段 lerpsaturateclampsmoothsteppow
数值与三角 absmaxminsincosatan2

全局量如 _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 名字、PropertiesSubShader(可多个)、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 内声明与属性同名类型匹配的变量(如 Colorfloat42Dsampler2D)。
  • 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 的工程含义

题目

UsePassGrabPass 分别适合什么需求?使用时要考虑哪些性能点?

深入解析
  • UsePass:复用其他 Shader 中已命名 Pass,减少重复代码;Pass 名会转大写,路径要正确。
  • GrabPass:抓取当前屏幕颜色供后续采样(折射、扭曲等);每帧额外复制/渲染成本大,移动项目要慎用或减少分辨率。
  • 性能:GrabPass 常比多 Pass 纯几何更伤带宽;UsePass 主要注意 Shader 变体与包含关系是否清晰。
答题示例

UsePass 是引用别人写好的 Pass 复用逻辑。
GrabPass 是抓屏给后面用,效果强但贵,移动端要特别小心。

参考文章
  • 8.ShaderLab-ShaderLab语法规则-Shader的子着色器-Pass渲染通道

3. float、half、fixed 的选用与语义

题目

在 Unity Shader 中 floathalffixed 大致对应什么精度?移动端为何要区分?

深入解析
  • float:32 位,全平台一致性好,用于位置、矩阵、需要高精的标量。
  • half:16 位浮点,适合颜色、UV 插值、中间量,减少寄存器与带宽压力。
  • fixed:历史上有低精度定点语义,现代平台常按最低精度实现;适合部分颜色、遮罩等,勿假设跨平台数值行为完全一致
  • 移动端:过多 float varyings 会增加插值与寄存器压力;在精度够用处用 half 有助于 ALU 与带宽。
答题示例

float 最高精度,half 常用在中间计算和颜色,fixed 更省但要留心平台差异。
移动上要控制 varying 精度和数量来省带宽和寄存器。

参考文章
  • 15.CG-基础数据类型
  • 16.CG-特殊数据类型


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏