资讯动态

Unity ShaderLab 格式详解:四层骨架与 CGPROGRAM

发布时间:2026/9/29 3:59:43 来源:尧图企业网站定制
刚接触 Unity Shader 那会儿我在 Assets 里新建出一个 .shader 文件盯着满屏的大括号、中括号和看起来像 JSON 又不像 JSON 的东西看了半天。写着写着编译器就报错报错信息还特别含糊只说某一行 unexpected token具体是哪的问题得自己一格格去猜。后来我把 Unity 手册里 ShaderLab 那一章翻来覆去看了几遍又在小项目里连续摔了几个跟头才算真正把 shader 语言的格式规律摸清楚。这一篇是 shader 基础系列的第二集专讲格式不碰光照模型也不推数学公式。上一集我们聊了 shader 是什么、它在渲染流程里处在哪个环节这一集把 ShaderLab 这门语言的骨架拆开一个 shader 文件由哪几层嵌套组成、每一层的括号和分号怎么摆、Properties 和 Pass 内部有哪些约定俗成的写法以及那些让新手反复踩的格式坑。1. 从大括号说起ShaderLab文件的四层骨架1.1 最外层的 Shader 块承担了什么角色任何一个 Unity shader 文件第一个非注释的语句一定是Shader关键字后面跟一个用双引号包起来的名字然后是一对大括号把所有内容罩住。这个最外层的块不是可有可无的装饰它决定了这个 shader 在 Unity 眼里是一个不可分割的资产——一个 .shader 文件只能有一个顶层Shader块你没办法在同一个文件里并排写两个写了会直接编译失败。名字那一串字符串要按路径来理解。写Custom/MyShader它就会出现在材质面板的 shader 下拉菜单里路径是 Custom 下面一个叫 MyShader 的条目写成Hidden/MyShader它就会从材质面板里藏起来只能靠代码或者特定组件引用很多后处理和后端工具用的就是这种隐藏命名。斜杠可以有多层MyFolder/SubFolder/Effect也是合法的层级越深在菜单里嵌套越深。命名这件事看着随意实际建议从一开始就养成习惯把项目自己的 shader 统一放在一个自己起的前缀下面比如Game/Water/River将来 shader 一多找起来不抓狂。顶层Shader块里面允许出现的东西是固定的几种——Properties、SubShader、FallBack、CustomEditor。除了这四类你往里塞别的东西基本都会报错。其中Properties只能出现一次SubShader可以出现任意多次FallBack和CustomEditor各至多一次。1.2 Properties、SubShader、Pass 各自的分工这三兄弟经常被新手搞混其实分工非常清楚。Properties块负责的是“这个 shader 在材质面板上长什么样”——它声明的是材质层面的可调参数也就是美术同学在 Inspector 里能看到、能拖滑块、能选贴图的那一堆东西。它本质上只是 UI 声明不参与真正的渲染逻辑你在里面写什么名字、什么类型直接决定材质面板的呈现。SubShader块负责“在不同硬件能力下跑哪一段逻辑”。Unity 会从上往下检查每一个SubShader找到第一个当前显卡能支持的然后就用它后面的全部忽略。这就是为什么你可以为高端机写一个复杂效果、为老设备补一个简化版把它们按顺序摆进同一个文件里。Pass块藏在SubShader内部是最贴近渲染管线的部分。一个SubShader里可以有多个Pass每个Pass代表一次完整的绘制调用。前向渲染里常见的 ForwardBase 加 ForwardAdd 组合就是同一个SubShader里两个不同Pass各管一段光照计算。可以这么理解Properties是给美术看的菜单SubShader是按设备能力分层跑的方案Pass是方案里真正干活的每一次绘制。1.3 为什么 SubShader 可以有很多个而 Properties 只能有一个这里有个新手常问的问题既然渲染逻辑在 SubShader 里为什么属性反而只能声明一遍不能在每个 SubShader 里各写一套原因在于属性的作用域是材质而材质是跨 SubShader 存在的。你在场景里挂了一个材质材质上保存的是属性值运行时切到哪个 SubShader 是设备决定的如果属性每个 SubShader 各写一套那材质里的值到底该按哪套来存就说不清了。所以 Unity 的设计是属性在顶层声明一次所有 SubShader 共享同一份材质数值。反过来说SubShader 之所以要支持多个是因为“能力探测”这件事没法在单个方案里完成。不同显卡支持的指令数量、纹理采样上限、渲染目标格式都不一样Unity 没法用一个 SubShader 覆盖所有情况只能给你一个按顺序兜底的机制。理解了这一点你写 shader 时的心理模型就顺了属性块是全局的一份声明SubShader 是排好队的备选方案。2. Properties属性块名字、显示名与类型的书写规则2.1 一条属性声明的四个组成部分Properties块里每一行声明格式都是固定的四段式内部名字、显示名、类型、默认值。写成模板就是_Name (Display Name, Type) DefaultValue。内部名字必须以大写或小写字母或下划线开头社区约定俗成用下划线开头加驼峰比如_MainTex、_BaseColor、_Glossiness这不是强制要求但跟着来能省很多沟通成本因为 C# 脚本里取值时用的就是这个名字一眼能对上。显示名是给美术看的可以带空格、可以写中文Unity 会原样印在 Inspector 面板上。类型决定了这一项在面板上的交互方式是颜色选择器、是贴图槽、还是滑块。默认值则是材质刚创建、还没被美术调整过时的初始数值。这里第一个容易踩的坑ShaderLab 的属性声明行末尾不要写分号。很多人从 C# 或者 CG 代码段过来手会自动补一个分号结果编译报错还找不到方向。属性块里的每一行就是声明语法上不接受尾部分号。反过来等会儿进入CGPROGRAM之后代码段里的语句又必须写分号。同一个文件里两种写法正是新手最容易被绊倒的地方。2.2 六种基础类型和它们的默认值格式Properties 支持的类型看着不少常用的其实就几种我列个对照表把写法和默认值格式一起摆出来方便直接抄。类型关键字面板呈现默认值写法示例说明Color颜色选择器(1,1,1,1)四通道 RGBA范围 0 到 1Vector四个数值框(1,2,3,4)四分量向量Float单个数值框0.5浮点数无范围限制Range滑块Range(0,1) 0.5类型位置写 Range(min,max)Int整数框1整数实际底层仍以 float 传递2D贴图槽white {}二维纹理Cube立方体贴图槽white {}环境贴图常用3D三维纹理槽black {}体积纹理贴图类型的默认值字符串有几个内置取值white是白色贴图、black是黑色、gray是中灰、bump是默认法线图、red是纯红。后面那对空大括号{}是贴图类型的固定尾巴Unity 官方格式里一直保留着它写漏了会报语法错误。这个小细节几乎所有老手都被坑过一次因为它看起来完全多余但编译器不认情面。Range 这个类型值得单独说一句它的默认值必须落在你声明的区间内写成Range(0,1) 2会报错。它的实际用途是给美术一个视觉上不容易调飞的范围。Float 则没有任何限制你想给多大给多大但也就意味着美术可能拖一个离谱的数进去所以能用 Range 的地方尽量用 Range。2.3 方括号里的属性特性在类型前面还可以套一层方括号叫属性特性用来微调这一项在面板上的行为。常用的有这么几个[HideInInspector]把这一项从面板上藏起来但代码里依然能取到值常用于脚本自动填充的参数。[NoScaleOffset]贴图槽默认会带一组 Tiling 和 Offset 控件加上这个就把它们去掉适合那些不需要平铺的贴图。[Normal]声明这是一张法线贴图面板上会有相应提示导入设置不匹配时也更好排查。[HDR]颜色会以 HDR 方式处理取色器会带上强度倍率做自发光的时候很有用。[MainTexture]和[MainColor]标记主贴图和主颜色Unity 在生成材质缩略图、以及在 C# 里用material.mainTexture取贴图时会参考这两个标记。特性的位置是写在类型前面的比如[NoScaleOffset] _MainTex (Main Tex, 2D) white {}。一个属性可以叠加多个特性顺序无所谓。有个细节值得提属性名称里不能带中文字符和空格只能字母数字下划线组合显示名才允许随便写。见过有人在内部名字里写中文编译直接挂报错还看不出所以然。3. SubShader与Pass的状态格式标签、渲染开关、LOD3.1 Tags 的花括号语法和常用键值对Tags这个块是 ShaderLab 里少数长得像字典的地方格式是Tags { Key1Value1 Key2Value2 }。注意键和值都是字符串中间用等号连接键值对之间不用逗号也不用分号直接空格隔开就行。SubShader 级别的 Tags 和 Pass 级别的 Tags 用的是同一套语法但支持的键不一样。SubShader 层级最常用的几个键RenderType给渲染类型打标签取值有 Opaque、Transparent、TransparentCutout、Background、Overlay 等替换渲染、后处理筛选都会参考它。Queue控制渲染顺序写字符串如 Geometry、Transparent也可以直接写数字。数值越小越先渲染。IgnoreProjector设为 True 时这个物体不受投影器影响适合角色以外的杂物。DisableBatching在需要拿到模型原始顶点数据、又怕 Unity 合批打乱数据时设成 True。ForceNoShadowCasting强制不投射阴影。Pass 层级最关键的键是LightMode。它决定这个 Pass 在前向渲染流程里什么时候被调用ForwardBase 管主方向光和环境光ForwardAdd 管额外的逐像素光源ShadowCaster 负责投射阴影Always 则是不管什么情况都渲染。写多 Pass 前向光照的时候LightMode 写错是效果不对的头号原因因为漏一个 Pass 可能就少了一段光照贡献画面看着就差一口气但又不报错非常难查。3.2 渲染状态的写法Cull、ZWrite、Blend渲染状态指令写在 Pass 里也可以写在 SubShader 里会作为默认值向下传递它们控制的是 GPU 在绘制这个 Pass 时的固定管线行为。这些指令的语法是关键字加参数末尾同样不写分号。Cull Back / Front / Off决定剔除哪一面。默认是 Back也就是不渲染背面。做双面材质时写Cull Off性能会降一点因为片元数量翻倍。ZWrite On / Off是否写入深度缓冲。不透明物体开半透明物体通常关否则会挡住它后面的东西。ZTest LEqual / Always / Greater...深度测试的比较方式。LEqual 是默认值Always 常用于描边或者需要在物体前面强行绘制的东西。Blend系列混合模式像Blend SrcAlpha OneMinusSrcAlpha是常规透明混合Blend One One是叠加。做发光效果时这个参数怎么配直接决定最终亮不亮。这里想多说一句ZWrite和Blend配合的经验。很多新手做半透明效果把Queue改成 Transparent、Blend配好看着像那么回事了但侧面看模型会发现前后关系错乱一半原因是ZWrite没关另一半是模型自己的三角形排序问题。半透明明明是渲染里最难伺候的一类能不做就不做非要做也得先把这两项调对。3.3 LOD 与 FallBack 的位置讲究LOD是一个整数指令写在 SubShader 内部用来配合 C# 里的Shader.globalMaximumLOD做整体降级。比如你给复杂效果标LOD 300、简化版标LOD 100运行时把全局上限设成 100Unity 就会跳过那个 300 的 SubShader。这个机制在需要动态降画质、给低端机兜底时非常好用代价是要提前把每个档位的方案都写出来。FallBack写在所有 SubShader 之后、最外层Shader块的末尾。它告诉 Unity如果前面所有 SubShader 当前设备都跑不了那就退而用哪个内置 shader。常见的写法是FallBack Diffuse或者FallBack VertexLit。还有一种是FallBack Off意思是明确表示没有兜底方案跑不了就算了。什么时候用哪个取决于你对效果的坚持程度能接受画质降级的用前者宁可不显示也不能出错就用后者。CustomEditor是可选的用来指定一个自定义的材质面板编辑器脚本类名用字符串写在这个指令后面。做工具化 shader 的时候才会用到日常基础 shader 不写也完全没问题。4. CGPROGRAM到ENDCG代码段的格式约定4.1 #pragma 指令的顺序和必填项从CGPROGRAM到ENDCG这一大段是真正用 CG 或 HLSL 语法写的着色器代码。这里的语法规则和上面 ShaderLab 那套完全不同该写分号写分号该挖括号挖括号一点都马虎不得。代码段开头通常是若干条#pragma指令。最基础的两条是#pragma vertex 函数名和#pragma fragment 函数名分别指定顶点着色器和片元着色器的入口函数。函数名可以随便起但必须和下面实际定义的函数对得上写错了编译器会明确告诉你找不到入口这个错误反而好查。其余常用的 pragma 有这么几类#pragma target 3.0声明所需的 shader model 版本涉及的指令越多、分支越复杂这个数就得往上升。手机上写 3.0 一般够用用到计算、几何着色器才需要 4.0 以上。#pragma multi_compile和#pragma shader_feature声明变体。做多光源、多开关的时候靠它来分支。它们直接影响打包体积用之前最好想清楚变体数量。光照相关的 pragma比如声明需要完整前向阴影、需要光照贴图等这些属于光照模型范畴格式上就是多写几行而已。pragma 的顺序没有严格强制要求但整个项目里保持一致会让维护舒服很多我的习惯是入口函数放最前target 和变体声明放后面include 放最后。4.2 顶点输入结构体与语义冒号代码段里第一件事通常是声明顶点输入结构体用来接收 Unity 传进来的模型数据。结构体里的每个成员后面要跟一个冒号加语义名比如struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; float4 color : COLOR; };这些语义名都是大写保留字Unity 认识了之后才知道每个字段该填什么数据。POSITION 是模型空间的顶点坐标NORMAL 是法线TANGENT 是切线COLOR 是顶点色TEXCOORD0 到 TEXCOORD7 是八组 UV 通道。这里有个格式细节语义前面的空格有没有都行但冒号不能省省了直接报错。而且语义必须是大写写小写: position编译器不认。UV 通道数量这块要留个心模型上实际有几套 UV 取决于建模软件你声明了 TEXCOORD1 但模型没导出来读到的是全零值不会报错表现出来就是贴图采样全黑或者全白。这种问题最容易在接过别人模型的时候遇到排查第一站就是确认 UV 通道对不对。4.3 v2f 结构体与片元函数返回值顶点着色器的输出结构体一般叫 v2f意思是 vertex to fragment。它的字段语义和输入不完全一样位置信息必须用SV_POSITION这是裁剪空间坐标的专用语义其他需要插值传给片元的数据用TEXCOORD系列语义编号从 0 往后排就行编译器并不关心你第几号通道装的是世界法线还是视方向只关心它能不能插值。struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; };片元函数的返回值同样要带语义。常规颜色输出用fixed4或float4后面跟: SV_Target如果要写多个渲染目标就写SV_Target0、SV_Target1。这里有个细节早年间 Unity 的写法是SV_Target和COLOR两种都能用SL_DX11 之后的平台统一走 SV_Target所以新代码就跟着这个写兼容性没问题。还有一个很常见的坑片元函数如果声明了返回值语义函数体里就一定要有return而且返回类型要和签名一致。声明成fixed4却返回了一个 float3编译器不一定直接报错可能会截断或补零表现出来是颜色偏色或者 alpha 全零导致看不见东西这种问题比语法错误难查十倍。5. 那些让编译器骂人的格式细节5.1 分号ShaderLab里不用CG里必须用前面提过一次这里再强调一遍因为它是新手阶段最高频的报错源。判断规则其实很简单在CGPROGRAM和ENDCG之间的代码段里一切按 C 语言来语句末尾写分号在 ShaderLab 的部分——也就是Properties、Tags、状态指令、FallBack——一律不写分号。属性行末尾加分号、状态指令末尾加分号都是语法错误。记忆方法可以这样ShaderLab 里带括号的写法属性声明、Tags 块是声明式语法像写配置配置行不需要分号状态指令像Cull Off是开关式语法也不需要分号。真正需要分号的是代码段里的赋值、函数调用、返回语句。刚上手的时候我建议每写一行都停一下想想它属于哪一侧写过几十行之后肌肉记忆就有了。5.2 中括号、圆括号、花括号的配对ShaderLab 里三种括号各有明确用途混用会报错。方括号用在属性特性上和函数属性的语义之外实际最常出现在特性声明[NoScaleOffset]、[HDR]里圆括号出现在属性类型Color、Vector、Range的默认值和中括号后的类型参数里花括号出现在 Tags 块、贴图默认值尾巴、以及所有代码块和结构体定义里。新手最容易错的地方是贴图默认值后面那对{}忘写以及 Tags 块里忘记闭合。这两种错误编译器的提示都不算友好经常只指一个大范围的区域。我的做法是写 Tags 的时候先敲Tags { }再往里面填写结构体的时候先写好一对花括号再补字段能明显降低漏括号的概率。文本编辑器的括号匹配高亮功能在这个场景下几乎是必需品装一个好用的 shader 语法插件能省大量时间。5.3 类型关键字与精度fixed、half、floatCG 语法里除了 float还有 half 和 fixed 两个精度类型。这三位在 PC 平台上基本等价于 float但到了移动端就不一样了half 是 16 位fixed 是更低的定点数范围大约在 -2 到 2、精度约 1/256。历史上一段时间里社区都在劝把颜色、UV 之类的量用 fixed 来省性能但现在的移动 GPU 大多把 half 和 float 同样处理盲目降精度带来的画面问题反而比省下的性能更头疼。我自己的判断标准是顶点位置、世界坐标这类对精度敏感的量老老实实用 float法线、方向、光照结果用 half 一般够颜色插值可以用 fixed4但如果发现颜色出现色带、渐变跳变第一反应应该就是把它换成 half4 试试。这种类型选择没有绝对正确答案只能靠实测而且不同设备表现还不一样所以拿不准的时候宁可先用 float 跑对效果再针对性降精度。另外还要注意CGPROGRAM 会自动包含UnityCG.cginc里的一堆常用函数比如UnityObjectToClipPos、TRANSFORM_TEX、UnityObjectToWorldNormal这也是为什么基础 shader 里可以直接调用它们。如果你用 HLSLPROGRAM 写这些函数不会自动引入得自己手动 include 对应的库文件否则会报未定义。URP 项目里推荐统一用 HLSLPROGRAM原因是它的包含关系更透明出问题容易定位。5.4 命名与大小写最后说一个看起来不起眼但很耽误事的点大小写敏感。ShaderLab 的关键字、CG 里的语义名、内置函数名全都是区分大小写的。写SHADER不行写sv_position不行写unityobjecttoclippos也不行。Unity 的报错有时候只会告诉你某个标识符未定义不会提醒你是大小写问题新手经常在这上面绕圈。命名习惯上我的建议是属性名统一_加驼峰结构体名小写语义名全大写内置函数名严格按文档抄。抄文档这步看着笨但比记错大小写再回头查要快得多。写到后面你会发现自己常用的函数就那么十几个抄熟之后自然就记住了。真记不准的时候直接翻 Unity 手册里UnityCG.cginc那一页比在搜索引擎里翻半天靠谱。6. 我踩过的三个真实格式问题6.1 属性声明后面的分号引发的一连串报错第一次自己写带属性的 shader 时我在_Color (Color, Color) (1,1,1,1)后面顺手加了分号编译器报的不是这一行的问题而是指向了下一行说 parse error。当时我盯着下一行看了十几分钟怎么都觉得没问题。后来把分号删掉错误直接消失。这件事给我的教训是Unity 的 shader 编译器在遇到语法错误时报错行号常常会往后偏一行。所以看到某一行报错但内容看着正常时先怀疑上一行。这个经验后来帮我节省了大量时间尤其在大段属性列表里找漏写或错写的地方。6.2 LightMode 写错导致的光照缺失有一次我写一个双 Pass 的前向光照 shader主光那部分怎么调都不亮环境光倒是正常。查了很久光照计算最后发现是其中一个 Pass 的 LightMode 写成了 Forward而正确的写法应该是 ForwardBase。就一个字母的差别画面差一半。这个问题难就难在它不报错。Pass 里 LightMode 不匹配任何已知模式时这个 Pass 会在常规光照流程里被跳过但不会阻止编译。所以做多 Pass 光照的时候我现在的习惯是先把两个 Pass 写出来各自return一个纯色做验证确认两个 Pass 都被调用了再往里面填真正的光照计算。这个习惯让我避免了不少这种情况。6.3 从 CGPROGRAM 切到 HLSLPROGRAM 时忘记 include后来做 URP 项目我把一个能跑的 CGPROGRAM shader 直接改成 HLSLPROGRAM结果满屏的未定义函数错误UnityObjectToClipPos、TRANSFORM_TEX 全都不认识。原因是 HLSLPROGRAM 不会自动引入 UnityCG.cginc得手动加#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl这类包含。这个切换看着只是改个关键字实际上背后的库依赖完全变了。我的建议是项目用什么管线就从头用哪套写法别中途改。真要迁移把 include 列表先补齐再替换那些 Unity 内置函数最后再跑效果验证一步一步来比一次性全改完再调试省事得多。7. 写完格式之后该盯住的东西格式这层东西说到底只是门槛跨过去之后你会发现真正需要下功夫的是光照模型和数学。但门槛也得先跨过去不然你在编辑器里连一个能编译通过的 shader 都跑不起来后面的学习根本没法进行。我给自己的复习方式是把上面这些规则整理成一张小抄贴在显示器边上写上三个月后面写新 shader 基本不用再翻。再提一个我觉得值得养成的习惯每写一段新的 CGPROGRAM先只让顶点着色器直通、片元着色器返回一个固定颜色确认编译通过、物体能显示再往里加 UV 采样、加光照、加各种效果。格式上的问题如果混在功能开发里一起查会非常痛苦而先用最小可运行版本把骨架跑通出问题的时候你就知道答案一定在最近加的那几行里。这个排查思路在 shader 调试里几乎是通用法则我用了很多年现在还在用。关于这个系列的下一集我打算聊属性在 C# 脚本里怎么读写、material 和 sharedMaterial 到底有什么区别、以及为什么改材质会莫名多出一堆实例。这些和格式关系不大但和实际项目里用 shader 关系极深属于绕不过去的一环。等把这几块拼齐一个完整的从写 shader 到在项目里用起来的闭环才算真正合上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