三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity的Shader文件结构:一座精心设计的“三层小楼“

Unity的Shader文件结构:一座精心设计的“三层小楼“

引子:一次让新手"眼花缭乱"的初次相遇

想象你是一个刚入门Unity的开发者。

**某天——你想学着写一个自己的Shader——**满怀期待地打开一个官方Shader示例——然后被眼前的景象"当场击晕"

Shader "Custom/MyShader" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Color", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // 一大段HLSL代码... ENDCG } } FallBack "Diffuse" }

你的脑袋一片混乱

  • **“Shader”、“Properties”、“SubShader”、“Pass”——这些嵌套是什么?
  • **“CGPROGRAM"和"ENDCG”——为什么代码要被这两个东西包起来?
  • **“Tags”、“LOD”、“FallBack”——又是些什么神秘咒语?
  • **明明是Shader——为什么看起来一半像HLSL,一半又像"配置文件"?

你合上教材——深吸一口气——心想“这……这真的能学会吗?”**

别急——**Unity的Shader文件——**看起来复杂——其实是一座精心设计的"三层小楼"——每一层都有明确的职责、每一个房间都有清晰的用途

今天,就让我们像"参观一座建筑"一样——从大门走进——从一楼看到三楼——把Unity Shader的文件结构,看得清清楚楚、明明白白

一、鸟瞰全貌:一座"三层小楼"的整体结构

**先站远一点——看看这座"小楼"的整体轮廓

