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”“Transparent1”意味着渲染顺序在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类型的纹理_STScale-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和SubShaderURP有自己预定义的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问题时希望你的第一反应不再是盲目搜索和尝试而是能系统地思考是属性定义问题是变体丢失问题是渲染状态问题还是平台兼容性问题有了这个思维框架解决问题就有了清晰的路径。