
1. 项目概述为什么Unity Shader是游戏开发的“魔法画笔”如果你刚接触Unity可能会觉得Shader是个神秘又复杂的东西它不像C#脚本那样直观动动鼠标就能看到效果。但我想告诉你Shader恰恰是决定你游戏画面最终呈现效果的“魔法画笔”。一个简单的模型配上不同的Shader可以变成锈迹斑斑的废铁也可以变成流光溢彩的水晶。我们常说的“画面风格”其底层技术支撑很大程度上就来自于Shader。很多新手开发者包括几年前的我自己都曾对Shader望而却步觉得那是图形程序员的专属领域。但事实上现代游戏开发中无论是实现一个简单的卡通描边还是制作一个复杂的海浪效果Shader知识都不可或缺。它不再是可选项而是提升作品表现力、实现独特视觉语言的必修课。本章“Unity Shader基础”就是为你打开这扇魔法大门的钥匙。它不会一上来就扔给你一堆晦涩的数学公式和图形API而是从Unity引擎的视角告诉你Shader在Unity中是如何被组织、管理和使用的。这就像学做菜我们先不深究每种调料背后的化学原理而是先认识厨房里有哪些锅碗瓢盆以及基本的操作流程。理解了这些基础你才能知道在Unity里写一个Shader效果从哪里开始文件放哪里怎么挂到物体上以及为什么有时候你的Shader代码明明看起来没错却怎么也不起作用。这些看似琐碎的基础恰恰是后续深入学习、避免踩坑的坚实保障。无论你的目标是成为技术美术还是想自己实现一些酷炫的视觉效果这一章的内容都是你无法绕过的起点。2. Unity Shader的整体设计与架构思路2.1 理解Shader在Unity中的角色定位在纯粹的图形编程中Shader是一段运行在GPU上的小程序它严格遵循着渲染管线处理顶点、片元等数据。但Unity作为一个游戏引擎它在这层“硬核”的图形接口之上构建了一层自己的抽象和管理体系。你可以把Unity Shader理解为一个“包装盒”这个盒子里不仅包含了真正的GPU着色器代码比如HLSL、GLSL还包含了Unity引擎需要的一系列元数据如何接收和处理来自材质球Material的属性、如何与光照系统交互、如何在不同平台如PC、移动端上自动适配等。这种设计带来了巨大的便利性。作为开发者你不需要为每个图形API如DirectX、OpenGL ES单独写一份Shader代码。Unity的ShaderLab语言我们后面会详细讲就像一份“说明书”Unity引擎会根据目标平台将这份“说明书”编译成对应的底层代码。同时它也将Shader与材质Material系统紧密绑定。材质球是Shader的实例化你可以创建无数个材质球它们都使用同一个Shader但通过调整材质球上的参数如颜色、纹理得到千变万化的表现。这种“Shader定义能力材质赋予参数”的分离是Unity工作流的核心。2.2 Unity Shader的三种主要形态Surface Shader, Vertex/Fragment Shader, Fixed Function ShaderUnity主要支持三种编写Shader的方式它们面向不同需求和复杂度的开发者。Surface Shader这是Unity提供的一种高级抽象是新手入门和快速实现标准光照效果的最佳选择。你不需要自己处理复杂的光照计算如漫反射、高光、阴影只需要告诉Unity“我的表面是什么样子的”比如反照率颜色、法线、光滑度Unity会自动帮你生成处理光照的顶点和片元着色器代码。它的代码结构通常包裹在CGPROGRAM和ENDCG块中并使用#pragma surface surf Standard这样的编译指令。对于大多数需要与场景灯光交互的物体如角色、场景物件Surface Shader能极大提升开发效率。Vertex/Fragment Shader也称为“可编程着色器”或“Unlit Shader”虽然不绝对。这是更底层、更灵活的方式。你需要完全自己编写顶点着色器处理模型顶点位置、法线等和片元着色器计算每个像素的最终颜色。它不包含任何内置的光照模型所有效果都需要你手动计算实现。这给了你最大的控制权常用于实现屏幕后处理特效、UI特效、自定义的非真实感渲染如卡通着色等。它的代码同样写在CGPROGRAM和ENDCG块中但使用#pragma vertex vert和#pragma fragment frag指令。Fixed Function Shader固定函数着色器这是非常古老的、基于渲染状态配置的写法使用SetTexture、Lighting On等命令。它功能有限且在现代GPU上可能效率不高主要用于支持非常老的设备或实现极其简单的效果。在新项目中基本不推荐使用了解即可。选择哪种形态我的经验是优先使用Surface Shader实现需要标准光照的物体当需要完全自定义、或不需要光照时如特效、UI使用Vertex/Fragment Shader。Fixed Function Shader除非有明确的兼容性需求否则可以忽略。2.3 ShaderLabUnity Shader的“容器”语言无论你内部使用上述哪种方式编写核心着色代码最外层的“包装盒”都是由ShaderLab语言定义的。一个Unity Shader文件.shader本质上就是一个ShaderLab脚本。它的基本结构如下Shader “Custom/MyShader” // 定义Shader在材质下拉菜单中的路径和名字 { Properties // 属性块定义暴露给材质球 Inspector 面板的参数 { _MainTex (“Texture”, 2D) “white” {} _Color (“Color”, Color) (1,1,1,1) _Glossiness (“Smoothness”, Range(0,1)) 0.5 } SubShader // 子着色器块可以定义多个用于适配不同硬件或渲染路径 { Tags { “RenderType”“Opaque” } // 标签告诉Unity如何渲染该物体 LOD 100 // 细节级别根据距离等因素选择不同的SubShader CGPROGRAM // 开始CG/HLSL代码块 // 编译指令和核心着色器代码写在这里 #pragma surface surf Standard fullforwardshadows // ... 其他代码 ENDCG // 结束CG/HLSL代码块 } FallBack “Diffuse” // 备选Shader如果所有SubShader都不支持则使用这个 }注意Properties块中定义的变量名如_MainTex必须与后面CG代码中声明的变量名完全一致包括下划线。这是连接材质球参数与Shader内部逻辑的桥梁如果名字对不上参数将无法传入。理解这个结构至关重要。Properties是你的Shader对外的“控制面板”SubShader是核心执行体Tags和LOD是调度策略FallBack是保底方案。一个设计良好的Shader必须清晰地区分这些部分。3. 核心细节解析Properties、SubShader与Pass3.1 Properties属性块Shader的对外接口Properties块是Shader与美术师、策划甚至其他脚本沟通的窗口。这里定义的每一个属性都会出现在材质球的Inspector面板上允许非程序员进行调节。Unity支持丰富的属性类型数字类Range(min, max)滑动条如_Metallic (“Metallic”, Range(0,1)) 0.0。Float浮点数输入框。Int整数输入框。颜色与向量Color颜色选择器附带RGBA四个浮点数。Vector四分量向量输入框。纹理类2D2D纹理最常用。Cube立方体贴图用于天空盒或反射。3D3D纹理。2DArray2D纹理数组。其他默认值每个属性声明后都有一个默认值如 “white” {}或 (1,1,1,1)。这个值决定了新建材质球时该属性的初始状态。实操心得给属性起一个清晰易懂的显示名括号内的第一个字符串如“Albedo (RGB)”非常重要这能极大提升团队协作效率。同时合理的默认值可以减少每次创建材质时的重复设置工作。3.2 SubShader与Pass渲染指令的层层封装这是Unity Shader中概念最易混淆但也最关键的部分。SubShader你可以把它理解为一个“渲染方案”。一个Shader可以包含多个SubShader。Unity在渲染时会从上到下检查每个SubShader选择第一个能被当前图形硬件支持的来执行。这常用于为不同性能级别的设备提供不同复杂度的Shader版本。例如第一个SubShader使用复杂的光照模型和实时阴影针对高端PC第二个SubShader使用简化的光照模型针对移动端。PassPass是SubShader内的基本执行单元。一次Pass基本上对应一次绘制调用Draw Call。一个SubShader可以包含多个Pass。每个Pass都会完整地执行一遍顶点和片元着色器或表面着色器将物体渲染一次到屏幕上。多个Pass通常用于实现需要叠加多次渲染的效果比如第一个Pass渲染物体的漫反射颜色。第二个Pass使用混合Blending模式在同一个物体上叠加一层边缘光。 这就是为什么一个复杂的特效Shader可能会导致Draw Call翻倍的原因——因为它包含了多个Pass。Tags标签是附加在SubShader或Pass上的键值对用于向Unity的渲染引擎传达“如何渲染我”的元信息。常见的SubShader级标签有“RenderType”“Opaque”告诉Unity这是一个不透明物体用于相机深度纹理、屏幕后处理等系统。“Queue”“Geometry”定义渲染队列决定物体被渲染的先后顺序例如“Transparent”队列的物体会在“Geometry”之后渲染以实现正确的透明混合。LOD (Level of Detail)细节级别。你可以为每个SubShader设置一个LOD值。在运行时可以通过Shader.globalMaximumLOD或材质的material.shader.maximumLOD来设置一个阈值。Unity会选择LOD值小于等于该阈值的、且能被硬件支持的SubShader。这常用于根据机器性能动态切换Shader质量。3.3 FallBack与自定义渲染路径FallBack在Shader文件的最后可以指定一个FallBack。如果当前硬件不支持该Shader中的所有SubShaderUnity就会尝试使用这个备用的Shader。通常我们会回退到内置的“Diffuse”、“Specular”或“VertexLit”。这是一个重要的兼容性保障确保你的游戏在最差的硬件上至少能显示点东西而不是一片粉红Missing Shader的错误颜色。渲染路径Rendering Path这不是在Shader中直接定义的但Shader需要声明它支持哪些渲染路径。Unity主要有前向渲染路径Forward和延迟渲染路径Deferred。在Surface Shader的编译指令中你可以通过#pragma surface surf Standard addshadow这样的指令来声明。对于Vertex/Fragment Shader你需要在Pass中编写适配不同渲染路径的代码或者使用LightMode标签来指定Pass的用途如Tags { “LightMode” “ForwardBase” }。理解你项目所使用的渲染路径并编写对应的Shader是保证光照和阴影正确性的前提。4. 实操过程创建你的第一个Unity Shader4.1 从零开始创建一个Surface Shader理论说了这么多我们动手创建一个。在Unity项目窗口中右键 - Create - Shader - Standard Surface Shader。Unity会为你生成一个模板文件。我们将其重命名为MyFirstSurfaceShader.shader并打开。你会看到一个结构清晰的ShaderLab文件。我们来做几个简单的修改理解其运作机制修改Shader菜单路径将第一行Shader “Custom/MyFirstSurfaceShader”改为Shader “Tutorial/BasicSurface”。保存后在材质球的Shader下拉菜单中你就能在Tutorial类别下找到它。添加自定义属性在Properties块里我们添加一个控制高光强度的属性Properties { _Color (“Color”, Color) (1,1,1,1) _MainTex (“Albedo (RGB)”, 2D) “white” {} _Glossiness (“Smoothness”, Range(0,1)) 0.5 _Metallic (“Metallic”, Range(0,1)) 0.0 // 添加的自定义属性 _MySpecularPower (“Specular Power”, Range(0.1, 10)) 1.0 }在CG代码中使用属性在CGPROGRAM内部你需要声明与Properties中同名的变量sampler2D _MainTex; fixed4 _Color; half _Glossiness; half _Metallic; // 声明自定义属性变量 float _MySpecularPower;修改表面函数找到void surf (Input IN, inout SurfaceOutputStandard o)函数。这个函数就是Surface Shader的核心它计算表面的各种属性并输出。我们暂时不做复杂修改但可以尝试一个有趣的效果让高光度Smoothness随着模型Y轴坐标变化。在surf函数内部添加// 原始代码o.Smoothness _Glossiness; // 修改为 o.Smoothness _Glossiness * (IN.worldPos.y * 0.1 0.5); // 假设世界坐标Y轴影响光滑度IN.worldPos是Surface Shader输入结构体Input中可用的世界空间坐标。通过这样的修改物体在世界中越高的部分会显得越光滑。保存Shader文件在Unity中创建一个材质球并使用这个Shader将其拖到一个物体上比如一个Cube。调整材质球上的Glossiness滑块你会发现物体的反光强度会随着其世界Y坐标变化底部的反光弱顶部的反光强。4.2 创建一个简单的Vertex/Fragment Shader无光照现在我们来创建一个完全不依赖Unity光照系统的、自己控制颜色的Shader。右键创建 Shader - Unlit Shader命名为MyUnlitColor.shader。打开后你会看到这是一个标准的Vertex/Fragment Shader模板。它的结构更“原始”顶点着色器输入结构体appdata_base或appdata_tan等包含了顶点位置、法线、纹理坐标等模型数据。顶点到片元的数据传递结构体通常命名为v2f(vertex to fragment)用于将顶点着色器计算出的数据如裁剪空间位置、纹理坐标、颜色等传递给片元着色器。顶点着色器函数(vert)主要任务是将顶点从模型空间变换到裁剪空间通过UnityObjectToClipPos(v.vertex)并填充v2f结构体。片元着色器函数(frag)接收v2f结构体计算并返回该像素的最终颜色。我们来做一个渐变色的效果。修改frag函数fixed4 frag (v2f i) : SV_Target { // 原始fixed4 col tex2D(_MainTex, i.uv); // 修改使用模型空间法线的Y分量来混合两种颜色 float factor i.normal.y * 0.5 0.5; // 将法线Y从[-1,1]映射到[0,1] fixed4 colorA fixed4(1, 0.2, 0.2, 1); // 红色系 fixed4 colorB fixed4(0.2, 0.2, 1, 1); // 蓝色系 fixed4 col lerp(colorA, colorB, factor); // 线性插值 return col; }为了让i.normal有值我们需要修改v2f结构体和vert函数将法线从模型空间变换到世界空间并传递struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD0; // 使用一个纹理坐标插值器来传递世界法线 }; v2f vert (appdata_base v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.worldNormal UnityObjectToWorldNormal(v.normal); // 变换法线到世界空间 return o; }保存并应用这个Shader到一个球体上你会看到球体顶部偏蓝底部偏红侧面是渐变的紫色。这就是一个完全由你自己定义的、无光照的颜色效果。4.3 在材质球中调整参数并观察效果创建两个材质球分别使用我们刚刚制作的Tutorial/BasicSurface和MyUnlitColorShader。将它们分别赋予场景中的不同物体。对于Surface Shader材质球尝试拖动Color、Smoothness、Metallic以及我们自定义的Specular Power虽然代码里还没用上滑块观察物体材质的变化。移动物体的位置改变Y坐标观察高光区域的变化。对于Unlit Shader材质球由于我们没有暴露属性到Properties所以材质球面板是空的。但你可以通过修改Shader代码中的colorA和colorB来改变渐变色的起止颜色。这个练习的关键在于理解Shader代码定义了效果的算法和可能性而材质球Material则是这个算法的具体参数实例。同一个Shader可以创建出无数个不同表现的材质。5. 常见问题与排查技巧实录5.1 Shader编译错误与粉红材质Pink Shader这是新手最常遇到的问题。粉红色意味着Unity无法编译或找到对应的Shader。编译错误最常见。查看Unity编辑器底部的Console窗口会有红色的错误信息。错误通常来自语法错误缺少分号、括号不匹配、关键字拼写错误。仔细检查错误信息指向的行号。未声明变量在CG代码中使用了未在Properties块声明或未在CG中声明的变量。确保变量名完全一致。语义绑定错误顶点着色器输出结构体v2f中的变量没有正确绑定语义如: SV_POSITION,: TEXCOORD0。排查技巧双击Console中的错误信息Unity通常会尝试跳转到出错行。养成写一段代码就回到Unity窗口让其自动编译一下的习惯可以及时发现错误。Shader丢失或FallBack失败如果Shader文件被删除、移动或者当前图形硬件不支持所有SubShader且FallBack也失败也会显示粉色。检查Shader文件是否在项目中并确保有一个可靠的FallBack。5.2 属性Properties在材质球上不显示或修改无效不显示检查Properties块内的语法是否正确特别是显示名和变量名是否用引号和括号正确分隔。例如_MyColor (“My Color”, Color) (1,1,1,1)。修改无效这是最典型的问题几乎100%是因为变量名不匹配。检查拼写Properties中的_MyValue和CG代码中声明的_MyValue必须一字不差包括大小写。一个常见的错误是在CG中声明为float _myValue;小写m而Properties中是_MyValue。检查类型Properties中定义为Range(0,1)在CG中应声明为float、half或fixed。定义为2D的纹理在CG中应声明为sampler2D。检查作用域在CGPROGRAM中声明的变量必须是全局的在函数体外并且与Properties中的名字对应。5.3 渲染效果异常黑屏、过亮、透明排序错误物体完全黑色或不渲染检查渲染队列Queue如果物体是透明的但使用了“Queue”“Geometry”不透明队列它可能不会被正确渲染。透明物体应使用“Queue”“Transparent”。检查深度写入ZWrite和深度测试ZTest在Pass中ZWrite Off和ZTest Always等指令会影响物体之间的遮挡关系。对于大多数不透明物体保持ZWrite On和ZTest LEqual默认即可。检查片元着色器返回值确保你的frag函数或surf函数输出的颜色值是正确的。一个常见的错误是计算出的颜色值所有通道都是0或负数。透明物体排序错乱透明渲染依赖于渲染顺序而顺序由“Queue”标签和物体到相机的距离共同决定。但同一个队列内的透明物体如果它们相互重叠顺序可能不可预测。解决方案尽量将透明物体拆分成不重叠的部分。使用两个Pass渲染第一个Pass只写入深度ColorMask 0且ZWrite On第二个Pass进行透明渲染。这可以解决一部分内部穿透问题。对于复杂的透明效果如毛玻璃可能需要使用屏幕空间技术这超出了基础范畴。效果在移动设备上不显示或性能极差检查Shader Target在CGPROGRAM开头使用#pragma target 3.0或更高版本可能某些移动GPU不支持。对于简单的移动端Shader可以尝试#pragma target 2.0。检查计算复杂度移动端GPU对sin,cos,pow, 循环、分支等操作非常敏感。尽量避免在片元着色器中进行大量复杂计算。将计算上移到顶点着色器或者使用查找纹理Lookup Texture来替代实时计算。使用半精度变量在移动端尽量使用half16位浮点来代替float32位浮点存储颜色、纹理坐标等数据使用fixed更低精度来处理颜色计算。这能显著提升性能。5.4 性能分析与优化入门写Shader不能只追求效果酷炫还要考虑性能。这里有几个基础的优化思路减少纹理采样纹理采样tex2D是GPU上的昂贵操作。尽可能复用采样结果或者将多个贴图如反照率、金属度、粗糙度打包到一张纹理的不同通道例如RGB通道存储反照率A通道存储粗糙度。简化数学运算用mad(乘加) 指令替代单独的乘法和加法用rsqrt替代1.0 / sqrt。虽然现代着色器编译器会做很多优化但保持简洁的数学表达式总是好的。警惕discard操作在片元着色器中使用clip()或discard指令会打断GPU的早期深度测试和像素着色器的并行性可能严重影响性能尤其是在移动端。谨慎使用。利用LOD和多重SubShader为你的Shader编写多个SubShader设置不同的LOD值。在低端设备上自动切换到更简单的版本例如关闭实时阴影、简化光照计算。使用Shader Variant StrippingUnity在打包时会尝试移除用不到的Shader变体。但如果你在代码中使用Material.EnableKeyword来动态启用功能可能会导致变体数量爆炸。合理使用#pragma shader_feature和#pragma multi_compile并注意控制变体的组合数量。6. 从基础到进阶Shader Graph与URP/HDRP掌握了ShaderLab和基本的CG/HLSL编写后你的Shader学习之路才刚刚开始。现代Unity开发中有两个重要的方向需要了解。6.1 Shader Graph可视化Shader创作工具对于不擅长代码的美术师或想快速原型化的开发者Unity的Shader Graph是一个革命性的工具。它允许你通过拖拽节点、连接线的方式来构建Shader逻辑完全可视化。优势直观、快速迭代、易于理解数据流向、内置大量常用节点数学运算、纹理采样、UV处理等。与代码Shader的关系Shader Graph最终也会生成一个.shader文件虽然你看不到。它并没有取代编程而是提供了一种新的创作方式。理解本章的基础概念Properties、SubShader、Pass、渲染流程对于高效使用Shader Graph同样至关重要因为你需要知道每个节点在图形管线中扮演的角色。适用场景表面着色、简单的顶点动画、基于纹理和参数的混合效果等。对于极度定制化、需要复杂算法或屏幕空间操作的效果代码Shader仍然不可替代。6.2 可编程渲染管线SRPURP与HDRP从Unity 2018开始Unity引入了可编程渲染管线Scriptable Render Pipeline, SRP的概念并推出了两个预置的管线通用渲染管线Universal Render Pipeline, URP和高清渲染管线High Definition Render Pipeline, HDRP。URP轻量、高效专为移动端、VR和低端PC平台设计。它提供了一套简化的、但功能齐全的渲染特性。URP有自己的一套Shader编写框架。如果你新建一个URP项目之前写的内置管线Built-inSurface Shader可能无法直接使用。URP提供了它自己的Lit/Unlit Shader模板和一套新的函数库。学习URP Shader需要理解其新的光照模型、Lighting.hlsl包含文件以及UniversalForwardPass的写法。HDRP面向高端PC和主机平台提供电影级质量的渲染效果功能极其复杂和强大。HDRP的Shader系统也更加复杂。核心建议在学习完本章的内置管线Shader基础后根据你的目标平台选择深入学习URP或HDRP的Shader编写。它们的基本原理顶点变换、纹理采样、颜色混合是相通的但API和框架有所不同。Unity官方文档和示例项目是学习这些新管线的最佳起点。7. 调试与学习资源推荐7.1 调试ShaderFrame Debugger与RenderDoc当Shader效果不符合预期时光靠猜是不行的必须借助工具。Unity Frame DebuggerWindow - Analysis - Frame Debugger。这是一个神器它可以让你“暂停”游戏的一帧并一步步查看Unity是如何发出每一个绘制指令Draw Call的。你可以看到每个Draw Call所使用的Shader、材质参数、渲染状态等。这对于理解渲染顺序、排查合批问题、确认Shader是否正确应用至关重要。RenderDoc一个强大的第三方图形调试器。它可以捕获一帧完整的GPU调用序列并允许你深入查看任何一个Draw Call的输入顶点数据、纹理、常量缓冲区和输出渲染目标、深度模板缓冲。当你的Shader出现极其诡异的视觉错误而Frame Debugger无法解决时RenderDoc是终极武器。你可以看到每一个像素在片元着色器中计算出的中间值精准定位问题所在。7.2 持续学习路径与资源Shader学习是一个漫长的过程需要理论图形学数学和实践不断写代码、看效果相结合。图形学数学基础向量、矩阵、点乘、叉乘、线性插值、坐标系变换模型、世界、视图、裁剪、屏幕空间。这是理解所有Shader操作的基石。不需要成为数学家但必须熟悉这些概念。必读资料Unity官方手册关于ShaderLab、Surface Shader、内置着色器变量/函数的文档是第一手资料。《Unity Shader入门精要》这本书非常适合中文开发者由浅入深理论与实践结合得很好是本章标题的出处也是系统学习的优秀教材。Catlike Coding Rendering Tutorials一个免费的、质量极高的系列教程从最基础的渲染一个三角形开始逐步构建一个完整的渲染器能让你对渲染管线的理解达到新的高度。社区与灵感ShaderToy一个网站上面有无数令人惊叹的片段着色器Fragment Shader作品。你可以查看代码、实时修改并看到效果。这是学习高级图形技巧和寻找灵感的宝库。注意ShaderToy的代码需要稍作修改才能移植到Unity中主要是输入输出和坐标系差异。Unity Asset Store有很多优秀的付费和免费Shader资源。下载下来反编译对于非加密资源或直接研究其代码和节点图是学习他人思路的绝佳方式。最后我的个人体会是Shader学习最大的障碍是开始的畏惧心理。不要试图一次性理解所有东西。从一个最简单的、能改变颜色的Shader开始然后尝试让它动起来比如用时间_Time变量再尝试混合一张纹理一步一步来。每当你实现一个小效果都会获得巨大的成就感并驱动你向下一个目标前进。图形编程的世界充满魅力而Unity Shader是你踏入这个世界最友好的入口之一。