Shader "路径/名字" ← 大门(Shader名字) { Properties { ... } ← 一楼(对外接口) SubShader ← 二楼(渲染实现) { Tags { ... } ← 二楼客厅(标签设置) LOD 200 ← 二楼门牌(细节等级) Pass ← 三楼(真正的Shader代码) { Tags { ... } CGPROGRAM ← 三楼的核心房间 #pragma vertex vert #pragma fragment frag // HLSL代码 ENDCG } } FallBack "Diffuse" ← 后门(备用方案) }

这座"小楼"的层次很清晰

  • 大门Shader "路径/名字"——这个Shader叫什么、放在哪儿
  • 一楼Properties——“对外接口”,让美术/程序员在Inspector里调参
  • 二楼SubShader——“渲染实现”,包含标签、LOD和Pass
  • 三楼Pass——“核心房间”,装着真正的HLSL代码
  • 后门FallBack——“备用方案”,硬件不支持时的退路

**理解了这个整体结构——再一层一层深入看细节

二、大门:Shader的"名字招牌"

**每个Shader文件的第一行——都是这样一个"招牌"

Shader "Custom/MyShader" { // ... }

这里的"Custom/MyShader"——是这个Shader的"名字和路径"

名字里的"斜杠"是什么?

Unity用斜杠/来创建"菜单层级"

  • "Custom/MyShader"在Inspector里显示为Custom菜单下的MyShader
  • "Custom/PBR/Metal"多层菜单CustomPBRMetal

这就像给文件夹分类——方便美术在Inspector里"按类别找Shader"

命名的智慧

好的Shader命名

"MyProject/Terrain/GrassWavy" // 项目-类别-具体功能 "UI/Blur/Gaussian" // UI用的高斯模糊 "Effects/Water/Ocean" // 特效-水-海洋

**清晰的层级——让美术在数百个Shader里也能一眼找到需要的那个

这个"招牌"看起来简单——却是整个Shader和外界打交道的第一步

三、一楼:Properties——“对外接口”

走进这座"小楼"——首先来到的是一楼——Properties

Properties { _MainTex ("主贴图", 2D) = "white" {} _Color ("颜色", Color) = (1,1,1,1) _Metallic ("金属度", Range(0, 1)) = 0.5 _Smoothness ("光滑度", Range(0, 1)) = 0.5 _Emission ("自发光", Color) = (0,0,0,0) }

Properties是干什么的?

**Properties块——声明Shader对外暴露的"参数接口"

它的价值:**让美术在Unity Inspector面板上——能"图形化"地调整Shader参数——而不用改代码

看看这段代码在Unity里长什么样

Inspector面板显示: ┌─────────────────────────┐ │ 主贴图 [图片选择器] │ │ 颜色 [色板] │ │ 金属度 [滑块 0-1] │ │ 光滑度 [滑块 0-1] │ │ 自发光 [色板] │ └─────────────────────────┘

**美术不用碰代码——拖拖拽拽就能调整Shader效果——这是Unity的一大魅力

Properties的语法结构

每一行Properties的格式是

_属性名 ("显示名字", 类型) = 默认值

举例说明

_Color ("颜色", Color) = (1,1,1,1)
  • _Color——内部变量名(HLSL代码里用它)
  • "颜色"——Inspector里显示的名字
  • Color——参数类型(这里是颜色)
  • (1,1,1,1)——默认值(RGBA全1,即白色)

支持的参数类型

Unity Shader支持多种参数类型

// 数值类型 _Value ("数值", Float) = 1.0 _IntValue ("整数", Int) = 5 _Range ("范围", Range(0, 10)) = 5 // 向量与颜色 _Color ("颜色", Color) = (1,1,1,1) _Vector ("向量", Vector) = (0,0,0,0) // 贴图类型 _MainTex ("主贴图", 2D) = "white" {} _CubeMap ("立方体贴图", Cube) = "gray" {} _3DTex ("3D贴图", 3D) = "" {}

**每种类型——在Inspector里显示为不同的控件——滑块、色板、图片选择器等等

**Properties这一楼——是Shader和"外部世界"之间的桥梁——是艺术与技术相遇的地方

四、二楼:SubShader——“渲染实现”

**上到二楼——SubShader——Shader真正的"实现层"

SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry" } LOD 200 Pass { // ... } Pass { // ... } }

为什么叫"SubShader"?

**注意——一个Shader文件里可以有多个SubShader

Shader "MyShader" { Properties { ... } SubShader // 高端硬件用这个 { // 复杂高质量的实现 } SubShader // 中端硬件用这个 { // 简化实现 } SubShader // 低端硬件用这个 { // 最简实现 } }

**Unity启动时——从上到下找"当前硬件支持的第一个SubShader"来执行——这是"跨平台兼容"的设计

**在移动端跑不动的高级效果——可以降级到简单版本——保证运行

SubShader里的关键元素

SubShader里,有几个重要元素

元素1:Tags(标签)

Tags { "RenderType"="Opaque" // 渲染类型:不透明 "Queue"="Geometry" // 渲染队列:几何体 "LightMode"="ForwardBase" // 光照模式:前向渲染 }

Tags告诉Unity渲染系统

  • “这个Shader是渲染不透明物体的还是透明物体的?”
  • “它属于哪个渲染队列?”(不透明→透明→UI……)
  • “它用于哪种光照模式?”(前向、延迟、阴影……)

Tags是Shader和Unity渲染管线的"通信标签"

元素2:LOD(Level of Detail)

LOD 200

LOD是一个"复杂度评分"——数值越大,代表Shader越复杂

  • LOD 100最简单
  • LOD 200中等
  • LOD 300复杂
  • LOD 500最复杂

用途:**Unity可以设置"全局LOD阈值"——只用不超过该阈值的Shader——用于低端设备强制降级

元素3:Pass(真正的Shader代码)

这是SubShader里最核心的部分——留到下一节详细说

一个SubShader可以有多个Pass

SubShader { Pass { // 第1个Pass:绘制基础颜色 } Pass { // 第2个Pass:绘制描边 } Pass { // 第3个Pass:绘制阴影 } }

每个Pass就是一次"完整的渲染流程"——多Pass可以实现复杂效果(如描边、多次光照、后处理)**。

每一次渲染同一个物体——都会依次执行所有的Pass——层层叠加出最终效果

五、三楼:Pass——“真正的Shader代码”

**上到三楼——**是Pass块——这是Shader真正干活的地方

Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _Color; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * _Color; return col; } ENDCG }

CGPROGRAM和ENDCG是什么?

注意这一对特殊的标记

CGPROGRAM // ... HLSL代码 ... ENDCG

它们的作用“这中间是真正的Shader代码——用HLSL/Cg语言写”

为什么要这个"围栏"?——因为Unity Shader文件是"混合的"

  • 外面那些PropertiesTagsLOD是Unity自己定义的ShaderLab语法
  • 里面这些CGPROGRAMENDCG之间)是HLSL/Cg语法

CGPROGRAMENDCG把它们分开——让编译器知道"这里开始用HLSL编译"

在URP/HDRP中——通常用HLSLPROGRAMENDHLSL——功能相同——只是明确用HLSL而非Cg

HLSLPROGRAM // ... HLSL代码 ... ENDHLSL

#pragma指令:告诉编译器"入口函数"

#pragma vertex vert #pragma fragment frag

**这两行——告诉编译器

  • 顶点着色器的入口函数叫vert
  • 片段着色器的入口函数叫frag

**编译器根据这个指令——**从代码里找到对应的函数——分别编译成顶点Shader和片段Shader

函数名可以随便起

#pragma vertex MyVertex #pragma fragment MyFragment MyVertex_Output MyVertex(MyVertex_Input i) { ... } half4 MyFragment(MyVertex_Output i) : SV_Target { ... }

只要名字对得上就行

#include:引入公共代码

#include "UnityCG.cginc"

这行的意思——“把Unity提供的公共Shader代码包含进来”**:

  • UnityCG.cginc——基础工具函数库
  • Lighting.cginc——光照相关
  • AutoLight.cginc——阴影相关

里面提供了大量常用函数——**比如UnityObjectToClipPos(把顶点从模型空间变到裁剪空间)——不用自己实现

**这就像C++的#include——站在别人的肩膀上写代码

Pass的Tags

Pass自己也可以有Tags

Pass { Tags { "LightMode"="ForwardBase" } // ... }

**Pass的Tags——告诉Unity"这个Pass是用于什么渲染阶段"

  • ForwardBase——前向渲染的基础Pass
  • ForwardAdd——前向渲染的追加光源Pass
  • ShadowCaster——投射阴影用的Pass
  • DepthOnly——只写深度的Pass

**Unity根据这些标签——在合适的时机调用对应的Pass

一个Pass里的"典型结构"

**一个完整的Pass——通常包含这些部分

Pass { Tags { ... } ← Pass自己的标签 // 渲染状态设置(可选) Cull Back ← 剔除背面 ZWrite On ← 开启深度写入 Blend SrcAlpha OneMinusSrcAlpha ← 混合模式 CGPROGRAM // 指令 #pragma vertex vert #pragma fragment frag // 包含文件 #include "UnityCG.cginc" // 输入输出结构体 struct appdata { ... } struct v2f { ... } // Properties里声明过的变量,这里也要声明一次 sampler2D _MainTex; float4 _Color; // 顶点着色器 v2f vert (appdata v) { ... } // 片段着色器 fixed4 frag (v2f i) : SV_Target { ... } ENDCG }

**每一部分——都有清晰的职责

六、后门:FallBack——“备用方案”

**在Shader文件的最后——通常有这么一行

FallBack "Diffuse"

这就是Shader的"后门"——**当所有SubShader都不被硬件支持时——回退到指定的Shader

"Diffuse"是Unity内置的一个基础Shader——几乎所有硬件都支持——保证"至少能渲染出个东西"

FallBack的价值

  • 保证跨平台兼容性
  • 让Shader在极端老旧设备上也不会"完全崩掉"
  • 提供一个"安全网"

也可以选择不要FallBack——但那需要开发者自己保证Shader的兼容性

七、完整地"参观"一遍这座"三层小楼"

**现在——让我们完整地"从头到尾"看一遍Unity Shader的结构

Shader "MyProject/BasicLit" // 大门:Shader名字 { Properties // 一楼:对外接口 { _MainTex ("主贴图", 2D) = "white" {} _Color ("主颜色", Color) = (1,1,1,1) _Metallic ("金属度", Range(0,1)) = 0.5 } SubShader // 二楼:渲染实现(高端) { Tags { "RenderType"="Opaque" "Queue"="Geometry" } LOD 300 Pass // 三楼:绘制主颜色 { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" // ... 具体代码 ... ENDCG } Pass // 三楼:绘制阴影 { Tags { "LightMode"="ShadowCaster" } CGPROGRAM // ... 阴影代码 ... ENDCG } } SubShader // 二楼:渲染实现(低端) { // 简化版实现 } FallBack "Diffuse" // 后门:备用方案 }

这就是Unity Shader的完整结构——清晰、层次分明、每一层都有明确职责

八、Unity Shader结构的哲学思考

**从这个结构中——能提炼出几条深刻的设计哲学

哲学1:分层解耦

**Unity Shader的每一层——都有清晰的职责

  • **Properties——对外接口
  • **SubShader——平台适配
  • **Pass——具体渲染

改一层不影响另一层——Properties改了,Pass不受影响;Pass改了,SubShader结构不变

这是软件工程"关注点分离"的经典体现

哲学2:跨平台兼容

**多SubShader机制——是Unity跨平台野心的体现

  • 同一个Shader——在高端PC上跑复杂版本
  • 在低端手机上跑简化版本
  • 全靠SubShader的分层匹配

**这种"一个文件,多种适配"的设计——让Unity Shader真正"跨平台一次编写、多处运行"

哲学3:艺术与技术的桥梁

**Properties的存在——是Unity对"美术友好"的深度体现

  • 程序员写Shader逻辑
  • 美术在Inspector里调参数
  • 各司其职,配合无间

**没有Properties——每次调整都要改代码——美术无法参与——协作效率低下

Properties是"程序美术协作"最优雅的桥梁

哲学4:混合语言的智慧

Unity Shader是"两种语言的混合体"

  • 外部——ShaderLab(Unity自定义的配置语法)
  • 内部——HLSL/Cg(真正的着色器语言)

用ShaderLab配置"外部结构"——用HLSL写"核心算法"——两种语言各司其职、优雅配合

**这种混合设计——是Unity对Shader文件"既要灵活配置又要强大编程"的完美回答

结语:一座"三层小楼"的建筑之美

从"眼花缭乱的初次相遇",
到"三层小楼"的整体鸟瞰,
到从大门到后门的完整"参观",
到每一层的详细拆解——

**Unity的Shader文件结构——是这样一座精心设计的"三层小楼"

  • 大门是Shader "名字"——告诉世界这座楼叫什么、在哪儿
  • 一楼是Properties——对外接口,让美术拖拖拽拽调参数
  • 二楼是SubShader——渲染实现,用Tags和LOD做平台适配
  • 三楼是Pass——核心房间,装着真正的HLSL代码
  • 后门是FallBack——备用方案,硬件不支持时的退路

它像一座建筑师精心设计的小楼

  • 每一层都有清晰的职责
  • 每一个房间都有明确的用途
  • 每一次参观都能让人"哦,原来如此"

它有Properties的"美术友好"——让艺术家能优雅地参与Shader开发
它有SubShader的"跨平台智慧"——让一个Shader文件适配所有硬件
它有Pass的"灵活组合"——让复杂效果由多次绘制层层叠加
它有FallBack的"安全兜底"——让老旧设备也能勉强运行
它有ShaderLab+HLSL的"混合美学"——让配置和代码各得其所

下次当你打开一个Unity Shader文件——请记得

这不是杂乱无章的代码堆积——
这是一座三层小楼的建筑蓝图——
每一层都是设计者精心思考的成果——
每一个块都是软件工程哲学的完美体现——

Properties的第一行——FallBack的最后一句——都是Unity对"如何优雅组织Shader"的深度回答——都是"艺术、技术、平台、性能"完美平衡的智慧结晶——

**才让你今天能这样自然地——写下一段Shader、看着Inspector里的滑块、拖着贴图到那个方框、然后看到3D世界里的物体绽放出美丽的光影

这就是Unity的Shader文件结构——一座精心设计的"三层小楼"——是每一位Unity开发者,都值得细细品味、深入理解的建筑之美

它不炫技——但它每一层都恰到好处
它不复杂——但它蕴含着深厚的工程智慧
它不完美——但它是这个时代最优雅的游戏Shader组织方式之一**。

在这座"三层小楼"的每一个角落

  • 是分层设计的清晰
  • 是关注点分离的智慧
  • 是跨平台兼容的野心
  • 是美术友好协作的温度
  • 是混合语言组合的灵活

这——就是Unity Shader文件结构真正的伟大——不只是一堆嵌套的花括号——而是一门语言对"如何优雅表达Shader"这个问题的完整回答**——是每一位Unity开发者,都应该带着敬意,细细"参观"的建筑杰作

每一个Shader——都是这座小楼的招牌
每一个Properties——都是这座小楼的接待厅
每一个SubShader——都是这座小楼的功能层
每一个Pass——都是这座小楼的核心房间
每一个FallBack——都是这座小楼的安全后门

这——就是Unity送给每一位Shader开发者的、精美绝伦的"建筑礼物"。 🏛️✨🎨

← 返回列表