Unity URP光照系统迁移指南:从Built-in到URP的Shader改造与避坑

发布时间:2026/10/14 1:40:59
Unity URP光照系统迁移指南:从Built-in到URP的Shader改造与避坑
1. 从Built-in到URP光照系统迁移的认知重构很多做了两三年Unity的朋友第一次把项目从Built-in管线切到URP的时候都会经历一个相同的困惑场景里同样的模型、同样的材质、同样的平行光参数切过去之后画面要么暗了一大截要么亮得发白阴影边缘还出现了奇怪的锯齿和断层。更让人抓狂的是以前在Built-in里写得好好的Surface Shader到了URP直接编译报错连个能跑起来的版本都找不到。这不是你的错觉也不是Unity在故意折腾人。Built-in和URP在光照处理上从底层架构到数据传递方式几乎可以说是两套完全不同的体系。Built-in走的是前向渲染为主、延迟渲染为辅的传统路线光照计算大量依赖内置的全局变量和自动生成的代码URP则彻底重构了渲染管线把光照数据打包成结构体通过SRP Batcher和Constant Buffer统一管理Shader端需要显式声明和接收这些数据。这篇文章就是写给那些正在经历这个迁移过程的开发者。不管你是刚接触URP的新手还是已经用了一段时间但始终没搞明白光照差异的老手我都会从实际代码和渲染流程出发把Built-in和URP在光照处理上的核心区别一条一条拆开讲清楚。读完你至少能明白三件事为什么同样的参数在两个管线里表现不同、URP的光照数据到底是怎么传到Shader里的、以及迁移时哪些坑是必须提前避开的。2. 光照架构的底层差异为什么不能直接照搬2.1 Built-in的“自动挡”光照模式Built-in管线最让人又爱又恨的地方就是它的自动化程度极高。你写一个Surface ShaderUnity会自动帮你生成大量的光照计算代码。你只需要在#pragma surface surf Standard里指定光照模型剩下的漫反射、高光、阴影、光照探针、反射探针Unity全帮你处理了。这种设计的好处是上手快坏处是黑盒化严重。很多开发者写了几年Surface Shader都不知道_WorldSpaceLightPos0这个变量到底是怎么来的也不知道UNITY_LIGHT_ATTENUATION宏展开后到底做了什么。一旦遇到需要自定义光照效果的需求就完全不知道从哪下手。Built-in的光照数据传递主要依赖几个全局变量和宏// Built-in中常见的光照变量 float4 _WorldSpaceLightPos0; // 主平行光方向 float4 _LightColor0; // 主平行光颜色 float4 _WorldSpaceCameraPos; // 相机位置这些变量由Unity在渲染时自动填充Shader里直接拿来用就行。阴影衰减通过UNITY_LIGHT_ATTENUATION宏计算光照探针通过ShadeSH9函数采样。整个流程是隐式的你不需要知道数据从哪来只需要知道怎么用。2.2 URP的“手动挡”数据传递URP把光照数据的传递方式彻底改了。它不再依赖那些散落的全局变量而是把每帧的光照信息打包成结构体通过Constant Buffer统一上传到GPU。在Shader端你需要显式地声明这些结构体然后从里面读取你需要的数据。最核心的变化是Light结构体和InputData结构体// URP中光照数据的核心结构体 struct Light { half3 direction; half3 color; half distanceAttenuation; half shadowAttenuation; uint layerMask; }; struct InputData { float3 positionWS; half3 normalWS; half3 viewDirectionWS; float4 shadowCoord; half fogCoord; half3 vertexLighting; half3 bakedGI; };这个变化带来的直接影响是你不能再像Built-in那样随手拿全局变量了。在URP的Shader里你必须先通过InitializeInputData函数把顶点数据、法线、光照探针等信息填充到InputData结构体里然后再调用LightingLambert或LightingPhysicallyBased等函数来计算最终光照。注意URP的Light结构体里direction字段在平行光下表示光线方向在点光源和聚光灯下表示从光源指向着色点的方向。这个细节在写自定义光照时非常容易搞混建议在代码里加注释标明。2.3 两套体系的核心差异对照为了更直观地看出区别我把Built-in和URP在光照处理上的关键差异整理成了一张表对比维度Built-inURP光照数据传递全局变量 宏结构体 Constant BufferShader编写方式Surface Shader为主HLSL手写为主阴影计算UNITY_LIGHT_ATTENUATION宏ShadowAttenuation函数光照探针ShadeSH9函数SampleSH函数多光源处理自动遍历需手动声明Additional Light光照模型内置Standard/BlinnPhong需自定义或使用Lit Shader变体管理自动生成需手动声明#pragma multi_compile这张表里最值得关注的是多光源处理和变体管理。Built-in会自动帮你处理额外光源的遍历和衰减计算而URP需要你在Shader里显式声明_ADDITIONAL_LIGHTS关键字并手动写循环来遍历额外光源。这个差异直接导致了迁移时最常见的问题场景里的点光源和聚光灯在URP下全部失效。3. URP光照Shader的核心实现拆解3.1 顶点着色器把数据从模型空间搬到世界空间URP的顶点着色器承担了一个在Built-in里被Surface Shader自动完成的任务把顶点位置、法线、切线等数据从模型空间转换到世界空间并填充到Varyings结构体里。Varyings LitPassVertex(Attributes input) { Varyings output (Varyings)0; // 顶点位置模型空间 - 世界空间 - 裁剪空间 VertexPositionInputs vertexInput GetVertexPositionInputs(input.positionOS.xyz); output.positionCS vertexInput.positionCS; output.positionWS vertexInput.positionWS; // 法线模型空间 - 世界空间 VertexNormalInputs normalInput GetVertexNormalInputs(input.normalOS, input.tangentOS); output.normalWS normalInput.normalWS; output.tangentWS half4(normalInput.tangentWS, input.tangentOS.w); output.bitangentWS half3(normalInput.bitangentWS); // 阴影坐标和雾效坐标 output.shadowCoord GetShadowCoord(vertexInput); output.fogCoord ComputeFogFactor(vertexInput.positionCS.z); // UV和光照探针 output.uv TRANSFORM_TEX(input.texcoord, _BaseMap); output.bakedGI SampleSH(normalInput.normalWS); return output; }这段代码里有几个关键点需要展开说。GetVertexPositionInputs是URP提供的一个工具函数它一次性返回了顶点在世界空间、观察空间、裁剪空间三个坐标系下的位置。这个函数内部会根据当前渲染路径前向还是延迟和是否开启GPU Instancing来做不同的处理。你不需要关心这些细节直接拿来用就行。GetVertexNormalInputs同样是一个工具函数它接收模型空间的法线和切线返回世界空间的法线、切线和副切线。这里有一个容易踩的坑如果你的模型没有导入切线数据这个函数返回的切线方向是错的会导致法线贴图完全失效。解决办法是在模型导入设置里把Tangents选项设为Calculate。SampleSH函数负责采样光照探针数据。在Built-in里这个函数叫ShadeSH9URP改成了SampleSH参数也从球谐系数变成了世界空间法线。这个变化让调用更直观但要注意SampleSH返回的是间接光照颜色不包含主光源的直接光照。3.2 片元着色器光照计算的核心战场片元着色器是URP光照处理最核心的部分也是和Built-in差异最大的地方。在Built-in的Surface Shader里你只需要写一行#pragma surface surf StandardUnity就会自动生成完整的光照计算代码。而在URP里你需要自己把整个流程串起来。half4 LitPassFragment(Varyings input) : SV_Target { // 1. 准备基础数据 half4 baseMap SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); half3 albedo baseMap.rgb * _BaseColor.rgb; half alpha baseMap.a * _BaseColor.a; half3 normalWS normalize(input.normalWS); // 2. 填充InputData结构体 InputData inputData (InputData)0; inputData.positionWS input.positionWS; inputData.normalWS normalWS; inputData.viewDirectionWS normalize(_WorldSpaceCameraPos - input.positionWS); inputData.shadowCoord input.shadowCoord; inputData.fogCoord input.fogCoord; inputData.bakedGI input.bakedGI; inputData.normalizedScreenSpaceUV GetNormalizedScreenSpaceUV(input.positionCS); // 3. 准备表面数据 SurfaceData surfaceData (SurfaceData)0; surfaceData.albedo albedo; surfaceData.alpha alpha; surfaceData.metallic _Metallic; surfaceData.smoothness _Smoothness; surfaceData.normalTS half3(0, 0, 1); surfaceData.occlusion 1.0; surfaceData.emission 0; // 4. 调用URP的光照函数 half4 color UniversalFragmentPBR(inputData, surfaceData); // 5. 雾效和最终输出 color.rgb MixFog(color.rgb, inputData.fogCoord); color.a alpha; return color; }这段代码里最值得深挖的是UniversalFragmentPBR函数。这个函数是URP光照计算的总入口它内部做了以下几件事第一计算主光源的直接光照。它会根据inputData.shadowCoord采样阴影贴图得到阴影衰减系数然后调用LightingPhysicallyBased函数计算BRDF。第二遍历额外光源。如果Shader变体里声明了_ADDITIONAL_LIGHTS关键字它会循环处理场景里的点光源和聚光灯。每个额外光源都会单独计算衰减和阴影然后累加到最终颜色上。第三叠加间接光照。把inputData.bakedGI里的光照探针数据、Lightmap数据加到直接光照结果上。第四处理环境反射。根据表面粗糙度和金属度采样反射探针计算环境反射贡献。提示UniversalFragmentPBR是一个高度封装的函数如果你需要自定义光照效果比如卡通渲染的阶梯光照不建议直接修改这个函数而是自己写一个类似的光照计算函数在里面调用LightingLambert或LightingPhysicallyBased等底层函数。3.3 多光源与阴影URP里最容易翻车的部分多光源处理是Built-in和URP差异最大的地方也是迁移时问题最集中的区域。在Built-in里你不需要写任何额外代码Unity会自动处理场景里的所有光源。但在URP里默认情况下只有主光源通常是平行光会被渲染额外光源需要显式声明和手动遍历。先看额外光源的声明方式#pragma multi_compile _ _ADDITIONAL_LIGHTS #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS这两个关键字分别控制是否启用额外光源和额外光源阴影。如果你不声明这两个关键字即使场景里放了十个点光源Shader也不会去计算它们。声明了关键字之后还需要在片元着色器里手动遍历#ifdef _ADDITIONAL_LIGHTS uint pixelLightCount GetAdditionalLightsCount(); for (uint lightIndex 0u; lightIndex pixelLightCount; lightIndex) { Light light GetAdditionalLight(lightIndex, inputData.positionWS); color LightingPhysicallyBased(surfaceData, inputData, light); } #endifGetAdditionalLightsCount返回当前像素受影响的额外光源数量这个数量由URP Asset里的Additional Lights设置决定。你可以选择Per Pixel每个像素计算所有额外光源或Per Vertex每个顶点计算前者质量高但性能开销大后者性能好但光照效果粗糙。GetAdditionalLight函数负责获取指定索引的光源数据包括方向、颜色、距离衰减和阴影衰减。这里有一个性能陷阱每次调用GetAdditionalLight都会重新计算光源数据如果在循环里多次调用同一个光源的数据会造成不必要的重复计算。正确的做法是在循环外先把需要的光源数据缓存起来。阴影处理在URP里也变得更复杂了。Built-in的UNITY_LIGHT_ATTENUATION宏会自动处理主光源和额外光源的阴影URP则需要分别处理// 主光源阴影 half shadow MainLightRealtimeShadow(inputData.shadowCoord); // 额外光源阴影 #ifdef _ADDITIONAL_LIGHT_SHADOWS half additionalShadow AdditionalLightRealtimeShadow(lightIndex, inputData.positionWS); #endifMainLightRealtimeShadow函数内部会根据阴影贴图的采样结果结合阴影强度和平滑参数返回一个0到1之间的衰减系数。这个函数比Built-in的宏更透明你可以清楚地看到阴影是怎么算出来的也方便做自定义修改。4. 迁移实操从Built-in Shader到URP Shader的完整改造4.1 迁移前的准备工作在动手改Shader之前有几件事必须先确认好否则改到一半发现方向错了返工成本会很高。第一确认URP版本和Unity版本匹配。不同版本的URP在API上有细微差异比如GetVertexPositionInputs在URP 7.x和10.x里的参数就略有不同。建议在Package Manager里锁定URP版本不要用Latest。第二备份原始Shader。迁移过程中你很可能需要反复对照Built-in版本的实现把原始Shader复制一份放到项目外的文件夹里方便随时查阅。第三准备一个测试场景。场景里至少包含一个平行光、两个点光源、一个聚光灯、一个带法线贴图的模型、一个带阴影的平面。这个场景用来验证迁移后的Shader是否在所有光照条件下都能正常工作。第四确认渲染路径。URP默认使用前向渲染如果你的项目需要延迟渲染需要在URP Asset里手动开启。前向和延迟渲染在光照数据的组织方式上有差异Shader里的处理逻辑也需要相应调整。4.2 逐行改造从Surface Shader到HLSL假设我们有一个Built-in的Surface Shader功能是标准PBR光照加一张法线贴图。改造的第一步是把Surface Shader的自动生成代码替换成URP的手写HLSL。Built-in版本的典型结构是这样的Shader Custom/BuiltInLit { Properties { _MainTex (Albedo, 2D) white {} _NormalMap (Normal, 2D) bump {} _Metallic (Metallic, Range(0,1)) 0.0 _Smoothness (Smoothness, Range(0,1)) 0.5 } SubShader { Tags { RenderTypeOpaque } CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; sampler2D _NormalMap; half _Metallic; half _Smoothness; struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; void surf (Input IN, inout SurfaceOutputStandard o) { o.Albedo tex2D(_MainTex, IN.uv_MainTex).rgb; o.Normal UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); o.Metallic _Metallic; o.Smoothness _Smoothness; } ENDCG } }这个Shader在Built-in下工作得很好但直接拿到URP里会报一堆编译错误。改造后的URP版本需要完全重写Shader Custom/URPLit { Properties { _BaseMap (Albedo, 2D) white {} _BaseColor (Base Color, Color) (1,1,1,1) _NormalMap (Normal, 2D) bump {} _Metallic (Metallic, Range(0,1)) 0.0 _Smoothness (Smoothness, Range(0,1)) 0.5 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { Name ForwardLit Tags { LightModeUniversalForward } HLSLPROGRAM #pragma vertex LitPassVertex #pragma fragment LitPassFragment #pragma target 3.0 #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile _ _SHADOWS_SOFT #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // ... 结构体和函数定义 ENDHLSL } } }这里有几个关键变化需要特别注意。Tags的变化Built-in的SubShader只需要RenderTypeURP还需要RenderPipeline标签Pass里需要LightMode标签。UniversalForward是URP前向渲染的标准LightMode如果写成ForwardBaseURP会直接忽略这个Pass。Include路径的变化Built-in用UnityCG.cginc和Lighting.cgincURP用Packages/com.unity.render-pipelines.universal/ShaderLibrary/下的文件。这个路径是固定的不要试图去Assets文件夹里找。变体声明的变化Built-in的fullforwardshadows关键字在URP里不存在了需要手动声明_MAIN_LIGHT_SHADOWS、_ADDITIONAL_LIGHTS等关键字。这些关键字决定了Shader会编译出多少个变体声明得越多编译时间越长包体越大。4.3 光照探针和Lightmap的适配光照探针和Lightmap是场景间接光照的主要来源在Built-in和URP里的处理方式也有差异。Built-in里光照探针数据通过ShadeSH9函数采样Lightmap通过UNITY_LIGHTMAP宏和DecodeLightmap函数处理。这些在Surface Shader里都是自动完成的你不需要写任何代码。URP里光照探针数据通过SampleSH函数采样Lightmap通过SAMPLE_TEXTURE2D和DecodeLightmap函数处理。但URP的Lightmap采样需要额外的变体声明#pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ DYNAMICLIGHTMAP_ON然后在顶点着色器里填充Lightmap UV#if defined(LIGHTMAP_ON) output.lightmapUV input.lightmapUV * unity_LightmapST.xy unity_LightmapST.zw; #endif在片元着色器里采样Lightmap#if defined(LIGHTMAP_ON) inputData.bakedGI SampleLightmap(input.lightmapUV, normalWS); #else inputData.bakedGI SampleSH(normalWS); #endif这里有一个容易忽略的细节URP的Lightmap采样需要和光照探针数据做混合。如果场景里既有烘焙的Lightmap又有动态物体的光照探针需要根据物体的静态/动态状态来决定使用哪种间接光照。这个逻辑在Built-in里是自动处理的URP里需要手动判断。注意如果你的场景使用了Lightmap但Shader里没有声明LIGHTMAP_ON关键字模型会完全丢失烘焙光照看起来像是没有光照一样。这个问题在迁移时非常常见建议在Shader里把Lightmap相关的变体全部声明上。5. 常见问题与排查技巧实录5.1 光照突然变暗或变亮这是迁移后最常见的问题通常有以下几个原因。原因一颜色空间不一致。Built-in项目可能用的是Gamma空间URP默认使用Linear空间。在Gamma空间下光照计算是在非线性空间进行的颜色看起来更亮切换到Linear空间后光照计算在线性空间进行最终输出时再转回Gamma空间颜色会变暗。解决办法是在Player Settings里把Color Space设为Linear然后调整光照强度来补偿。原因二光照强度单位不同。Built-in的光照强度是相对值URP的光照强度是物理单位Lux。同样的平行光在Built-in里强度设为1在URP里可能需要设为10000才能达到类似的亮度。这个差异在URP Asset的Lighting设置里可以调整。原因三环境光设置不同。Built-in的环境光在Lighting窗口里设置URP的环境光在URP Asset的Lighting设置里。如果迁移后没有重新设置环境光场景会丢失环境光照看起来整体偏暗。5.2 阴影消失或出现锯齿阴影问题在URP里比Built-in更常见因为URP的阴影设置更分散。阴影消失首先检查URP Asset里是否开启了阴影然后检查主光源的Shadow Type是否设为Soft或Hard。如果都开了还是没有阴影检查Shader里是否声明了_MAIN_LIGHT_SHADOWS关键字以及是否在片元着色器里调用了MainLightRealtimeShadow函数。阴影锯齿URP的阴影分辨率在URP Asset里设置默认是1024。如果场景较大需要提高到2048或4096。另外开启_SHADOWS_SOFT关键字可以启用软阴影但会增加性能开销。如果阴影边缘出现明显的阶梯状锯齿还可以调整Shadow Distance和Shadow Cascade参数。阴影偏移如果阴影出现明显的偏移或漏光需要调整光源的Shadow Bias参数。URP的Shadow Bias分为Depth Bias和Normal Bias前者控制阴影贴图的深度偏移后者控制法线方向的偏移。通常把Depth Bias设为1左右Normal Bias设为0.5左右可以解决大部分阴影偏移问题。5.3 额外光源不生效这个问题几乎每个从Built-in迁移到URP的开发者都会遇到。排查步骤如下第一步检查URP Asset里的Additional Lights设置。如果设为Disabled所有额外光源都不会被渲染。设为Per Vertex或Per Pixel前者性能好但质量低后者质量高但性能开销大。第二步检查Shader里是否声明了_ADDITIONAL_LIGHTS关键字。这个关键字控制Shader是否编译额外光源的代码路径没有声明的话即使URP Asset里开了额外光源Shader也不会去计算。第三步检查片元着色器里是否手动遍历了额外光源。URP不会自动遍历额外光源需要在Shader里写循环调用GetAdditionalLight函数。第四步检查光源的Render Mode。如果光源设为Not ImportantURP会把它当作顶点光源处理不会出现在像素光照循环里。需要把光源设为Important才能被GetAdditionalLight获取到。5.4 常见问题速查表问题现象可能原因排查方法解决方案整体偏暗颜色空间不一致检查Player Settings的Color Space统一使用Linear空间整体偏亮光照强度单位不同检查光源强度值调整URP光照强度阴影消失关键字未声明检查Shader变体声明_MAIN_LIGHT_SHADOWS阴影锯齿阴影分辨率低检查URP Asset设置提高Shadow Resolution额外光源不亮未遍历额外光源检查片元着色器手动写额外光源循环光照探针失效SampleSH未调用检查顶点着色器调用SampleSH填充bakedGILightmap丢失LIGHTMAP_ON未声明检查Shader变体声明Lightmap相关关键字法线贴图错误切线数据缺失检查模型导入设置设置Tangents为Calculate5.5 几个踩过的坑和实操心得坑一不要直接修改URP的Lit Shader。很多人为了快速实现效果直接复制URP自带的Lit Shader来改。这个做法在短期内没问题但URP升级时Lit Shader的代码结构可能会变你的修改版本需要重新适配。更好的做法是新建一个Shader文件通过#include引用URP的库函数只写自己需要的部分。坑二变体数量要控制。URP的Shader变体数量比Built-in多得多因为每个关键字都会生成一个变体。如果声明了_MAIN_LIGHT_SHADOWS、_ADDITIONAL_LIGHTS、_ADDITIONAL_LIGHT_SHADOWS、_SHADOWS_SOFT、LIGHTMAP_ON、DIRLIGHTMAP_COMBINED、DYNAMICLIGHTMAP_ON这七个关键字理论上会产生128个变体。实际项目中要根据需求精简不需要的关键字不要声明。坑三SRP Batcher的兼容性。URP默认开启SRP Batcher它要求所有材质使用相同的Shader变体并且CBUFFER里的变量布局一致。如果你的Shader里用了自定义的CBUFFER需要确保变量顺序和URP的Lit Shader一致否则SRP Batcher会失效Draw Call会飙升。坑四阴影级联的配置。URP的阴影级联Shadow Cascade默认是关闭的大场景里阴影质量会很差。建议在URP Asset里开启Shadow Cascade并设置为2或4级。级联数量越多远处阴影越清晰但性能开销也越大。通常2级级联可以满足大部分场景需求。坑五光照探针的更新频率。URP的光照探针默认每帧更新如果场景里有大量动态物体性能开销会很大。可以在URP Asset里把Light Probe Update Mode设为Indirect Only或Off减少不必要的更新。6. 性能优化与进阶方向6.1 URP光照的性能开销分析URP的光照性能开销主要来自三个方面像素光照数量、阴影贴图采样、变体切换。像素光照数量由URP Asset里的Additional Lights设置决定。Per Pixel模式下每个像素会计算所有影响它的额外光源开销随光源数量线性增长。Per Vertex模式下光照计算在顶点阶段完成开销大幅降低但光照效果会变得粗糙尤其是大三角形模型上会出现明显的光照断层。阴影贴图采样是另一个性能大头。主光源阴影需要采样一次阴影贴图每个额外光源阴影也需要采样一次。如果开启了软阴影每次采样会变成多次采样开销成倍增加。建议只在主光源上开启软阴影额外光源使用硬阴影。变体切换在移动端尤其明显。每次切换Shader变体都会导致GPU管线状态变化产生额外的CPU开销。减少变体的方法是精简关键字声明把不常用的功能拆分成独立的Shader。6.2 移动端URP光照的适配要点移动端GPU的带宽和算力都有限URP光照需要做针对性优化。第一关闭额外光源阴影。移动端上额外光源阴影的开销非常大建议只保留主光源阴影额外光源用距离衰减代替阴影。第二使用Per Vertex光照。移动端上Per Pixel光照的开销可能无法接受尤其是中低端设备。Per Vertex光照虽然质量差一些但性能提升明显。第三降低阴影分辨率。移动端上阴影分辨率设为512或1024就足够了2048以上对画质提升有限但性能开销翻倍。第四关闭阴影级联。移动端屏幕小阴影级联带来的画质提升不明显但性能开销不小。建议关闭级联用较大的Shadow Distance来覆盖整个场景。第五使用Lightmap代替实时光照。静态物体全部烘焙Lightmap动态物体使用光照探针可以大幅减少实时光照计算量。6.3 从URP光照延伸到自定义光照模型掌握了URP的光照数据传递方式之后就可以开始尝试自定义光照模型了。比如卡通渲染的阶梯光照、皮肤渲染的次表面散射、头发渲染的各向异性高光都可以在URP的光照框架下实现。自定义光照模型的核心思路是替换UniversalFragmentPBR函数自己写一个光照计算函数。在这个函数里你可以自由控制漫反射、高光、阴影的计算方式实现各种非真实感渲染效果。half4 CustomLighting(InputData inputData, SurfaceData surfaceData) { // 主光源 Light mainLight GetMainLight(inputData.shadowCoord); half3 diffuse surfaceData.albedo * mainLight.color * saturate(dot(inputData.normalWS, mainLight.direction)); // 阶梯化处理 half stepValue floor(diffuse * 3) / 3; // 额外光源 #ifdef _ADDITIONAL_LIGHTS uint count GetAdditionalLightsCount(); for (uint i 0; i count; i) { Light light GetAdditionalLight(i, inputData.positionWS); half3 additionalDiffuse surfaceData.albedo * light.color * saturate(dot(inputData.normalWS, light.direction)); stepValue floor(additionalDiffuse * 3) / 3; } #endif return half4(stepValue, surfaceData.alpha); }这个自定义光照函数把连续的光照强度量化成了三个阶梯实现了卡通渲染的效果。你可以根据需要调整阶梯数量或者加入高光、边缘光等效果。提示自定义光照模型时记得保留阴影衰减和距离衰减的计算否则光照会失去立体感。阴影衰减通过mainLight.shadowAttenuation获取距离衰减通过mainLight.distanceAttenuation获取。6.4 后续学习路径建议如果你已经搞清楚了Built-in和URP在光照处理上的核心差异接下来可以往这几个方向深入。方向一URP的渲染管线源码。URP的Shader库是开源的可以在Package Manager里找到URP的安装目录直接阅读Lighting.hlsl和LightingCommon.hlsl的源码。这是理解URP光照机制最直接的方式。方向二自定义渲染Pass。URP允许通过ScriptableRendererFeature添加自定义渲染Pass可以实现描边、Bloom、景深等后处理效果。掌握了光照之后自定义Pass是下一个自然的学习目标。方向三Shader Graph。如果不想手写HLSLShader Graph提供了可视化的Shader编辑方式。URP的Shader Graph内置了光照节点可以快速搭建自定义光照效果。不过Shader Graph生成的代码可读性较差复杂效果还是建议手写HLSL。方向四多平台适配。不同平台PC、移动端、主机的GPU架构不同光照优化的策略也不同。建议在多个平台上测试同一个Shader观察性能差异积累跨平台优化经验。我在实际项目里迁移了十几个Built-in Shader到URP最大的体会是不要试图一次性把所有Shader都改完。先改一个最简单的跑通整个流程确认光照、阴影、Lightmap都正常再批量处理其他Shader。每改一个Shader都在测试场景里验证一遍确保没有引入新的问题。另外URP的版本更新比较频繁建议锁定一个稳定版本不要频繁升级否则Shader的API变化会让你疲于奔命。