
1. 从“黑盒”到“可控”为什么你需要理解Unity Shader刚接触Unity那会儿Shader对我来说就是个“魔法黑盒”。拖一个Standard材质球到模型上调整一下颜色和光滑度效果就出来了。直到有一天美术同学跑过来问我“能不能给这个武器做个边缘发光的溶解效果就是角色死亡时武器从边缘开始发光、破碎、消失的那种。”我第一反应是去Asset Store找插件但要么效果不完全匹配要么性能开销太大。那一刻我意识到如果不懂Shader你永远只能做“资源的搬运工”一旦遇到定制化需求就只能抓瞎。Shader这个运行在GPU上的小程序是决定屏幕上每一个像素最终颜色的核心规则。它远不止是让模型“有颜色”那么简单。从《原神》里随风摇曳的草地和波光粼粼的水面到《黑神话悟空》中充满质感的神话生物皮肤和武器特效其视觉表现力的基石正是Shader。很多人觉得Shader高深莫测是图形程序员的专属领域。但事实上对于Unity开发者——无论是程序、TA技术美术还是甚至是有追求的美术——掌握Shader的基础知识和实现思路是从“功能实现者”迈向“效果创造者”的关键一步。学习Shader不是为了让你从头到尾手写复杂的渲染方程而是让你获得一种“可控性”。当UI需要一种特殊的模糊背景时当角色需要根据血量变化皮肤颜色时当场景需要一种非真实感渲染NPR的水墨风格时你能清晰地知道该从哪个方向入手是修改表面着色器还是写一个全屏后处理效果或是利用Shader Graph进行可视化搭建。这份“入门精要总结”就是我结合多年踩坑经验为你梳理的一条从恐惧到驾驭的清晰路径。我们会绕开那些晦涩难懂的纯理论紧扣Unity引擎的工作流聚焦于“如何用起来”和“为什么这么做”让你能快速将知识转化为实际项目中的战斗力。2. 核心概念拆解Shader、Material、ShaderLab与渲染管线在深入代码之前我们必须把Unity中几个最容易混淆的概念彻底理清。很多新手卡在第一步就是因为这些基础术语搅成了一团浆糊。2.1 ShaderGPU执行的算法脚本首先Shader着色器本质上是一个文本文件通常以.shader为后缀。它里面编写的代码使用HLSL、GLSL等语言定义了顶点如何变换、像素如何着色的完整算法。但这个文件本身在场景里是不可见的它只是一套“宪法”或“菜谱”。Unity中的Shader主要分为三大类这与官方手册的划分一致但理解角度可以更实用表面着色器Surface ShaderUnity提供的一种高级抽象层。你不需要关心光照模型的具体计算只需描述表面的颜色Albedo、光滑度Smoothness、法线Normal等属性Unity会自动帮你生成处理前向渲染或延迟渲染所需的多Pass顶点/片元着色器代码。它是新手入门和实现大多数标准PBR基于物理的渲染效果最快的方式。它的代码通常被CGPROGRAM和ENDCG包裹。顶点/片元着色器Vertex/Fragment Shader这是更底层、更灵活的方式。你需要手动编写顶点着色器函数处理顶点位置、法线、UV等和片元着色器函数计算最终像素颜色。它让你对渲染流程有完全的控制权适合实现诸如顶点动画、复杂纹理混合、自定义光照等效果。固定函数着色器Fixed Function Shader非常古老的写法用一系列指令如SetTexture,Lighting On来配置固定的渲染管线功能。在现代Unity中已基本弃用仅在维护非常老的项目时可能遇到。注意很多人会把通过Shader Graph可视化工具创建的.shadergraph文件也叫做Shader这没问题。它最终也会被编译成顶点/片元着色器。你可以把Shader Graph看作是编写Shader的“可视化编程”环境。2.2 MaterialShader参数的实例化容器如果说Shader是菜谱那么Material材质就是按照这份菜谱加入了具体食材参数后做出来的那道菜。材质球.mat文件必须关联一个Shader。你在Inspector面板里看到的所有可调节选项——_MainTex主纹理、_Color颜色、_Metallic金属度——都是该Shader所暴露出来的属性。同一个Shader可以创建出无数个材质球每个材质球都可以有一套独立的参数。一个常见的误区是试图通过修改材质来改变渲染算法这是行不通的。要改变算法必须更换Shader。例如Standard Shader可以实现金属质感但要变成半透明效果你必须换成Standard (Specular setup)或者自定义的透明Shader。2.3 ShaderLabUnity的Shader包装语言这是Unity特有的一层封装。打开任何一个.shader文件你会看到大量不在HLSL中的语法比如Shader “Custom/MyShader”、Properties{}、SubShader{}、Pass{}。这部分就是ShaderLab。它的作用至关重要定义结构组织一个Shader对象的整体框架可以包含多个SubShader用于适配不同硬件或渲染管线每个SubShader下又包含多个Pass渲染通道。声明属性在Properties{}块中声明的变量如_MainTex(“Albedo”, 2D) “white” {}会显示在材质面板上并可以通过C#脚本进行访问和修改。配置渲染状态在Pass中你可以用命令设置深度测试ZTest、混合模式Blend、剔除方式Cull等GPU状态。这是控制物体如何与场景中其他物体进行交互的关键。一个生动的类比把整个Shader文件看作一个产品。ShaderLab是产品的外壳和说明书定义产品名、可调节按钮、使用规则而CGPROGRAM里面的HLSL代码则是产品的核心电路板执行计算的硬件逻辑。两者缺一不可。2.4 渲染管线Shader运行的舞台这是近年来Unity变化最大、也最容易让人困惑的部分。你可以把渲染管线Rendering Pipeline理解为一条固定的汽车生产线。Shader是这条生产线上的机器人负责完成喷涂颜色片元着色等具体工序。Unity现在主要有三条“生产线”内置渲染管线Built-in Render Pipeline传统、固定的管线。你写的Shader尤其是表面着色器是专门为这条老生产线设计的机器人。通用渲染管线URPUniversal Render Pipeline新的、可轻度定制的轻量级生产线。它为了高性能和跨平台做了优化。你需要使用为URP编写的Shader如“Universal Render Pipeline/Lit”或者用Shader Graph创建旧的Built-in Shader在这条线上无法直接工作。高清渲染管线HDRPHigh Definition Render Pipeline新的、高度可定制的高保真生产线用于追求极致画面的PC/主机游戏。它需要专门为HDRP编写的Shader。关键点在选择或学习一个Shader前必须首先明确你的项目使用的是哪种渲染管线。用错了管线Shader要么不显示要么效果异常。这也是为什么从Asset Store下载的Shader插件经常需要你导入对应的URP或HDRP版本。对于新手和大多数移动端、独立游戏项目强烈建议从URP开始它代表了未来的方向且学习资源越来越丰富。3. 动手编写第一个自定义Shader从“Hello World”到动态效果理论说得再多不如动手写一行代码。我们从一个最简单的、不依赖光照的顶点/片元着色器开始然后逐步为它添加功能。请在你的Unity项目中创建一个新的Shader文件Create Shader Unlit Shader命名为MyFirstShader。3.1 基础结构一个纯色Shader双击打开你会看到Unity生成的模板代码。我们将其精简并拆解// 1. ShaderLab外壳定义Shader在材质面板中的路径 Shader Custom/MyFirstShader { // 2. 属性块暴露给材质面板和C#脚本的变量 Properties { // 语法_变量名(“面板显示名”, 类型) 默认值 {} _BaseColor (Color, Color) (1, 1, 1, 1) // 默认白色 } // 3. 子着色器SubShader至少一个 SubShader { // 标签告诉Unity如何以及何时渲染这个SubShader Tags { RenderTypeOpaque } // 不透明物体 // 渲染状态此处使用默认状态深度写入开启深度测试开启等 // 4. 通道Pass至少一个真正的渲染绘制在这里发生 Pass { // 开始CG/HLSL编程 CGPROGRAM // 指定顶点着色器函数名 #pragma vertex vert // 指定片元着色器函数名 #pragma fragment frag // 引入常用的工具函数和宏 #include UnityCG.cginc // 声明与Properties块中同名的变量以便在CG代码中使用 fixed4 _BaseColor; // 5. 定义从CPU模型数据传入GPU的结构体 struct appdata { float4 vertex : POSITION; // 模型空间顶点位置语义POSITION }; // 定义从顶点着色器传递给片元着色器的结构体 struct v2f { float4 pos : SV_POSITION; // 裁剪空间顶点位置语义SV_POSITION }; // 6. 顶点着色器函数每个顶点执行一次 v2f vert (appdata v) { v2f o; // 将模型空间顶点坐标转换到裁剪空间这是最核心的变换 o.pos UnityObjectToClipPos(v.vertex); return o; } // 7. 片元着色器函数每个像素更准确说是片元执行一次 fixed4 frag (v2f i) : SV_Target { // 直接返回我们定义的_BaseColor return _BaseColor; } ENDCG // 结束CG编程 } } }创建一个材质球使用这个Shader然后拖到场景中的一个模型上。你应该能看到一个纯色的物体。恭喜你完成了Shader的“Hello World”你可以通过材质面板调整_BaseColor来改变它的颜色。实操心得UnityObjectToClipPos这个函数是顶点着色器的核心。它内部完成了模型空间 - 世界空间 - 观察空间 - 齐次裁剪空间这一系列矩阵变换。在入门阶段你可以把它当作一个黑盒函数直接使用这能避免你陷入复杂的矩阵数学中。3.2 添加纹理采样纯色太单调了我们来贴上纹理。这需要做三件事在Properties中声明一个纹理属性。在CG代码中声明对应的纹理和采样器。在顶点着色器中传递UV坐标在片元着色器中采样纹理。修改Shader如下Shader Custom/MyFirstShader { Properties { _BaseColor (Color, Color) (1, 1, 1, 1) _MainTex (Albedo (RGB), 2D) white {} // 新增纹理属性 } SubShader { Tags { RenderTypeOpaque } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc fixed4 _BaseColor; sampler2D _MainTex; // 声明纹理采样器 float4 _MainTex_ST; // 声明纹理的缩放平移参数非常重要 struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; // 新增接收第一套UV坐标 }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; // 新增传递UV给片元着色器 }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); // 应用纹理的缩放Scale和平移Translation变换 o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // 使用uv坐标对_MainTex进行采样得到纹理颜色 fixed4 texColor tex2D(_MainTex, i.uv); // 将纹理颜色与基础颜色相乘这是最常见的混合方式 return texColor * _BaseColor; } ENDCG } } }关键点解析_MainTex_ST这是一个Unity自动提供的四维向量float4。xy分量是纹理的缩放Tilingzw分量是纹理的偏移Offset。你在材质面板的纹理槽里调整Tiling和Offset就是在修改这个变量。TRANSFORM_TEX(v.uv, _MainTex)这是一个内置宏它的作用等同于v.uv * _MainTex_ST.xy _MainTex_ST.zw。它简化了应用纹理变换的代码。tex2D(sampler2D tex, float2 uv)HLSL内置函数用于在给定的UV坐标处对2D纹理进行采样。现在你的材质球上会出现一个纹理槽拖入一张图片模型就会显示该纹理并且你可以通过调整Tiling和Offset来重复或移动纹理。3.3 实现顶点动画让旗帜飘动Shader的魅力在于其动态性。我们利用顶点着色器修改顶点位置来实现一个简单的旗帜飘动效果。这需要在CPU传入的顶点数据中利用其原有的位置信息来计算一个偏移量。修改顶点着色器部分// 在Properties中添加控制参数 Properties { _BaseColor (Color, Color) (1, 1, 1, 1) _MainTex (Albedo (RGB), 2D) white {} _WaveSpeed (Wave Speed, Float) 1.0 _WaveFrequency (Wave Frequency, Float) 1.0 _WaveAmplitude (Wave Amplitude, Float) 0.1 } // 在CG代码中声明这些变量 float _WaveSpeed; float _WaveFrequency; float _WaveAmplitude; v2f vert (appdata v) { v2f o; // 1. 在模型空间计算顶点动画偏移 // 我们假设旗帜沿模型空间的X轴方向飘动Y轴为高度 // 使用正弦函数模拟波浪offset sin(频率 * 位置 时间 * 速度) * 振幅 float wave sin(_WaveFrequency * v.vertex.x _Time.y * _WaveSpeed) * _WaveAmplitude; // 2. 将偏移量加到顶点的Y坐标上 // 注意这里直接修改了传入的顶点数据v.vertex这是一个float4修改其y分量 v.vertex.y wave; // 3. 将修改后的顶点坐标转换到裁剪空间 o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; }原理与技巧_TimeUnity内置的浮点四维向量提供了时间信息。_Time.y是自场景加载以来的总时间秒常用于动画。sin函数是周期性动画的利器。sin(w * x t * s)是一个经典的一维波动方程其中w是角频率控制波的数量t是时间s是波速。性能提示顶点动画在顶点着色器中完成其性能消耗与顶点数量成正比。对于像旗帜、水面这样顶点密集的网格需要谨慎控制计算复杂度。对于大量草叶的摆动通常采用更优化的方式如在片元着色器中计算顶点ID的抖动。现在运行游戏你会看到模型尤其是像一个平面网格像旗帜一样波动起来。通过材质面板调整_WaveFrequency频率波的数量、_WaveAmplitude振幅波的高度和_WaveSpeed速度可以控制波动的效果。4. 深入核心理解光照模型与表面着色器现实世界的光照极其复杂Shader通过光照模型来模拟。最简单的光照模型是兰伯特Lambert漫反射物体表面的颜色 光源颜色 * 表面颜色 * max(0, dot(表面法线, 光源方向))。dot是点积衡量两个向量的夹角余弦值夹角越小越面向光源亮度越高。4.1 自己实现一个简单的漫反射光照我们修改之前的无光照Shader让它接受一个方向光。这需要我们在片元着色器中获取光照信息。// 修改片元着色器 fixed4 frag (v2f i) : SV_Target { fixed4 texColor tex2D(_MainTex, i.uv); // 1. 获取世界空间法线需要从appdata传入法线并通过顶点着色器变换 // 假设我们在appdata和v2f结构体中添加了法线信息normal : NORMAL // 并在顶点着色器中使用 o.worldNormal UnityObjectToWorldNormal(v.normal); 进行变换 float3 worldNormal normalize(i.worldNormal); // 2. 获取主平行光的方向世界空间从表面指向光源 float3 lightDir normalize(_WorldSpaceLightPos0.xyz); // _WorldSpaceLightPos0是第一个逐像素光源的位置/方向。对于平行光其w分量为0xyz即方向。 // 3. 计算兰伯特漫反射系数 float lambert max(0, dot(worldNormal, lightDir)); // 4. 获取主平行光的颜色 fixed4 lightColor _LightColor0; // 5. 结合纹理颜色、光照颜色和漫反射系数 fixed4 finalColor texColor * _BaseColor * lightColor * lambert; // 6. 添加环境光Unity内置变量 finalColor.rgb UNITY_LIGHTMODEL_AMBIENT.rgb * texColor.rgb; return finalColor; }这个实现非常基础它只考虑了单个平行光和环境光没有高光镜面反射也没有处理多光源。要完整地处理Unity的整个光照系统包括前向渲染中的多Pass逐像素光、延迟渲染等代码会变得非常复杂。这就是表面着色器的价值所在。4.2 表面着色器让Unity帮你处理光照表面着色器是一种“代码生成”模板。你只需要关注物体表面的物理属性反照率、法线、金属度、光滑度等Unity会自动为你生成处理所有光照所需的顶点/片元着色器代码。创建一个表面着色器Create Shader Standard Surface Shader看看它的核心结构Shader Custom/MySurfaceShader { 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 } SubShader { Tags { RenderTypeOpaque } LOD 200 CGPROGRAM // 关键编译指令声明这是一个表面着色器使用Standard光照模型 #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; fixed4 _Color; half _Glossiness; half _Metallic; // 输入结构体由顶点着色器自动填充 struct Input { float2 uv_MainTex; }; // 核心表面函数。你的主要工作区。 void surf (Input IN, inout SurfaceOutputStandard o) { // 采样纹理 fixed4 c tex2D (_MainTex, IN.uv_MainTex) * _Color; // 设置表面输出 o.Albedo c.rgb; // 漫反射颜色 o.Metallic _Metallic; // 金属度 o.Smoothness _Glossiness; // 光滑度 o.Alpha c.a; // 透明度 } ENDCG } FallBack Diffuse }优势与局限优势极简代码实现PBR标准光照自动处理多光源、阴影、光照贴图等复杂问题。局限对渲染流程的控制较弱难以实现一些非标准或高度定制化的渲染效果如某些特殊的卡通渲染、扭曲效果。生成的代码可能包含冗余对移动平台需要关注性能。选择建议对于项目中90%的标准材质石头、金属、木头、塑料等使用URP/Lit或Standard表面着色器是最佳选择。当你需要实现风格化渲染如卡通、水墨或特殊的屏幕后处理效果时才需要转向手写的顶点/片元着色器。5. 实战进阶常见效果实现思路与Shader Graph入门掌握了基础我们就可以探讨一些常见需求的实现思路。同时对于不擅长代码的开发者Shader Graph是一个强大的替代方案。5.1 常见效果实现思路速查顶点溶解效果思路在片元着色器中使用一张噪声纹理Noise Texture采样得到一个随机值。将这个随机值与一个由脚本控制的_DissolveThreshold溶解阈值比较。如果随机值小于阈值则丢弃clip该片元使其完全透明。同时可以在阈值边缘处利用smoothstep函数计算一个渐变区域并在此区域内输出发光颜色。关键点clip(value)函数会丢弃 value 小于0的像素。噪声纹理的平铺模式Wrap Mode最好设置为RepeatUV可以加上时间偏移以实现动态溶解。滚动UV动画思路在顶点或片元着色器中在采样纹理前对UV坐标进行偏移。float2 scrolledUV i.uv float2(_Time.y * _SpeedU, _Time.y * _SpeedV);。这常用于流动的河流、瀑布、传送门等效果。菲涅尔效应边缘光思路菲涅尔效应指视线与表面法线夹角越大即看向边缘反射越强的现象。计算公式通常为fresnel pow(1.0 - max(0, dot(viewDir, normal)), _FresnelPower)。将fresnel系数叠加到最终颜色或自发光Emission上就能得到边缘发光效果。_FresnelPower控制边缘的锐利程度。屏幕后处理全屏特效思路这不是一个材质Shader而是一个需要挂载在Camera上通过OnRenderImage函数执行的图像处理Shader。它的核心是获取当前渲染完成的屏幕图像_MainTex然后对每个像素进行处理如模糊、调色、Bloom。你需要使用Graphics.Blit函数来应用这个Shader。5.2 Shader Graph可视化的Shader创作对于美术同学或对代码有恐惧感的开发者Shader Graph是福音。它的逻辑和编写Shader完全一致只是用节点连线代替了代码。创建一个简单的动态纹理混合效果在Project窗口右键 Create Shader Graph URP Lit Shader Graph根据你的管线选择。打开Shader Graph编辑器。属性创建两个Texture 2D属性_TexA,_TexB和一个Range属性_LerpFactor, 0-1。采样添加两个Sample Texture 2D节点分别连接到两个纹理属性。将UV坐标节点UV连入它们的UV端口。混合添加一个Lerp线性插值节点。将_TexA的采样结果连入A端口_TexB连入B端口_LerpFactor属性连入T端口。Lerp节点会根据T值0到1在A和B之间平滑过渡。输出将Lerp节点的输出颜色RGBA连接到主节点的Base Color输入口。保存创建一个材质使用此Shader。拖动_LerpFactor滑块你会看到纹理在A和B之间平滑过渡。Shader Graph的核心优势实时预览任何修改都能立刻在预览窗口看到结果迭代速度极快。降低门槛无需记忆HLSL语法通过连接节点直观构建逻辑。易于复用可以将复杂的节点组封装成Sub Graph像函数一样在不同Shader中调用。局限性对于极其复杂或需要精细性能控制的算法手写代码仍然更灵活、更高效。Shader Graph生成的代码可能不如手写代码优化得好。6. 性能优化与调试让Shader既好看又高效一个效果炫酷但导致帧率暴跌的Shader是失败的。在移动平台尤其需要关注性能。6.1 性能优化黄金法则精度选择在CG/HLSL中使用尽可能低的精度。fixed(低精度通常11位)适用于颜色0-1范围和简单的系数。half(中精度16位浮点)适用于UV坐标、法线、光照计算等。float(高精度32位浮点)仅用于世界坐标、矩阵运算等需要高精度的场合。错误示例fixed4 worldPos mul(unity_ObjectToWorld, v.vertex);(世界坐标应用float)正确示例fixed4 albedo tex2D(_MainTex, i.uv);减少纹理采样纹理采样tex2D是GPU中最耗时的操作之一。合并纹理将金属度、光滑度、环境光遮蔽AO等单通道信息打包到一张纹理的R、G、B通道中即RGBA贴图一次采样即可获取所有数据。使用Mipmap确保纹理启用了Mipmap这能显著改善远处物体的缓存效率和视觉质量。避免条件分支在片元着色器中if-else或switch语句可能导致性能大幅下降因为GPU是并行处理多个像素的分支会导致部分核心空闲。尽量用数学函数如step,smoothstep,lerp来替代。简化数学运算用mad(乘加) 指令替代单独的乘法和加法现代编译器通常会自动优化。优先使用内置函数如UnityObjectToWorldNormal它们通常经过高度优化。将可以在顶点着色器中计算的值如一些不依赖像素插值的向量移到顶点着色器因为顶点数量远少于片元数量。注意透明与混合半透明物体Blend SrcAlpha OneMinusSrcAlpha由于需要从后往前排序渲染且无法写入深度缓冲会严重透支Overdraw性能。尽量减少半透明物体的数量和覆盖面积。6.2 Shader调试技巧Shader调试不像C#代码那样可以单步执行通常采用“可视化输出”法。颜色调试法这是最常用的方法。当你怀疑某个中间值计算错误时直接将它作为片元着色器的最终输出颜色。例如你想看法线是否正确return float4(worldNormal * 0.5 0.5, 1.0);将法线从(-1,1)映射到(0,1)的颜色空间。查看UVreturn float4(i.uv, 0, 1);查看深度return i.pos.z / i.pos.w;需要归一化。使用Frame DebuggerUnity的Frame DebuggerWindow Analysis Frame Debugger可以逐渲染命令查看绘制调用。你可以看到每个物体是以哪个Shader的哪个Pass渲染的这对于理解渲染顺序和发现冗余绘制非常有用。平台差异GLSLOpenGL和HLSLDirectX在语法和某些内置函数上略有差异。Unity通常会帮你处理但在处理屏幕坐标VPOS或导数函数ddx/ddy时要特别注意。在编写跨平台Shader时多使用Unity提供的内置宏如UNITY_VPOS_TYPE,UNITY_CALC_FOG_FACTOR。7. 常见问题排查与避坑指南在实际开发中你一定会遇到各种奇怪的Shader问题。这里记录了一些高频“坑点”和解决方案。问题现象可能原因排查与解决思路模型显示为洋红色Missing1. Shader编译错误。2. 当前渲染管线不支持该Shader。3. Shader文件本身丢失或引用错误。1. 查看Console窗口通常会有红色的编译错误信息根据提示修改代码。2. 检查项目渲染管线设置Edit Project Settings Graphics确保Shader与之兼容。Built-in Shader不能用于URP/HDRP项目。3. 在材质面板重新指定正确的Shader。纹理采样出现拉扯或错位1. UV坐标传递错误或未应用TRANSFORM_TEX。2. 纹理导入设置中Wrap Mode不是Repeat导致边缘采样错误。3. 在顶点着色器中修改了顶点位置但未考虑其对UV插值的影响少见。1. 检查顶点着色器中是否将UV正确从appdata传递到v2f并在片元着色器中使用了正确的UV变量。2. 确认_MainTex_ST被正确定义和使用。3. 在Inspector中检查纹理的Wrap Mode对于需要平铺的纹理设为Repeat。半透明物体渲染顺序错乱半透明物体渲染需要从后往前。Unity根据物体中心到相机的距离排序对于大物体或复杂形状可能不准。1. 尝试将大物体拆分成多个小物体。2. 使用两个Pass渲染第一个Pass只写入深度ColorMask 0第二个Pass进行半透明混合。3. 对于UI等2D元素使用Canvas并确保正确的层级顺序。在移动设备上效果异常或性能极差1. 使用了高精度(float)进行大量计算。2. 纹理采样次数过多或纹理尺寸过大。3. 片元着色器中有复杂的循环或分支。1. 使用Shader.SetGlobalFloat(“_MyFloat”, value);在C#中设置全局属性避免在Shader中声明过多float精度变量。2. 使用纹理图集压缩纹理格式ASTC, ETC2。3. 使用Performance Analysis工具如Unity Profiler的GPU模块定位瓶颈。考虑将计算移到顶点着色器或使用查找表LUT。使用_Time动画在编辑器下正常打包后不动编辑器下_Time始终更新但打包后如果游戏逻辑暂停如Time.timeScale0_Time.y也会停止。如果希望动画不受游戏时间缩放影响可以使用unity_DeltaTime.x累加一个自定义的时间变量。或者检查游戏逻辑中是否有暂停时间尺度的代码。法线贴图看起来是平的或颜色奇怪1. 法线贴图纹理类型未设置为“Normal map”。2. 在Shader中采样法线贴图后未将其从切线空间转换到世界空间或观察空间。1. 在纹理导入设置中将Texture Type设为“Normal map”这会启用正确的sRGB和压缩设置。2. 使用内置函数UnpackNormal(tex2D(_BumpMap, i.uv))来解码法线贴图然后使用UnityObjectToWorldNormal或TBN切线-副切线-法线矩阵进行空间变换。一个关于#pragma指令的深坑在表面着色器中#pragma surface surf Standard和#pragma surface surf Standard fullforwardshadows有天壤之别。后者会为所有前向渲染的灯光生成阴影投射的Pass而前者不会。如果你发现你的表面着色器物体不投射阴影很可能就是漏掉了fullforwardshadows这个选项。类似的选项还有addshadow为自定义顶点变形生成正确的阴影等务必根据需求添加。学习Shader是一个“实践-理论-再实践”的螺旋上升过程。不要试图一次性啃完所有的图形学理论。最好的方法是从一个具体的目标效果出发比如“我想做一个X光透视效果”然后去搜索、阅读、修改一个接近的Shader代码在实现的过程中遇到不懂的函数比如step,frac,dot再去查文档、理解其背后的数学和图形学原理。这样带着问题去学习效率最高成就感也最强。当你成功实现了第一个属于自己的特效时那种对渲染流程的掌控感将是驱动你继续深入探索的最大动力。