Unity Shader进阶:从语法到引擎层原理与性能优化实战
1. 项目概述:深入Shader的“里世界”
如果你已经跟着前两篇内容,把Shader的基础语法、光照模型和表面着色器都过了一遍,那恭喜你,你已经成功推开了Shader世界的大门。但门后的世界,远比门口看到的要复杂和精彩。这第三篇,我们不打算再讲新的“积木块”,而是要带你看看这些积木块是怎么被“引擎”这个巨兽组装、优化并最终驱动起来的。很多朋友卡在Shader进阶的路上,不是因为语法不会,而是不理解Unity底层在做什么——为什么我的Shader这么慢?为什么打包后效果变了?那些以“Unity”开头的内置变量和函数,背后到底藏着什么逻辑?今天,我们就来一次彻底的“引擎层”漫游,把这些黑盒子一个个打开。
简单来说,这篇内容的目标读者,是那些已经能写一些基础Shader,但在面对性能优化、平台适配、高级特性时感到无从下手的开发者。我们将聚焦三个核心:ShaderLab的完整语法与底层渲染管线交互、Shader变体与多平台编译的实战管理、以及现代渲染管线(URP/HDRP)下的Shader编写范式迁移。这不是一篇轻松的阅读材料,但啃下来之后,你会对Unity中的Shader有一个系统级的、通透的理解。
2. ShaderLab语法精要与渲染管线交互
很多人把ShaderLab仅仅看作是一个包裹CG/HLSL代码的“配置文件”,这大大低估了它的能力。ShaderLab是Unity定义的一套领域特定语言,它是Shader资产与Unity渲染引擎之间的契约。理解这份契约的细节,是写出高效、稳定Shader的前提。
2.1 Properties区块:不止是面板控件
Properties区块定义了Shader的对外接口。除了常见的_MainTex、_Color,这里有一些高级用法和背后的机制。
Properties { // 基础类型 _Color ("Main Color", Color) = (1,1,1,1) _MainTex ("Base (RGB)", 2D) = "white" {} // [Header] 和 [Space]:组织面板,纯编辑器功能,不影响运行时 [Header(Advanced Options)] [Space(20)] // [Toggle]:生成一个Shader变体关键字(shader_feature) [Toggle(_ENABLE_FOG)] _EnableFog ("Enable Fog", Float) = 0 // [Enum]:下拉菜单,对应不同的数值或关键字 [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend Mode", Float) = 1 // One [Enum(Off, 0, On, 1)] _ZWrite ("ZWrite", Float) = 1 // On // [KeywordEnum]:生成互斥的多选一关键字(shader_feature_local) [KeywordEnum(None, Add, Multiply)] _Overlay ("Overlay Mode", Float) = 0 // [HDR]:标记颜色为HDR,在Inspector中显示颜色拾取器 [HDR] _EmissionColor ("Emission Color", Color) = (0,0,0,1) // [NoScaleOffset]:隐藏纹理的Tiling/Offset字段 [NoScaleOffset] _NormalMap ("Normal Map", 2D) = "bump" {} // [Gamma] 和 [Linear]:指定数值属性的色彩空间解释 // 在Gamma空间下,[Gamma]属性会进行sRGB到线性的转换 [Gamma] _Metallic ("Metallic (Gamma)", Range(0,1)) = 0 }关键机制解析:
[Toggle]和[KeywordEnum]会在材质面板上生成一个开关或下拉菜单。更重要的是,它们会在Shader中生成对应的预处理宏(如_ENABLE_FOG)。当你勾选或选择不同选项时,Unity会为这个材质编译一个包含或不包含该宏代码块的特定Shader变体。这是实现功能开关、性能分级的核心手段。[Enum]可以直接映射到Unity引擎的枚举类型(如BlendMode、CompareFunction),这确保了你的选择与引擎内部定义一致,避免错误。[HDR]、[Gamma]这些属性修饰符,是Unity在特定渲染管线(如线性空间渲染)下进行正确色彩转换的保证。如果你在Gamma空间项目中使用[HDR]颜色而不进行手动转换,可能会得到错误的亮度和色彩。
实操心得:对于性能敏感的项目,谨慎使用
[Toggle]和[KeywordEnum]。每一个不同的关键字组合都会产生一个新的Shader变体,变体数量会呈指数级增长(m个二选一开关会产生2^m个变体)。无节制地使用会导致ShaderLab变体爆炸,显著增加内存占用和编译时间。一个实用的技巧是,将不常变化或与美术风格强相关的选项放在材质Property中,而将运行时可能频繁切换的效果(如受伤高亮、隐身效果)通过脚本设置Material.EnableKeyword来控制,这样变体数量更可控。
2.2 SubShader与Pass:渲染命令的调度中心
SubShader是适配不同硬件等级的逻辑单元,而Pass是实际的绘制调用。它们的标签(Tags)和状态(RenderState)设置,直接翻译成了底层图形API(如OpenGL, Direct3D, Metal)的调用命令。
SubShader { // ------------------------------------- // Tags: 告诉Unity渲染引擎“何时”以及“如何”渲染这个SubShader // ------------------------------------- Tags { “RenderType”=“Opaque” // 被Camera.RenderWithShader或替换材质时使用 “Queue”=“Geometry” // 渲染队列:“Background”=1000, “Geometry”=2000, “AlphaTest”=2450, “Transparent”=3000, “Overlay”=4000 “DisableBatching”=“True” // 某些需要模型空间信息的Shader(如顶点动画)必须禁用动态合批 “ForceNoShadowCasting”=“True” // 不投射阴影 “IgnoreProjector”=“True” // 忽略投影器(Projector)组件 } // ------------------------------------- // LOD:细节级别,可通过Shader.globalMaximumLOD或Material.shader.maximumLOD控制 // ------------------------------------- LOD 300 // ------------------------------------- // Pass 1: 基础颜色Pass // ------------------------------------- Pass { Name “FORWARD_BASE” // 给Pass命名,便于在其他地方引用(如UsePass) Tags { “LightMode”=“ForwardBase” } // **核心!** 定义Pass在渲染管线中的角色 // --------------------------------- // Render State:设置图形API的硬件状态 // --------------------------------- Cull Back // 背面剔除:Back, Front, Off ZWrite On ZTest LEqual // 深度测试:LEqual, GEqual, Less, Always等 Blend SrcAlpha OneMinusSrcAlpha // 混合模式:SrcFactor DstFactor // AlphaToMask On // 用于高质量的抗锯齿Alpha测试(MSAA) // ColorMask RGB // 只写入RGB通道,不写入A CGPROGRAM #pragma vertex vert #pragma fragment frag // ... CG/HLSL代码 ENDCG } // ------------------------------------- // Pass 2: 附加逐像素光Pass(模拟前向渲染的附加光) // ------------------------------------- Pass { Name “FORWARD_ADD” Tags { “LightMode”=“ForwardAdd” } Blend One One // 附加光混合模式:叠加 ZWrite Off // 附加光Pass通常不写入深度,避免深度冲突 CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdadd // **关键编译指令**,为附加光编译变体(点光、聚光等) // ... CG/HLSL代码 ENDCG } // ------------------------------------- // Pass 3: 阴影投射Pass(可选,但想让物体投射阴影必须有) // ------------------------------------- Pass { Name “ShadowCaster” Tags { “LightMode”=“ShadowCaster” } Cull Back ZWrite On ZTest LEqual CGPROGRAM #pragma vertex vertShadowCaster #pragma fragment fragShadowCaster #pragma multi_compile_shadowcaster // ... 通常使用Unity内置的ShadowCaster.cginc ENDCG } }核心机制与避坑指南:
LightModeTag是生命线:这个标签决定了Unity的渲染管线在哪个阶段调用这个Pass。ForwardBase(主方向光+环境光+光照贴图)、ForwardAdd(附加逐像素光)、ShadowCaster(渲染到阴影贴图)、Deferred(延迟渲染GBuffer Pass)、Meta(光照贴图烘焙元Pass)等都是预定义的模式。用错了,你的Pass就不会在预期的时间被渲染。- 渲染队列(Queue)的奥秘:
Queue标签的值实际上是一个偏移量。“Queue”=“Transparent+1”意味着渲染顺序在3001。透明物体通常从后往前渲染,以确保正确的混合。如果你的透明物体渲染顺序错乱,检查Queue标签和材质球的renderQueue属性。 - 混合(Blend)状态是性能与效果的关键:
Blend SrcAlpha OneMinusSrcAlpha是标准的Alpha混合。Blend One One是加法混合,常用于发光、粒子效果。切记:错误的混合模式是导致渲染顺序问题、过度绘制(Overdraw)的元凶。对于完全不透明的物体,不要开启混合。 - ZWrite与ZTest的配合:对于透明物体,通常设置
ZWrite Off,但ZTest LEqual(默认)。这意味着它们不写入深度缓冲,但会与已写入深度的不透明物体进行深度测试,从而保证被不透明物体遮挡的部分不会渲染。半透明物体之间的正确遮挡则需要严格排序,这是实时渲染的经典难题。
2.3 CGPROGRAM与HLSLPROGRAM:代码块的抉择
在Pass内部,我们用CGPROGRAM或HLSLPROGRAM来包裹实际的着色器代码。Unity历史上使用Cg语言,但现在已全面转向HLSL。CGPROGRAM在底层会被Unity转换为HLSL进行处理。
当前最佳实践是:在新项目中统一使用HLSLPROGRAM。它更符合现代图形API(如DX12、Vulkan、Metal)的发展趋势,能获得更好的跨平台兼容性和工具链支持(如Visual Studio的HLSL语法高亮和错误检查)。Unity内置的Shader包含文件(如UnityCG.cginc)同时兼容两者,但一些新的功能或示例可能只提供HLSL版本。
3. Shader变体、多平台编译与 Stripping(剥离)
这是Shader从编写到运行过程中最复杂、最容易出问题的一环,也是导致“为什么编辑器里好好的,打包后却错了”的罪魁祸首。
3.1 Shader变体是如何产生的?
Shader变体(Variant)本质上是同一份Shader源代码,经过不同的预处理宏定义编译后,产生的多个不同版本的GPU机器码。变体产生的源头主要有两个:
#pragma multi_compile和#pragma shader_feature:这是最主要的变体来源。#pragma multi_compile_fog:会为“有无雾效”编译两个变体。#pragma multi_compile DIRECTIONAL DIRECTIONAL_COOKIE POINT POINT_COOKIE SPOT:为不同类型的光源编译多个变体。#pragma shader_feature _NORMALMAP:基于材质是否启用_NORMALMAP关键字,选择性编译法线贴图相关代码。multi_compile会强制编译所有可能的组合,而shader_feature只编译材质实际用到的组合(在Editor模式下)。但在打包时,Unity需要知道所有可能用到的关键字组合,否则相关变体会被错误剥离。
不同的渲染路径和品质设置:Unity会根据项目的Graphics Settings和Quality Settings,为不同的渲染路径(前向、延迟)、不同的阴影质量等级、不同的光照贴图编码方式等自动生成变体。
3.2 实战:管理变体爆炸
一个复杂的Shader可能有成百上千个变体。管理不善会导致:
- 构建时间巨长:每个变体都需要编译。
- 包体巨大:编译后的ShaderLab变体数据会全部打进包。
- 内存占用高:运行时加载的Shader对象包含所有变体信息。
管理策略:
精确使用
shader_feature和multi_compile:- 对于材质特有的、静态的功能开关(如是否用法线贴图、是否开启视差),使用
shader_feature。它只在材质用到时才编译。 - 对于运行时可能动态切换的、或全局性的功能(如不同的雾效模式、不同的阴影级联数),使用
multi_compile。因为它保证所有变体在运行时都可用。 - 有一个特例:
#pragma shader_feature_local(在Unity 2019.3+)或#pragma shader_feature __(两个下划线)。这表示该关键字是“本地”的,不会在全局关键字列表中注册,能有效减少全局变体组合的复杂度,非常适合材质独有的小功能。
- 对于材质特有的、静态的功能开关(如是否用法线贴图、是否开启视差),使用
使用Shader变体收集器(ShaderVariantCollection): 这是Unity提供的官方解决方案。你可以在项目中创建一个
.shadervariants文件,然后手动为每个关键Shader拖入一些代表不同功能组合的材质球。Unity会记录这些材质球用到的变体。在Player Settings的Graphics设置中,指定这个Collection。这样,Unity在打包时就知道需要保留哪些变体,从而安全地剥离未使用的变体。操作步骤:
Assets -> Create -> Shader -> Shader Variant Collection。- 选中创建的SVC文件,在Inspector面板中,将你的Shader拖入“Shaders”列表。
- 在下方“Variants”区域,通过“Add Variant”按钮,从场景中或材质文件夹里,添加使用了不同属性组合(如开启/关闭法线、开启/关闭高光、使用不同混合模式)的材质球。
- 在
Project Settings -> Graphics -> Shader Stripping部分,将“Shader Variant Collection”设置为刚创建的SVC文件。
在脚本中预加载和预编译: 对于在运行时通过
Material.EnableKeyword动态启用的功能,其对应的变体必须在包内。为了确保这些变体不被剥离,除了使用multi_compile,还可以在游戏启动时(如Loading场景)通过Shader.WarmupAllShaders或对特定Shader调用ShaderVariantCollection.WarmUp来触发编译,确保变体已就绪。
3.3 多平台编译的坑与技巧
Unity会将HLSL代码交叉编译到目标平台(如GLSL for OpenGL/WebGL, MSL for Metal, SPIR-V for Vulkan)。这个过程大部分是自动的,但有些语法和特性需要特别注意:
- 精度修饰符:在移动平台(GLSL ES)上,精度(
highp,mediump,lowp)直接影响性能和精度。Unity的surface shader会自动处理。在手动编写的片段着色器中,对于颜色计算,可以尝试使用mediump来提升性能,但要注意可能出现的精度不足导致的条带问题。// 在片段着色器开头可以声明默认精度 #ifdef GL_ES precision mediump float; #endif - 纹理采样差异:DirectX和OpenGL在纹理坐标的V方向上相反(DX是左上角为(0,0),OpenGL是左下角)。Unity的
UnityCG.cginc中的TRANSFORM_TEX宏已经处理了这个问题,它使用了float4类型的纹理_ST(Scale-Translate)属性中的tiling和offset。但如果你手动计算UV,务必使用tex.xy * _MainTex_ST.xy + _MainTex_ST.zw这个模式,以保证跨平台一致性。 - 非2的幂次方(NPOT)纹理:在旧式移动GPU上,对NPOT纹理的Wrap模式可能受限(只能为
Clamp)。现代GPU大多已支持。如果遇到奇怪的黑边,检查纹理尺寸和Wrap模式。
4. 现代可编程渲染管线(SRP)下的Shader编写
Built-in管线虽然经典,但现代项目,尤其是追求高品质图形或需要定制化渲染流程的项目,越来越多地转向可编程渲染管线(SRP),即通用渲染管线(URP)和高清渲染管线(HDRP)。这里的Shader编写范式有显著变化。
4.1 从Built-in到URP/HDRP的核心差异
- 渲染架构不同:Built-in是固定管线(虽然可编程),而URP/HDRP是基于ScriptableRenderPipeline的完全可编程管线。你的Shader不再是与一个黑盒引擎对话,而是与一个由C#脚本定义的、明确的渲染流程对话。
- 内置包含文件和函数库:不再使用
UnityCG.cginc、Lighting.cginc等。URP使用Packages/com.unity.render-pipelines.universal/ShaderLibrary/下的文件,如Core.hlsl、Lighting.hlsl。HDRP的路径类似,但更复杂。 - 光照模型与输入输出结构体:URP/HDRP提供了预定义的
PBR光照函数(如LightingPhysicallyBased)和用于传递数据的结构体(如InputData、SurfaceData)。你需要按照它的“配方”来组装数据。 - Shader Graph的崛起:对于不擅长代码的美术或策划,Shader Graph成为了创建复杂表面着色器的首选可视化工具。但理解其背后的HLSL代码生成原理,对于调试和优化至关重要。
4.2 编写一个URP Lit Shader的步骤示例
假设我们要在URP中手写一个基于物理的、支持主纹理、法线贴图和金属光滑度的工作流Shader。
- 定义Properties:与Built-in类似,但命名可以遵循URP惯例。
- 定义HLSLINCLUDE:在
HLSLINCLUDE和ENDHLSL块中,包含必要的头文件,并定义常量缓冲区和纹理。HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); TEXTURE2D(_MetallicGlossMap); SAMPLER(sampler_MetallicGlossMap); CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Metallic; half _Smoothness; half _BumpScale; half _OcclusionStrength; CBUFFER_END // ... 顶点/片段输入输出结构体定义 ENDHLSL - 编写顶点着色器:主要任务是变换顶点位置、计算UV、以及准备光照所需的数据(如法线、切线、视线方向)。
VertexOutput LitPassVertex(VertexInput input) { VertexOutput output = (VertexOutput)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); output.positionWS = vertexInput.positionWS; // 世界空间位置 output.normalWS = normalInput.normalWS; // 世界空间法线 output.tangentWS = normalInput.tangentWS; // 世界空间切线 output.bitangentWS = normalInput.bitangentWS; // 世界空间副切线 output.viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS); // 视线方向 output.positionCS = vertexInput.positionCS; // 齐次裁剪空间位置 return output; } - 编写片段着色器:这是核心,需要组装URP要求的
SurfaceData和InputData,然后调用URP的UniversalFragmentPBR函数。half4 LitPassFragment(VertexOutput input) : SV_Target { // 1. 采样纹理 half4 albedoAlpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; half4 normalSample = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv); half4 metallicGloss = SAMPLE_TEXTURE2D(_MetallicGlossMap, sampler_MetallicGlossMap, input.uv); // 2. 准备SurfaceData(表面数据) SurfaceData surfaceData; surfaceData.albedo = albedoAlpha.rgb; surfaceData.alpha = albedoAlpha.a; surfaceData.metallic = metallicGloss.r * _Metallic; surfaceData.smoothness = metallicGloss.a * _Smoothness; surfaceData.normalTS = UnpackNormalScale(normalSample, _BumpScale); // 解包法线贴图 surfaceData.occlusion = LerpWhiteTo(metallicGloss.g, _OcclusionStrength); // 环境光遮蔽 surfaceData.emission = half3(0,0,0); // 自发光 surfaceData.clearCoatMask = 0.0; surfaceData.clearCoatSmoothness = 0.0; // 3. 准备InputData(输入数据) InputData inputData; inputData.positionWS = input.positionWS; inputData.normalWS = TransformTangentToWorld(surfaceData.normalTS, half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz)); // TBN矩阵变换 inputData.viewDirectionWS = SafeNormalize(input.viewDirWS); inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS); // 阴影坐标 inputData.fogCoord = 0.0; inputData.vertexLighting = half3(0,0,0); inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS); // 全局光照 // 4. 调用URP PBR光照函数 half4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } - 配置Pass和SubShader:URP有自己预定义的
LightMode,如UniversalForward、ShadowCaster、DepthOnly等。你需要确保Pass的Tag设置正确,并且渲染状态(混合、深度测试等)符合URP管线的要求。
从Built-in迁移到URP/HDRP的最大挑战在于思维转换:从“我写代码计算光照”转变为“我按照管线要求提供数据,让管线的光照函数去计算”。这需要仔细阅读URP/HDRP的官方文档和Shader库源码,理解每个函数和结构体的含义。
5. 调试、优化与常见问题排查
Shader写完了,效果不对或者性能太差怎么办?
5.1 调试技巧
- Frame Debugger(帧调试器):
Window -> Analysis -> Frame Debugger。这是最强大的工具,可以暂停游戏,一步步查看每一个Draw Call,查看当时渲染状态的详细信息,包括使用的Shader、Pass、渲染目标、以及所有Uniform/Texture的赋值情况。如果你的物体没画出来,或者画错了,首先用它。 - RenderDoc:第三方独立工具,功能比Frame Debugger更底层、更强大。可以捕获一帧完整的GPU调用序列,查看任意缓冲区的数据(顶点缓冲、索引缓冲、常量缓冲、纹理、深度/模板缓冲等)。对于解决复杂的渲染错误(如深度测试问题、混合问题、计算错误)不可或缺。
- 在Shader中输出调试颜色:这是最原始但有效的方法。在片段着色器中,临时将输出颜色改为某个中间值(如法线、深度、某个纹理通道),可以快速定位问题阶段。
// 输出世界空间法线(映射到0-1范围) return half4(input.normalWS * 0.5 + 0.5, 1.0); // 输出深度值 float depth = Linear01Depth(input.positionCS.z, _ZBufferParams); return half4(depth.xxx, 1.0);
5.2 性能优化要点
- 减少纹理采样:纹理采样是片段着色器中最耗时的操作之一。
- 合并纹理:将金属度、光滑度、环境光遮蔽(AO)打包到一张纹理的R、G、B通道。
- 使用Mipmap:确保纹理启用了Mipmap,远距离物体使用低级别Mip,减少缓存未命中。
- 避免条件采样:像
if (useNormalMap) sample(normalTex)这样的代码,在GPU上所有分支都会执行,然后丢弃一个结果。应尽量使用lerp或预计算权重来避免分支。
- 优化计算:
- 将计算移到顶点着色器:如果某些值在三角形面上变化不大(如雾效因子、简单的顶点动画),可以在顶点着色器计算,然后通过插值传递给片段着色器。但要注意透视校正插值可能带来的问题。
- 使用近似函数:例如,用
pow(x, 2.2)进行伽马校正很耗,在某些情况下可以用x*x(对应2.0)或查表法近似。 - 善用
mad指令:乘加(Multiply-Add)是GPU的单条指令。将a*b + c写成mad(a, b, c)(HLSL内置)或让编译器优化。
- 减少Overdraw(过度绘制):
- 严格的渲染顺序:不透明物体从前往后画(利用深度测试提前丢弃),透明物体从后往前画。
- 使用深度预通道(Depth Prepass):先只渲染深度,再渲染颜色。这样在渲染颜色时,大部分被遮挡的像素会被深度测试快速拒绝。这在URP/HDRP中可以通过Render Objects Renderer Feature实现。
- 尽早进行裁剪测试(Alpha Test/Clipping):如果片段最终会被丢弃(如Alpha Test的镂空部分),在着色器开头就用
clip()函数丢弃它,避免后续不必要的计算。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 物体完全黑色/不显示 | 1. Shader编译错误。 2. 渲染队列(Queue)设置错误,被其他物体遮挡。 3. 深度测试(ZTest)设置过于严格(如Greater)。 4. 顶点位置变换错误,物体在视锥体外。 | 1. 查看Console窗口是否有Shader错误。 2. 在Frame Debugger中查看该物体的Draw Call是否存在,检查其渲染状态。 3. 临时将Shader输出固定颜色(如红色)看是否显示。 4. 检查顶点着色器输出的 positionCS(裁剪空间坐标)是否在[-1,1]范围内。 |
| 纹理显示为紫色 | 1. 纹理未正确绑定或采样器状态错误。 2. Shader中声明的纹理变量名与Properties中或脚本中设置的不匹配。 3. 纹理导入设置错误(如非2的幂次方未正确设置)。 | 1. 在Frame Debugger中检查该Draw Call的纹理绑定列表。 2. 检查Shader中 sampler2D _MainTex的命名。3. 在Inspector中检查纹理的Wrap Mode和Filter Mode。 |
| 透明物体渲染顺序错乱 | 1. 透明物体的渲染队列(Queue)未正确设置或顺序不对。 2. 多个透明物体相互交错,无法正确排序。 3. 深度写入(ZWrite)被错误开启。 | 1. 确保所有透明物体的Queue是“Transparent”或更高,并手动调整“+N”的偏移量。 2. 对于复杂交错,考虑使用Alpha Test替代Alpha Blend,或重构美术资源。 3. 对于标准半透明混合,确保 ZWrite Off。 |
| 移动设备上Shader变慢或发热 | 1. 片段着色器过于复杂,计算量大或纹理采样多。 2. 使用了高精度(highp)计算,而实际可用mediump。 3. 未使用Mipmap导致纹理缓存效率低。 4. 过度绘制严重。 | 1. 使用Unity Profiler的GPU模块或第三方工具(如ARM Mobile Studio)分析热点。 2. 在片段着色器中对颜色、UV等变量尝试使用 mediump。3. 确保所有纹理启用Generate Mip Maps。 4. 在Scene视图开启Overdraw模式查看。 |
| 打包后Shader效果与编辑器不一致 | 1. Shader变体被错误剥离(Stripping)。 2. 使用了 shader_feature但未在ShaderVariantCollection中收集全。3. 不同平台的纹理压缩或色彩空间差异。 | 1. 检查打包日志,查看Shader变体剥离情况。 2. 确保所有运行时可能用到的关键字组合都通过 multi_compile或SVC保留。3. 检查不同平台的Player Settings,尤其是Color Space和Texture Compression。 |
| 法线贴图看起来不对(太亮/太暗/方向反) | 1. 法线贴图纹理导入设置错误(未标记为“Normal map”)。 2. 在Shader中解包法线时未使用正确的函数(如 UnpackNormal)。3. 切线空间计算错误,TBN矩阵构建不正确。 | 1. 在纹理导入设置中勾选“Bump Map”并将其类型设为“Normal map”。 2. 确保使用 UnpackNormal或UnpackNormalScale函数解包。3. 检查顶点着色器中从模型空间到切线空间的变换矩阵是否正确计算。 |
Shader的学习是一个从“知其然”到“知其所以然”的漫长过程。这篇“其三”试图带你深入到“所以然”的层面,去理解Unity引擎与GPU之间那层薄纱之下发生的事情。这些知识不会让你立刻写出炫酷的屏幕特效,但它们是你解决一切复杂、诡异渲染问题的基石。当再遇到Shader问题时,希望你的第一反应不再是盲目搜索和尝试,而是能系统地思考:是属性定义问题?是变体丢失问题?是渲染状态问题?还是平台兼容性问题?有了这个思维框架,解决问题就有了清晰的路径。