ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

HDRP自定义Shader获取阴影:Custom Pass烘焙RenderTexture全攻略

HDRP自定义Shader获取阴影:Custom Pass烘焙RenderTexture全攻略 有段时间我在HDRP里做一套地形交互效果具体来说是想让草地贴花根据阴影方向变暗同时还能让水面出现阴影遮罩。项目一开始用的是Unity内置渲染管线写起来很顺手GetShadowAttenuation一调就完事。后来切到HDRP再想在自定义Shader里拿一份阴影信息时我直接被卡了整整两天——不是没有api而是HDRP的阴影体系跟内置管线完全是两套逻辑官方Shader里封得严严实实正常路径下根本不让你“单独把阴影值拿出来用”。最后把我救回来的是Custom Pass。我用一个全屏Custom Pass在指定时机把主光阴影信息烤进一张RenderTexture再在任意Shader里采样这张RT问题就彻底解决了。这篇文章就是把当时做的整套方案复盘一遍包括Custom Pass怎么配、Shader怎么写、Cascade偏移为什么要那么算、以及我排过的几个让人抓狂的坑。如果你也在HDRP里遇到类似需求——比如给透明物体做阴影接收、做贴花投影、做雪地脚印的暗部叠加希望这篇能帮你少走弯路。1. 先把问题说清楚HDRP里为什么“取阴影”不是一行代码的事1.1 内置管线和HDRP的阴影获取差异在Unity内置渲染管线下只要你写过Surface Shader或者手写Pass基本都绕不开SHADOW_COORDS和TRANSFER_SHADOW这一套宏。这套东西帮你干了几乎所有事在顶点阶段把世界坐标变换到阴影贴图空间在片元阶段做一次深度比较最终输出一个0到1之间的shadow值。你甚至可以不关心阴影贴图长什么样、里面存的什么格式因为引擎全都封装好了。但是HDRP里这套逻辑彻底变了。HDRP的Lit Shader走的是物理光照管线的完整链路阴影不是简单的一个二值遮罩而是包含了级联阴影(Cascade Shadow)、屏幕空间阴影(Screen Space Shadow)、接触阴影(Contact Shadow)等多个层级的汇总结果。每个光源的阴影贡献要通过一系列复杂的函数计算例如EvaluateShadow、GetMainLightShadowmap等这些函数依赖大量的全局Shader变量——包括阴影图集的缩放偏移、Cascade球体参数、阴影质量设置等。官方Lit Shader里你能改但你要是想写一个自己的Shader把这些逻辑单独抽出来用门槛一下就上来了。我自己第一次尝试直接在自定义Shader里“抄”HDRP的阴影代码时发现拷过来的代码有一堆依赖纹理声明、采样器、各种_CascadeShadowSplitSpheres、_ShadowmapAtlas之类的变量稍微漏一个就编译报错。而且不同HDRP版本这些变量名还有变化网上抄来的老代码在12.x下根本跑不起来。1.2 Custom Pass在这套体系里的位置Custom Pass是HDRP提供的一套渲染注入机制可以理解成在HDRP渲染流程的指定节点上插入你自己的一段渲染逻辑。HDRP内置了完整的渲染帧结构从深度预通道、不透明物体、天空、透明物体到后处理每一段之间都有明确的事件点。Custom Pass可以挂在某个事件点前后执行自己的渲染命令。按照执行位置Custom Pass分为三类Before Rendering、After Opaque Depth And Normal、Before Transparent、Before Post Process、After Post Process等。而按照渲染方式又分Full Screen Pass、Object Pass和DrawRenderers Pass等。我这次用的是Full Screen Custom Pass它可以看作是一段对全屏像素执行的Shader计算很适合做“把某个屏幕空间信息提取出来另存一份”这种工作。HDRP为什么要提供Custom Pass因为真实项目里总有些需求是PBR光照模型覆盖不了的比如体积光边缘描边、传送门扭曲、卡通渲染的描边、地图遮挡高亮等。HDRP把这些“旁路”需求交给了Custom Pass你可以在不修改官方Shader的情况下灵活地插入自己的渲染步骤。而要获取阴影Custom Pass同样是关键手段——它帮助你在特定时机、把HDRP内置阴影系统已经生成好的阴影信息“抄一份”出来供后续自定义Shader使用。注意Custom Pass并不是一个轻量功能它会在每帧渲染中插入一次GPU工作量。如果只是少数几个物体需要阴影信息建议把Custom Pass的RenderTarget分辨率调低或者考虑直接用Object Pass只处理部分物体避免全屏开销。2. 方案选型四条路我都摸过最终选了Custom Pass2.1 四条路线横向对比我这里把HDRP里获取阴影的方式整理成了四种各自适用场景差异很大。第一种直接在自定义Shader里调用HDRP的高层阴影接口尝试和Lit Shader走同一条路径。这个方案理论上是最高保真的因为阴影结果和官方效果完全一致。问题在于接口的内部依赖链很长你需要在Shader里正确声明一堆全局纹理和变量代码量非常大还容易因为版本差异翻车。第二种通过屏幕空间阴影纹理(SSS, Screen Space Shadow)获取。HDRP在开启Screen Space Shadows后会在某个阶段生成一张全屏的阴影遮罩纹理主光阴影信息就在里面。如果只是想在当前屏幕范围内获取主光阴影直接采样这张纹理就够了代码量极小。缺点也很明显它只覆盖屏幕内可见像素而且依赖HDRP的Screen Space Shadows开关如果项目里没开启这个特性就用不了。第三种手动采样主光的Shadow Map自己计算Cascade偏移。这种方式可以把任意世界坐标转换到阴影图集UV精度和灵活性最高而且不局限于屏幕空间比如可以采样场景中某个具体位置的阴影。但需要理解HDRP的Cascade阴影图集布局和偏移公式代码量居中一旦写对了复用性极强。第四种用Custom Pass RenderTexture把阴影信息烘焙出来再传给后续Shader使用。这实际上不是第四种独立方案而是承载第二、第三种方案的容器。我最终的正式选择就是第三种思路放在Custom Pass里跑。做个表格来对比方案代码量精度适用场景坑点高层阴影接口大与Lit完全一致需要完全一致阴影效果的复杂Shader依赖太深版本差异明显采样SSS纹理极小屏幕空间精度屏幕特效、全屏混合、后处理需要开启SSS且无法获取屏幕外阴影手动采样Shadow Map中最高可任意取点贴花、草地交互、阴影查询Cascade偏移一旦写错就会错位Custom Pass烘焙RT小到中取决于选用的内部算法多Shader共享阴影信息需要处理RT时序和全局变量传递2.2 为什么最终决定走Custom Pass我先试的是第一种方案结果在拼接HDRP阴影函数时栽了跟头。当时从Lit.hlsl一路追到Shadows.hlsl发现中间绕了七八层函数再加上各种宏定义控制代码复制过来根本没法直接用。后来我把目标降低——我不需要逐像素完整复刻Lit的阴影链路只需要拿到主光的阴影衰减值用于自己做颜色混合。那就只剩一个思路在某个合适的时机把HDRP已经计算好的阴影信息抓取出来。到了这一步选择Custom Pass就成了自然而然的事情。Custom Pass的好处在于它本身就在HDRP渲染流程内执行所有阴影相关的纹理和变量在该阶段都是可用的不存在“这个纹理还没生成”的时序问题。你只需要在Pass内部正确地引用它们然后把结果写到你自己的RenderTexture里之后别的Shader就能通过全局纹理访问到这份数据。最终我选定的方案是Full Screen Custom Pass 手动采样_MainLightShadowmapTexture。这个组合的好处是精度高、不依赖屏幕空间阴影开关而且阴影信息以RenderTexture的形式全局共享项目里多个Shader都可以拿来用。如果你的需求比较简单只在全屏后处理里用一次阴影也可以直接用第二个方案采样SSS纹理代码能省掉一半。我这里会把两种方式都写出来你按项目场景挑。2.3 版本与工具Unity 2021.3 HDRP 12.x正式写代码之前先把版本环境说清楚。我的项目跑在Unity 2021.3 LTS上HDRP版本是12.x。不同HDRP版本里阴影相关的全局变量名确实有差异比如在某些版本中_ScreenSpaceShadowMapTexture是存在的但在另一些版本中可能改成了_ShadowmapAtlas加索引的方式。所以下面代码里凡是涉及HDRP内部纹理的地方建议你根据自己的版本在FrameDebugger里看一眼实际名字稍作调整就能跑通。附带说一句Shader Graph也可以实现一部分阴影处理它内置了Sample Shadowmap等节点但节点级别能拿到的控制力有限。如果你需要精细控制Cascade偏移或者想拿到原始Shadow Map数据还是手写HLSL更靠谱。我的经验是Shader Graph适合处理常规的PBR材质效果但像这种“提取阴影数据并跨Shader复用”的活手写Shader配合Custom Pass上限更高。3. 实操过程从Custom Pass Volume到把阴影写进RenderTexture3.1 第一步创建Custom Pass Volume并配置注入点在HDRP的项目里你要在场景中创建一个空物体挂上Custom Pass Volume组件。这一步很关键因为它决定了Custom Pass脚本在哪个渲染阶段被执行。组件面板上最重要的两个字段是Mode和Injection Point。Mode选择Global这样Custom Pass对整个相机生效而不是限定在一个Volume范围内。Injection Point需要根据你要抓取的阴影信息选择。如果选择After Opaque Depth And Normal那么此时不透明物体的深度和法线已经写好了阴影相关纹理也可用如果选择Before Transparent则不透明物体和阴影都已经渲染完毕适合抓取完整的阴影状态。我当时选的是After Opaque Depth And Normal。这个阶段主光的Shadow Map已经生成SSS纹理如果开启也会可用而且不透明物体的深度已经写入后续做位置重建不会出错。如果你的目标是抓取包含透明物体参与计算后的阴影状态就要选Before Transparent或更晚的节点。注意Custom Pass Volume组件不会自动创建对应的Custom Pass实例你需要配合代码里的CustomPass子类来挂载Pass。一个Volume上可以挂多个Custom Pass通过Add Custom Pass按钮添加并指定一个CustomPass类型的自定义脚本实例。3.2 第二步C#侧编写Custom Pass脚本接下来是核心部分写一个继承CustomPass的脚本在Execute方法里把你的全屏Shader渲染到一张RenderTexture上。我直接贴我当时用的脚本细节都在注释里。using UnityEngine; using UnityEngine.Rendering.HighDefinition; using UnityEngine.Rendering; public class ShadowToTexturePass : CustomPass { [Header(Settings)] public Material shadowMaterial; // 用于全屏采样的Shader材质 public int rtResolution 1024; // 阴影RT的分辨率建议和屏幕分辨率呈比例 private RenderTexture m_ShadowRT; private const string k_ShadowRTName _MyShadowRT; protected override void Execute(CustomPassContext ctx) { if (shadowMaterial null) { Debug.LogWarning(ShadowToTexturePass: shadowMaterial is null, skip.); return; } // 延迟创建RT避免每次Execute都new if (m_ShadowRT null) { m_ShadowRT new RenderTexture(rtResolution, rtResolution, 0, RenderTextureFormat.RFloat); m_ShadowRT.name MyShadowRT; m_ShadowRT.filterMode FilterMode.Bilinear; } // 关键把RT注册成全局纹理这样之后任何Shader都能用 _MyShadowRT 采样 ctx.cmd.SetGlobalTexture(k_ShadowRTName, m_ShadowRT); // 用目标材质把全屏Shader绘制到 m_ShadowRT 上 // 第4个参数0表示subShader pass index CoreUtils.DrawFullScreen(ctx.cmd, shadowMaterial, m_ShadowRT, null, 0); } }这里有个细节CoreUtils.DrawFullScreen会设置一个全屏三角形作为几何体所以你的Shader不需要写MeshRenderer只需要一个能处理全屏片的Fragment Shader。另外RenderTextureFormat.RFloat用来存阴影值足够如果你需要后面在Shader里直接把这个值当颜色用也可以用RGHalf或者ARGB32但那样会浪费带宽和精度。然后在Custom Pass Volume组件上添加这个自定义Pass把shader材质拖进去分辨率按需调整。我项目里用的rtResolution是屏幕宽度的四分之一左右这样既保证了采样精度又不会让GPU带宽爆炸。3.3 第三步Shader侧核心——手动采样Shadow Map这一步是整个方案最关键的环节。Custom Pass里执行的全屏Shader需要把当前像素的世界坐标反算出来再把它转换到主光阴影图集的采样坐标最后比较Shadow Map深度得到阴影值。下面是一个能跑的FullScreen Shader的完整代码我用的HDRP版本是12.x如果你版本不一样注意看_MainLightShadowmapTexture等变量名是否一致。Shader Custom/ShadowToTexture { SubShader { Tags { RenderPipeline HDRP } Pass { Name ShadowToTexture Cull Off ZWrite Off ZTest Always HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl #include Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl #include Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/Shadows.hlsl struct Attributes { float4 positionCS : POSITION; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; // FullScreen三角形的裁剪坐标直接透传 output.positionCS input.positionCS; output.uv input.positionCS.xy * 0.5 0.5; #if UNITY_UV_STARTS_AT_TOP output.uv.y 1.0 - output.uv.y; #endif return output; } // 声明HDRP主光阴影贴图 TEXTURE2D(_MainLightShadowmapTexture); SAMPLER(sampler_MainLightShadowmapTexture); float4 Frag(Varyings input) : SV_Target { // 从当前像素的uv和深度重建世界坐标 float depth LoadCameraDepth(input.positionCS.xy); float3 positionWS ComputeWorldSpacePosition(input.uv, depth, UNITY_MATRIX_I_VP); // 使用HDRP内置函数计算cascade shadow坐标 // 注意normalWS传0是因为阴影衰减通常不依赖法线 float4 shadowCoord GetCascadeShadowCoord(positionWS, float3(0, 0, 0)); // 采样Shadow Map float shadow SAMPLE_TEXTURE2D(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture, shadowCoord.xy).r; // 视平台差异做校正shadowCoord.z存储的是偏差后的深度 // 这里用比较简单的近似shadow值本身已经是0~1衰减 return float4(shadow.xxx, 1.0); } ENDHLSL } } }这段Shader做了三件事用屏幕UV和深度重建世界坐标然后通过GetCascadeShadowCoord得到阴影图集采样坐标最后用SAMPLE_TEXTURE2D采样主光阴影贴图。如果你手头HDRP版本里没有GetCascadeShadowCoord这个函数你需要手动实现Cascade偏移我后面在第4节详细讲。有些人可能会问为什么不让Lit Shader直接算好阴影我复制一份因为你不是每帧都在写一个和Lit完全一致的Shader反而想在不同Shader里拿到阴影数据做自己的逻辑。Custom Pass相当于在Lit Shader之外开辟了一条旁路你用自己的材质、自己的全屏Shader把阴影信息“提”出来。这种做法虽然多了一步RT绘制但胜在干净、可控、可复用。3.4 第三步备选采样Screen Space Shadow纹理如果你的项目已经开了HDRP的Screen Space Shadows选项其实可以省掉手动求Cascade的麻烦直接采样_ScreenSpaceShadowMapTexture。这个纹理在HDRP内部已经存好了屏幕空间的主光阴影遮罩你只需要在Custom Pass的Shader里按屏幕UV采样即可。Shader部分只需要把Frag简化成TEXTURE2D(_ScreenSpaceShadowMapTexture); SAMPLER(sampler_ScreenSpaceShadowMapTexture); float4 Frag(Varyings input) : SV_Target { float shadow SAMPLE_TEXTURE2D(_ScreenSpaceShadowMapTexture, sampler_ScreenSpaceShadowMapTexture, input.uv).r; return float4(shadow.xxx, 1.0); }这个方案的优点是省掉了世界坐标重建和Cascade偏移计算代码量直接少一半。缺点也很明显它只有屏幕空间的信息无法拿到屏幕外物体的阴影而且如果你没有开启Screen Space Shadows这个纹理就不存在采样结果会是默认值。对于我那种地形贴花和草地的需求屏幕空间阴影勉强够用但遇到需要跨物体法线方向做精细混合的情况Shadow Map方案明显更稳。版本提示不同HDRP版本中_ScreenSpaceShadowMapTexture的行为有差异。有的版本它只存主光的阴影有的版本会把多光源阴影打包到不同通道。建议先用FrameDebugger看一眼实际纹理内容和通道再决定采r通道还是a通道。3.5 第四步在任意Shader里使用这张阴影RT到了这一步阴影已经被烤到了_MyShadowRT这张全局纹理里。任何Shader想用只需要声明全局纹理然后采样即可。比如我要做草地变色就在草地Shader里这样写TEXTURE2D(_MyShadowRT); SAMPLER(sampler_MyShadowRT); float shadowFactor SAMPLE_TEXTURE2D(_MyShadowRT, sampler_MyShadowRT, uv).r; // 用shadowFactor控制草地的暗部渐变注意这里的uv要用屏幕坐标转换到0~1范围的uv。如果是世界空间投影的贴花则需要把世界坐标先换算到屏幕uv再用Load或Sample访问RT。HDRP中可以通过ComputeScreenPos完成转换或者直接在顶点着色器里把positionCS.xy / _ScreenSize.xy作为uv传出。我项目里是让阴影RT同时供三个Shader使用草地交互Shader用阴影值来压暗草地颜色水面Shader用阴影值做倒影遮罩贴花Shader用阴影值决定贴花亮部暗部的权重全程只用了一个Custom Pass性能和效果都满意。这正是把阴影数据烘焙到RT的最大价值——一次计算多处复现。4. 调优与验证Cascade偏移原理、调试手段、性能取舍4.1 Cascade偏移计算的“为什么”很多人卡在最开始就是不明白GetCascadeShadowCoord里头到底干了什么用了什么数学。要理解它先要理解HDRP的影子图集。HDRP的主光阴影不是一张简单贴图而是一张包含了多级Cascade的图集。假设开启了4级Cascade那么Shadow Atlas会被分成4个区域离相机越近的Cascade区域越大、分辨率越高。每个Cascade都有自己的一套世界坐标到阴影贴图空间的变换矩阵和偏移。所以当你有一个世界坐标你先要判断这个点落在哪个Cascade范围内然后用对应的offset和scale去采样正确的图集区域。HDRP判断某个点落在哪个Cascade时使用的是包围球体判定。它把每一级Cascade的体积抽象为一个球体如果世界坐标在球体范围内说明该点使用了这一级Cascade。_CascadeShadowSplitSpheres存了球心坐标和球半径相关参数CascadeShadowSplitSphereRadii存了各级球体的半径平方通过点积和距离比较就能得出cascade索引。判断好cascade索引后再用该级Cascade的偏移_CascadeShadowOffset0~3和缩放比例把世界坐标映射到Shadow Atlas的对应区域。偏移计算的关键是让每个Cascade的采样UV落到图集的正确块里否则就会出现“阴影贴图被切成好几块但采样采错块”的错位问题。所以如果你要手写Cascade偏移不能只写UV偏移还要加上图集区域的scale。参考HDRP源码中的简化逻辑大致是int cascadeIndex GetCascadeIndex(positionWS); float4 shadowCoord mul(_MainLightWorldToShadow[cascadeIndex], float4(positionWS, 1.0)); shadowCoord.xy shadowCoord.xy * _CascadeShadowOffset[cascadeIndex].w _CascadeShadowOffset[cascadeIndex].xy;这段写法不是所有版本都通用但思路是对的。实际项目里我更推荐直接使用HDRP内置的GetCascadeShadowCoord除非你确实需要自定义Cascade数量或者做非标准阴影变换。手写的好处是你能在Shader里把中间变量抠出来调试坏处是容易把自己绕晕。4.2 调试手段FalseColor、逐级输出、错位排查搞CG调试最忌讳“打开Game视图瞎调”。我在写这个Custom Pass时用过几个很有效的调试手段逐个分享。第一个手段是分通道显示。把Custom Pass的Shader改成输出cascade索引而不是阴影值比如在Frag里返回float4(cascadeIndex / 3.0, 0, 0, 1)这样你在屏幕上就能看到哪块区域用的是哪级Cascade。如果Cascade没有明显分层或者分层位置不对说明你的Cascade判定参数有问题。第二个手段是直接显示阴影RT的原始值。把Frag里最后的结果原样输出不经过任何混合、调整。正常情况下你应该看到被遮挡处是接近0的黑色亮处是接近1的白色边缘有柔和的半影过渡。如果你看到整屏都是0.5的灰色或者其他怪异颜色大概率是你采样的纹理根本不对或者屏幕UV翻转错误。第三个手段是错位排查。如果阴影RT中的阴影形状是正确的但位置偏移了半个屏幕那基本可以断定是世界坐标重建时出了问题。这时候先把positionWS可视化输出比如输出positionWS.y的某个范围看屏幕上的亮度变化是否符合相机视角。如果世界坐标重建正确再往下查Cascade偏移如果重建都错了先去检查深度纹理采样和UNITY_MATRIX_I_VP的使用方式。经验之谈很多时候“阴影错位”和“阴影闪烁”的根因都出在RT分辨率上。Custom Pass里如果RT分辨率比屏幕分辨率低很多采样时会产生明显的锯齿和抖动。这属于精度不够而非算法错误调整rtResolution即可。4.3 性能与精度的取舍Custom Pass本质上是在HDRP渲染流程中插入了一次额外的全屏Draw Call和一次RT写入代价主要在于三块RT本身的内存带宽、全屏Shader的计算量、以及后续其他Shader采样这张RT的带宽开销。第一块RT分辨率。我用rtResolution设为屏幕分辨率的四分之一这个值在多数项目里够用内存带宽也只有全屏的1/16。如果你要拿阴影做水面反射之类的精细效果建议保持一半分辨率如果只是做色调混合四分之一完全够。另外记得把RT的filterMode设成Bilinear能缓解低分辨率下的锯齿感。第二块全屏Shader的计算量。逐像素做世界坐标重建和Cascade偏移计算在PC上是小意思但移动端要注意。移动端GPU的ALU吞吐有限建议把重建逻辑尽量简化或者在顶点阶段预处理一部分计算。例如可以先做一张低分辨率的ShadowMask再在目标Shader中用双线性采样插值减少重复计算。第三块全局纹理生命周期的控制。我在脚本里偷懒直接用的RenderTexture私有字段项目退出时没有手动释放。长线项目一定要在OnDisable或Cleanup里调用m_ShadowRT.Release()否则编辑器反复Enter Play Mode时很容易出现内存上涨。另外如果多个相机共用同一个Custom Pass VolumeRT会被多个相机序列覆盖建议要么用一个RT按需更新要么根据ctx.camera为每个相机分配独立的RT。5. 踩坑实录这些问题我挨个排查过这里把我在这套方案里踩过的坑整理成一张速查表每个问题都是我实际遇到过并把原因查明白的。现象可能原因解法阴影RT整屏都是纯白色采样了错误纹理或屏幕UV不对用FrameDebugger确认纹理名称和RT绑定检查UV翻转阴影RT整屏都是纯黑色深度重建失败或Custom Pass注入点太早确认注入点在深度写入之后在Shader中单独输出positionWS检查阴影位置整体偏移世界坐标重建错误或Cascade偏移算错先显示positionWS验证重建再检查GetCascadeShadowCoord阴影边缘闪烁、锯齿RT分辨率太低或filterMode不是Bilinear提高rtResolution把filterMode改为BilinearScene视图正常Game视图不对不同相机的全屏UV约定差异在Shader里做平台判断和相机类型判断或统一用positionCS.xy计算UV后处理开启后偏色Custom Pass注入点选在了后处理之后把注入点改到Before Post Process移动端帧率下降明显全屏Shader计算量过大降低RT分辨率把自定义Shader的逐像素计算简化进Play Mode多次后内存上涨RenderTexture未释放在OnDisable中调用m_ShadowRT.Release()5.1 关于“全屏蓝图”的问题排查这个现象我可太熟了代码跑起来一切正常结果画面呈现出一片统一的蓝色调或者奇怪的渐变颜色。最开始我一直怀疑是Shader写错了后来才发现问题出在物理相机和后处理。HDRP默认有Tone Mapping和Color Grading如果你把Custom Pass的输出直接当成屏幕颜色后处理会把那个灰度值当成普通颜色做映射偏色就是这么来的。解决方法是看你的应用场景。如果只是想拿阴影数据给后续Shader做计算那根本不要把这个结果直接输出到屏幕上写入RT之后Camera的最终图像不会受影响颜色映射问题就无从谈起。如果你确实需要可视化阴影遮罩请把注入点放在After Post Process阶段或者关闭该相机的后处理进行预览。经验之谈在Custom Pass里建立的RT命名建议加下划线前缀例如_MyShadowRT这是Unity全局Shader纹理的命名惯例能避免和你自己材质的局部属性冲突。5.2 关于Scene视图和Game视图表现不一致有一次我发现Custom Pass在Game视图能看到阴影RT的纹理内容但切到Scene视图时全屏什么都没有。查了文档才知道Custom Pass默认只作用于相机的渲染而Scene视图使用的SceneView相机和Game视图的主相机是两个不同的渲染路径注入点也可能不一样。部分Custom Pass的配置会对SceneView也生效但如果你在Custom Pass里用了ctx.camera去做特定相机判断两个视图的行为就会分道扬镳。调试时最简单的方法是在Custom Pass Volume组件里勾选Show Debug或者在脚本里用Debug.Log输出ctx.camera.name确认当前到底跑的哪个相机。如果Scene视图也需要显示要么让Custom Pass对两个相机都执行要么临时在组件上把Current Camera Offset设为0让它以全局方式注入到所有相机。5.3 关于阴影RT在目标Shader里“时灵时不灵”的问题还有一种很恼火的状况你在A材质里采样_MyShadowRT没问题换到B材质里就变成默认值了。这种问题多半是全局纹理绑定时机不对。SetGlobalTexture在HDRP里绑定的是当前CommandBuffer的全局状态如果执行Custom Pass的时机太早目标Shader渲染时该纹理还未被设置就会取到默认的白纹理或者上次残留的旧纹理。我的做法是在每次Execute一开始就调用ctx.cmd.SetGlobalTexture并且把脚本挂在Custom Pass Volume的Global Pass列表里这样每帧都会重新绑定一次。只要你的Custom Pass执行节点在目标Shader渲染之前覆盖关系就是严格有序的。如果还有问题可以在目标Shader的Frag开头先输出一个固定值比如0.5确认Shader本身有没有编译问题和采样路径是否通畅。写在最后这套方案在我的项目里怎么演进的回头再看这个需求其实最关键的一步不是代码怎么写而是想清楚HDRP的阴影系统是怎么组织的。你要拿阴影不能把它当做一个孤立的值而要理解它背后依赖的Shadow Atlas、Cascade层级、屏幕空间纹理等概念。Custom Pass只是一个容器它解决了“在正确的时机用正确的Shader提取数据”这个工程问题。把这两件事打通之后HDRP里获取阴影就不再是玄学而是完全可以掌控的常规操作了。我后来在这个基础组件上扩展了几个变体一个变体是把Custom Pass输出叠加到水面的反射色上模拟大雨天阴影里水面更暗的效果另一个变体是额外采样一张低分辨率的ShadowMap做角色脚下的实时阴影交互用于演出场景里的灯光塑形。每次扩展都发现只要底层把阴影数据管道搭好上层创意几乎是想加就加。如果你在复现过程中遇到HDRP版本差异导致的函数名或纹理名对不上我的建议是先打开FrameDebugger在里面搜“shadow”或者“Shadowmap”看一下当前版本实际创建的纹理叫什么再对照调整代码。这套排查思路比在网上盲目搜某个报错要高效得多。祝你能顺利跑通如果这篇文章帮你少折腾两天那就值得了。
返回列表