尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity Shader从Built-in迁移到URP:5.2到6.0实战避坑指南

Unity Shader从Built-in迁移到URP:5.2到6.0实战避坑指南 1. 从Built-in到URPShader升级的底层逻辑拆解如果你手头有一堆基于Built-in管线写的Shader现在要把项目迁到URP那5.2到6.0这个版本跨度里的坑我基本都替你踩过一遍了。这篇文章不讲空泛的理论只聊我在实际迁移过程中遇到的真实问题和解决方案。核心关键词就几个UnityShader、URP、Built-in Shader、HLSL、GraphicsShader。适合谁看已经写过至少两三个Built-in Shader、能看懂CG/HLSL基本语法、但对URP的ShaderLab结构还不够熟悉的开发者。如果你连顶点片元着色器的基本结构都还没写过建议先把基础打牢再来看这篇。先说清楚一个前提Unity的Shader体系在Built-in时代和URP时代表面上看都是写ShaderLab但底层的编译路径、光照模型、变体管理、关键字系统完全不是一回事。很多人以为把CGPROGRAM改成HLSLPROGRAM、把#include UnityCG.cginc换成#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl就能跑结果发现光照全黑、阴影丢失、变体爆炸。这不是你代码写错了而是整个渲染管线的光照计算方式变了。1.1 Built-in和URP的核心差异到底在哪Built-in管线的Shader依赖UnityCG.cginc和AutoLight.cginc光照计算通过UNITY_LIGHT_ATTENUATION这类宏自动处理表面着色器Surface Shader更是把光照模型封装到了极致。你写一个#pragma surface surf StandardUnity帮你生成了一大堆顶点片元代码。这种便利性的代价是你很难精确控制渲染的每一个环节而且这些宏在URP下完全不可用。URP走的是另一条路。它把光照计算拆成了Lighting.hlsl里的显式函数调用你需要自己处理主光源、附加光源、阴影、环境光。UniversalFragmentPBR这个函数承担了大部分PBR光照计算的工作但前提是你得把正确的InputData和SurfaceData结构体填好。这就像从自动挡换到手动挡——控制更精细了但每一步都得自己来。还有一个容易被忽略的点URP的Shader变体数量比Built-in少很多但每个变体的编译要求更严格。Built-in下能跑的隐式类型转换在URP的HLSL编译器下可能直接报错。我遇到过最典型的就是fixed类型在URP下不被支持必须全部换成half或float。1.2 为什么不能直接复制粘贴旧Shader我试过最偷懒的办法把Built-in的Shader代码直接扔到URP项目里只改CGPROGRAM为HLSLPROGRAM。结果编译通过了但渲染出来一片粉红。原因很简单——URP的ShaderLab标签体系不一样。Built-in用的Tags { RenderTypeOpaque }在URP下虽然不报错但LightMode标签必须改成UniversalForward否则URP的渲染循环根本不会调用你的Pass。另一个致命问题是CBUFFER。URP要求所有材质属性必须声明在CBUFFER_START(UnityPerMaterial)和CBUFFER_END之间否则SRP Batcher不会生效DrawCall会暴涨。这个细节在Built-in时代完全不存在因为Built-in没有SRP Batcher这个概念。我第一次迁移时没注意这个场景里200个物体直接掉了30帧排查了半天才发现是CBUFFER没写对。2. ShaderLab结构重写从表面着色器到顶点片元迁移的第一步不是改代码而是重新理解URP的ShaderLab结构。Built-in的表面着色器在URP下没有对应物你必须手写顶点片元着色器。这不是可选项是强制要求。2.1 属性块和CBUFFER的正确写法先看一个标准的URP Shader属性块写法Properties { _BaseMap (Base Map, 2D) white {} _BaseColor (Base Color, Color) (1,1,1,1) _Smoothness (Smoothness, Range(0,1)) 0.5 _Metallic (Metallic, Range(0,1)) 0.0 } SubShader { Tags { RenderType Opaque RenderPipeline UniversalPipeline Queue Geometry } Pass { Name ForwardLit Tags { LightMode UniversalForward } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _SHADOWS_SOFT #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Smoothness; half _Metallic; CBUFFER_END TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); // ... 顶点片元代码 ENDHLSL } }这里有几个关键点必须注意。第一RenderPipeline标签必须设为UniversalPipeline这是URP识别Shader的硬性条件。第二LightMode必须是UniversalForward写错了URP直接跳过这个Pass。第三纹理声明要用TEXTURE2D和SAMPLER宏不能再像Built-in那样直接写sampler2D _MainTex。第四所有材质属性必须放进CBUFFER_START(UnityPerMaterial)里一个都不能漏。注意CBUFFER_START(UnityPerMaterial)里的变量顺序必须和Properties块里的声明顺序一致否则SRP Batcher会静默失效不报错但性能下降。2.2 顶点着色器的输入输出结构变化Built-in的顶点着色器通常用appdata_base或appdata_full作为输入结构URP下这些内置结构体还在但更推荐自定义Attributes和Varyings结构。原因很简单URP的Varyings结构需要包含positionCS裁剪空间位置和positionWS世界空间位置而Built-in的pos语义在URP下不够用。我通常这样定义struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; float4 tangentOS : TANGENT; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float4 shadowCoord : TEXCOORD3; };positionWS和shadowCoord是URP光照计算必需的。shadowCoord通过TransformWorldToShadowCoord函数计算用于采样主光源阴影贴图。Built-in下这些都由UNITY_LIGHT_ATTENUATION宏自动处理URP下必须手动传递。2.3 片元着色器中的光照计算重构这是迁移中最耗时的部分。Built-in的UNITY_LIGHT_ATTENUATION在URP下不存在你需要用Lighting.hlsl里的函数替代。核心流程是填充InputData结构体填充SurfaceData结构体然后调用UniversalFragmentPBR。half4 frag(Varyings input) : SV_Target { SurfaceData surfaceData (SurfaceData)0; half4 baseMap SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); surfaceData.albedo baseMap.rgb * _BaseColor.rgb; surfaceData.metallic _Metallic; surfaceData.smoothness _Smoothness; surfaceData.alpha baseMap.a * _BaseColor.a; surfaceData.occlusion 1.0; surfaceData.normalTS half3(0, 0, 1); InputData inputData (InputData)0; inputData.positionWS input.positionWS; inputData.normalWS normalize(input.normalWS); inputData.viewDirectionWS GetWorldSpaceNormalizeViewDir(input.positionWS); inputData.shadowCoord input.shadowCoord; inputData.fogCoord 0; inputData.vertexLighting 0; inputData.bakedGI SampleSH(inputData.normalWS); inputData.normalizedScreenSpaceUV GetNormalizedScreenSpaceUV(input.positionCS); half4 color UniversalFragmentPBR(inputData, surfaceData); return color; }这段代码看起来比Built-in的表面着色器复杂得多但好处是你完全掌控了每一步。UniversalFragmentPBR内部会处理主光源、附加光源、阴影、环境光的叠加你只需要保证InputData和SurfaceData填对了。实操心得InputData里的bakedGI如果填0物体在阴影里会全黑。用SampleSH采样球谐光照是最简单的补救方式但如果你项目里用了Lightmap需要换成SAMPLE_GI宏。3. 关键字与变体管理从shader_feature到multi_compileBuilt-in时代#pragma multi_compile和#pragma shader_feature的区别很多人不太在意因为Built-in的变体管理相对宽松。URP下这个问题被放大了——变体数量直接决定打包时间和运行时内存。3.1 关键字声明的常见误区我见过最常见的错误是把所有关键字都写成multi_compile。比如阴影相关的关键字URP官方Shader里用的是multi_compile因为阴影开关是运行时动态切换的。但如果你自己加的功能关键字比如_NORMALMAP用shader_feature就够了因为材质上是否启用法线贴图在编辑时就确定了。// 正确做法 #pragma shader_feature _NORMALMAP #pragma shader_feature _EMISSION #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _SHADOWS_SOFTmulti_compile_fragment表示这个关键字只在片元着色器阶段生效可以进一步减少变体编译量。_SHADOWS_SOFT用multi_compile_fragment而不是multi_compile就是因为软阴影只影响片元阶段的光照计算。3.2 变体爆炸的排查和优化迁移完成后我建议立刻做一件事打开Project Settings里的Shader Variant Collection看看你的Shader生成了多少个变体。我迁移的一个角色ShaderBuilt-in下大概200个变体迁到URP后直接飙到1200多个。原因是我把_MAIN_LIGHT_SHADOWS、_MAIN_LIGHT_SHADOWS_CASCADE、_ADDITIONAL_LIGHTS、_SHADOWS_SOFT这些关键字全用了multi_compile而且没有做任何裁剪。优化手段有几个。第一用#pragma skip_variants剔除不需要的变体。比如你的项目不用点光源阴影就可以跳过_ADDITIONAL_LIGHT_SHADOWS。第二把一些关键字改成shader_feature让Unity在打包时自动剔除未使用的变体。第三用multi_compile_fragment替代multi_compile把关键字限制在片元阶段。#pragma skip_variants _ADDITIONAL_LIGHT_SHADOWS #pragma skip_variants _MIXED_LIGHTING_SUBTRACTIVE注意skip_variants要慎用跳过的关键字如果被材质引用运行时会直接报错。建议在开发后期、确认功能稳定后再加。3.3 关键字与SRP Batcher的兼容性SRP Batcher对关键字非常敏感。如果两个材质使用了不同的关键字组合它们就无法合批。这意味着你每加一个shader_feature就多了一层合批断裂的风险。我的经验是把最常用的功能做成材质属性而不是关键字。比如颜色、光滑度、金属度这些用CBUFFER里的变量控制不要用关键字。只有那些真正需要切换代码路径的功能比如是否启用视差映射才用关键字。4. 常见报错与排查技巧实录迁移过程中我遇到过的报错至少有二十几种这里挑最典型的几个附上排查思路和解决方法。4.1 粉红Shader的三种可能原因粉红Shader是URP迁移中最常见的现象但原因可能完全不同。第一种是Shader编译错误Console里会有明确的报错信息通常是语法问题或函数未定义。第二种是LightMode标签写错Shader编译通过但URP不渲染这种情况Console里没有报错需要检查Pass的Tags。第三种是RenderPipeline标签缺失URP找不到这个Shader也会显示粉红。排查顺序建议先看Console有没有编译错误没有的话检查LightMode是否为UniversalForward再检查RenderPipeline是否为UniversalPipeline。4.2 阴影丢失的排查流程阴影丢失通常有三个原因。第一shadowCoord没有正确传递检查顶点着色器里是否调用了TransformWorldToShadowCoord。第二_MAIN_LIGHT_SHADOWS关键字没有声明检查#pragma multi_compile是否包含这个关键字。第三InputData.shadowCoord没有赋值检查片元着色器里是否把input.shadowCoord赋给了inputData.shadowCoord。还有一个隐蔽的原因AdditionalLightShadows在URP Asset里被关闭了。这个选项在URP Asset的Lighting设置里默认是关闭的。如果你需要附加光源投射阴影必须手动打开。4.3 性能骤降的定位方法迁移后性能下降最常见的原因是SRP Batcher没有生效。检查方法很简单打开Frame Debugger看DrawCall数量。如果每个物体都是单独的DrawCall说明SRP Batcher没起作用。原因通常是CBUFFER_START(UnityPerMaterial)没写或者材质属性没有全部放进CBUFFER。另一个原因是变体数量过多导致Shader加载时间变长。用Profiler看Shader.CreateGPUProgram的耗时如果超过5ms就需要优化变体了。问题现象可能原因排查方法解决方案粉红Shader编译错误查看Console修复语法错误粉红ShaderLightMode错误检查Pass Tags改为UniversalForward粉红ShaderRenderPipeline缺失检查SubShader Tags添加UniversalPipeline阴影丢失shadowCoord未传递检查顶点着色器调用TransformWorldToShadowCoord阴影丢失关键字未声明检查pragma添加_MAIN_LIGHT_SHADOWS性能骤降SRP Batcher失效Frame Debugger补全CBUFFER性能骤降变体过多Shader Variant Collection使用skip_variants4.4 平台差异导致的编译失败同一个Shader在Windows上编译通过打包到Android或iOS就报错这种情况在URP下比Built-in更常见。原因是URP的HLSL编译器对不同平台的精度要求更严格。half类型在移动平台上可能被降级为fixed导致精度不足。float类型在移动平台上性能开销较大。我的经验是颜色计算用half位置计算用floatUV用float2。如果移动平台上出现精度问题把关键的half改成float试试。另外#pragma target 3.5在URP下可能需要改成#pragma target 4.5因为URP的某些函数用到了更高的Shader Model特性。5. 从6.0反推5.2版本升级的逆向思维聊完具体的迁移操作我想换个角度说说版本升级的思路。很多人是从5.2往6.0迁但如果你理解了6.0的设计逻辑反过来看5.2的代码会发现很多当时觉得理所当然的写法其实是有问题的。5.1 URP 6.0带来的结构简化URP 6.0相比早期版本最大的变化是UniversalFragmentPBR函数的参数结构更清晰了。早期版本需要手动传一堆参数6.0把InputData和SurfaceData结构体标准化了。这意味着你从5.2迁移过来的Shader如果按照6.0的结构重写代码量反而比Built-in的表面着色器更少。另一个变化是ShaderVariablesFunctions.hlsl里的工具函数更完善了。比如GetWorldSpaceNormalizeViewDir、GetNormalizedScreenSpaceUV这些函数在6.0里可以直接调用不需要自己手算。我迁移时一开始不知道这些函数的存在自己写了一遍后来发现官方已经提供了白白浪费了半天时间。5.2 迁移后的性能对比实测我拿一个中等复杂度的场景做了对比测试200个物体每个物体使用同一个Shader的不同材质实例主光源带阴影附加光源两个。Built-in下DrawCall是200URP下开启SRP Batcher后DrawCall降到了3。帧率从Built-in的72fps提升到了URP的110fps。这个提升主要来自SRP Batcher的合批能力而不是URP本身的光照计算更快。但要注意SRP Batcher的合批是有条件的所有材质必须使用同一个Shader变体且材质属性必须全部在CBUFFER里。如果材质之间使用了不同的关键字组合合批就会断裂。所以迁移完成后一定要检查材质的关键字使用情况尽量统一。5.3 哪些Built-in特性在URP下没有对应物不是所有Built-in的功能都能在URP下找到替代。比如表面着色器的tessellate指令URP不支持。Geometry Shader在URP下虽然能用但移动平台上支持有限。Compute Shader在URP下可以用但需要额外的设置。我遇到最麻烦的是GrabPass。Built-in的GrabPass在URP下没有直接替代需要用CameraOpaqueTexture或者RenderTexture手动实现。如果你的Shader依赖GrabPass做折射或扭曲效果迁移工作量会比较大。实操心得迁移前先列一个清单把Shader里用到的所有Built-in特性列出来逐个确认URP下是否有替代方案。没有替代方案的要么改效果要么放弃。6. 迁移后的验证与优化清单Shader迁移完成、能正常渲染只是第一步。真正的工作量在于验证和优化。我整理了一份自己每次迁移后都会走的检查清单按优先级排列。6.1 功能验证的五个必查项第一主光源方向变化时光照是否正确响应。第二阴影是否正常投射和接收包括软阴影和级联阴影。第三附加光源是否生效点光源和聚光灯的衰减是否正确。第四环境光是否正常包括球谐光照和Lightmap。第五雾效是否正常URP的雾效计算和Built-in不同。这五项里最容易出问题的是附加光源和雾效。附加光源的关键字_ADDITIONAL_LIGHTS如果没声明附加光源完全不生效。雾效需要在InputData里正确设置fogCoord否则物体在雾里不会变暗。6.2 性能验证的三个关键指标第一个指标是DrawCall数量用Frame Debugger看。第二个指标是Shader加载时间用Profiler看Shader.CreateGPUProgram。第三个指标是GPU耗时用Profiler的GPU模块看。这三个指标如果都比Built-in版本差说明迁移有问题需要回头检查SRP Batcher和变体管理。我一般会做一个A/B对比同一个场景分别用Built-in Shader和URP Shader跑一遍记录三个指标的数据。如果URP版本的DrawCall更少但GPU耗时更高说明光照计算的开销增加了可能需要简化UniversalFragmentPBR的调用或者减少附加光源数量。6.3 长期维护的建议迁移完成后建议把Shader的变体管理纳入版本控制。每次修改Shader后检查变体数量是否增加。如果增加了确认新增的变体是否必要。另外建议给每个Shader写一个简单的测试场景包含主光源、附加光源、阴影、雾效等所有可能影响Shader效果的因素。每次修改后跑一遍测试场景确保没有回归问题。还有一个容易被忽略的点URP的版本升级可能会改变Shader的编译行为。比如从URP 7.x升级到10.x某些函数的签名变了Shader需要跟着改。建议在项目里锁定URP版本升级前先在测试分支验证所有Shader。验证项检查内容通过标准常见问题主光源方向变化时光照响应光照方向正确法线空间错误阴影投射和接收阴影清晰无锯齿shadowCoord未传递附加光源点光源和聚光灯衰减正确关键字未声明环境光球谐和Lightmap亮度自然bakedGI填0雾效距离雾和高度雾过渡平滑fogCoord未设置DrawCallFrame Debugger合批生效CBUFFER缺失Shader加载Profiler低于5ms变体过多GPU耗时Profiler与Built-in持平或更低光照计算过重这套流程走下来一个中等复杂度的Shader迁移大概需要两到三天。听起来很久但比起迁移后线上出问题再回头排查这个时间投入是值得的。我踩过最深的坑是一个角色Shader迁移后阴影在移动平台上完全丢失排查了一整天才发现是_MAIN_LIGHT_SHADOWS_CASCADE关键字在移动平台上需要额外声明#pragma multi_compile_fragment _ _MAIN_LIGHT_SHADOWS_CASCADE。这种问题在编辑器里完全看不出来只有真机测试才会暴露。最后分享一个小技巧如果你不确定某个Built-in函数在URP下对应什么去翻URP的Lighting.hlsl和ShaderVariablesFunctions.hlsl源码。这两个文件里包含了大部分你需要的工具函数而且注释写得很清楚。我迁移时把这两个文件打印出来放在旁边遇到不确定的就查比在网上搜答案快得多。
返回列表