
如果你是从内置渲染管线迁到 URP 的老 Unity 开发者应该都经历过这种瞬间翻遍整个工程发现 GrabPass 在 URP 下根本不生效。屏幕扭曲、水面倒影、全局模糊、镜子特效这类效果的实现逻辑都建立在抓取当前屏幕内容之上而 URP 偏偏没有那个一键搞定的 GrabPass。我接手一个带内景环视的数字孪生看板项目时正好撞上这个需求要在 URP 下同时做扭曲、镜子和全局模糊三种效果前前后后折腾了两周。这篇文章把完整的实现方案、代码和踩过的坑都整理出来希望能帮你少走弯路。先说结论URP 下抓取屏幕内容的本质不是去模拟旧管线的 GrabPass而是用 RenderTexture 把相机输出接管过来再交给 Shader 去采样。理解了这个思路后面所有效果都是水到渠成的事。1. 从GrabPass失效说起URP的渲染流程到底改了什么1.1 内置管线里屏幕抓取为什么是一键的在内置渲染管线Built-in Render Pipeline里GrabPass 用起来确实爽。Shader 里写一句GrabPass { _GrabTexture }Unity 就会把当前帧的屏幕内容拷贝到_GrabTexture这张纹理里后续 Pass 直接采样就能拿到屏幕像素。扭曲、流光、倒影全是基于这张纹理做七七八八的 UV 操作。这个机制本质上是一种魔法引擎悄悄帮你做了一次全屏拷贝把渲染目标切换到一张临时纹理上再切换回来。整个过程开发者不用关心什么 RenderTexture、CommandBuffer几条指令就把事办了。但它的问题也很明显——每次 GrabPass 都是一次全屏纹理拷贝这在 PC 上无所谓在移动端就是纯纯的带宽杀手。很多从端游移植到手游的项目第一个砍掉的就是无脑 GrabPass。1.2 URP的SRP架构决定了你得自己接管相机输出URP 全称 Universal Render Pipeline属于 Scriptable Render PipelineSRP。SRP 的核心思想是渲染流程不再由引擎内置的那套固定管线决定而是由 C# 脚本通过 CommandBuffer、ScriptableRenderContext 这些底层 API 一步步拼起来。好处是渲染流程完全可控坏处是你不能再指望引擎帮你擦屁股。URP 环境下旧管线的 GrabPass 直接失效因为整个渲染流程已经变了相机输出的颜色目标由 renderer 管理Shader 里再写 GrabPass 根本不会被解析。这就逼着你去思考更底层的问题——屏幕内容到底存在哪我怎么能拿到它。实际上 URP 自己也有一个类似机制_CameraOpaqueTextureScene Color它会在 Opaque 渲染完成之后抓取一张不透明物体图供透明物体采样做反射、折射效果。但这个是 URP 内部自动管理的材质里直接采样就行。这里有个坑它是不透明物体纹理画面里所有的透明物体和 UI 都不在里面。如果你想抓的是包含透明物体甚至 UI 的完整画面这条路走不通还得自己动手。1.3 两条可行路线整机输出RT vs 注入式抓屏PassURP 下自己抓屏幕主流做法有两条第一条是整机输出 RT把相机渲染的目标直接指定到一张 RenderTexture相机不直接显示到屏幕再把这张 RT 内容 Blit 到屏幕上。听起来绕但这是最直观的思路。第二条是注入式抓屏 Pass写一个自定义ScriptableRendererFeature和ScriptableRenderPass在 URP 渲染流程的某个注入点比如 Opaque 渲染完之后、Post-processing 之前用 CommandBuffer 把当前相机颜色目标拷贝到一张 RT。这种方式更接近抓屏的本意因为渲染主流程没被破坏UI、后处理都还在正常走。两条路我在项目里都试过。如果你的抓屏需求很简单比如整个画面就是要作为一张纹理显示在 UI 上做画中画方案一足够了。但如果你要做的是当前游戏画面保持正常显示额外抓一份做扭曲/模糊那必须走方案二。我最终项目中用的是第二套后面展开讲。2. 抓屏底座用RenderTexture把相机画面接管出来2.1 最省事的整相机输出到RT方案先说最简单的。把相机输出到 RT代码量很少public class ScreenToRT : MonoBehaviour { public RenderTexture grabRT; public Camera targetCamera; void OnEnable() { if (grabRT null) { grabRT new RenderTexture(targetCamera.pixelWidth, targetCamera.pixelHeight, 24); grabRT.name GrabRT; } targetCamera.targetTexture grabRT; } void OnDisable() { targetCamera.targetTexture null; if (grabRT ! null) { grabRT.Release(); Destroy(grabRT); } } }问题来了相机一旦设置了 targetTexture画面上就看不到东西了。你得把 RT 再 Blit 回屏幕。URP 里通常在某个自定义 Pass 的 Execute 里做Blit(cmd, grabRT, cameraColorTarget)。如果你不想写 Pass可以在 UI 上放一个 RawImage把 RT 显示出来但这样 UI 层就盖住了场景不适合常态使用。这个方案适合把场景当纹理用的场景比如画中画、分屏、数字孪生看板里的内景环视窗口。但它最大的限制是相机渲染到 RT 时后处理、UI 渲染、透明队列的最终合成结果你是拿不到的抓到的只是这个相机自己渲染的 3D 场景。2.2 注入式ScriptableRenderPass抓屏方案推荐我更推荐用自定义 Pass 在渲染流程里抓屏。先注册一个 Feature 到 URP Renderer Data 上然后在 Render Pass 里申请 RT、拷贝颜色、释放 RT。以 URP 14Unity 2022为例核心代码如下public class GrabScreenFeature : ScriptableRendererFeature { private GrabScreenPass grabPass; public override void Create() { grabPass new GrabScreenPass(); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { // 在渲染流程中插入这个 Pass renderer.EnqueuePass(grabPass); } } public class GrabScreenPass : ScriptableRenderPass { private RTHandle grabRT; private Material blitMaterial; public GrabScreenPass() { renderPassEvent RenderPassEvent.AfterRenderingOpaques; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { var desc renderingData.cameraData.cameraTargetDescriptor; desc.depthBufferBits 0; RenderingUtils.ReAllocateHandleIfNeeded(ref grabRT, desc, name: _GrabScreenTex); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd CommandBufferPool.Get(); var source renderingData.cameraData.renderer.cameraColorTargetHandle; Blit(cmd, source, grabRT); context.ExecuteCommandBuffer(cmd); cmd.Clear(); CommandBufferPool.Release(cmd); } public void Release() { grabRT?.Release(); } }这里有几个注意点。renderPassEvent选择了AfterRenderingOpaques也就是不透明物体渲染完之后这样抓到的画面包含大部分场景但又是在透明物体和 UI 之前。如果你需要抓到 UI得把事件改成AfterRenderingPostProcessing或者AfterRendering后面细讲。另一个关键是RenderingUtils.ReAllocateHandleIfNeeded它会自动根据相机的目标分辨率重新分配 RT相机分辨率变了也不用你手动去处理。抓完之后Shader 里可以直接声明_GrabScreenTex来采样。URP 的 Shader 里写法跟普通贴图没什么区别Texture2D _GrabScreenTex; SamplerState sampler_GrabScreenTex; float4 _GrabScreenTex_TexelSize;2.3 抓屏RT的分辨率管理与多相机场景RT 分辨率是个容易被忽略的细节。在 PC 上 1080p 随便抓但如果你正开发 WebGL 版本或者手机版本一张全屏 RT 的内存开销就是width * height * 4字节也就是 1920 * 1080 的 RGBA8 纹理占 8.3MB 显存。分辨率一定要跟着相机的 pixelWidth/pixelHeight 走避免写死否则窗口尺寸一变画面就糊或拉伸。我实际项目里主相机后面还挂了多个相机数字孪生项目的多个角度视口这时候要注意每个相机会单独走一遍渲染流程如果你在OnCameraSetup里分配 RT多相机情况下会反复重新分配。解决办法是判断renderingData.cameraData.camera是不是你要抓的那台相机或者在 Feature 里做一次开关控制。3. 屏幕扭曲噪声贴图驱动UV偏移的完整配方3.1 原理所有扭曲都是对UV做文章拿到_GrabScreenTex之后做扭曲的原理一句话能讲完采样屏幕纹理时不要让 UV 老老实实按原坐标走而是给它加上一个偏移量偏移量由噪声函数决定。这样画面里每个像素取到的都是旁边某个位置的颜色整体看起来就像水波一样扭起来。偏移的方向和大小不能是固定值否则整个画面只是平移了一下。要用连续变化的噪声作为偏移噪声的值在空间上连续扭曲看起来才自然。最常用的是 Perlin 噪声或 Simple Noise 那样的渐变色块贴图采样噪声纹理得到二维向量加到原始 UV 上。为什么不直接在 Shader 里用数学函数生成噪声比如sin(uv.x * freq) * cos(uv.y * freq)。能跑但数学噪声的图案太规律一眼就能看出是三角函数在晃动水波感很差。采样一张噪声贴图则要自然得多而且噪声贴图可以预处理得比较节省Shader 里只是两次采样和一次插值移动端压力很小。3.2 可复用的URP扭曲Shader核心代码URP 的 Shader 推荐用 HLSL 写。以全屏后处理材质为例核心代码长这样Shader Custom/ScreenDistort { Properties { _NoiseTex (Noise Texture, 2D) white {} _Strength (Distort Strength, Range(0, 0.1)) 0.02 _Speed (Distort Speed, Range(0, 5)) 1.0 _Freq (Noise Frequency, Range(1, 50)) 10.0 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Cull Off ZWrite Off ZTest Always Pass { HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_GrabScreenTex); SAMPLER(sampler_GrabScreenTex); TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex); CBUFFER_START(UnityPerMaterial) float _Strength; float _Speed; float _Freq; CBUFFER_END Varyings Vert(Attributes input) { Varyings output; output.positionHCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } half4 Frag(Varyings input) : SV_Target { // 采样噪声贴图得到两个方向的偏移量 float2 noise SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, input.uv * _Freq).rg; // 用时间驱动噪声滚动 float2 offsetDir noise - 0.5; float2 distortedUV input.uv offsetDir * _Strength; distortedUV.y _Time.y * _Speed * 0.05; half4 color SAMPLE_TEXTURE2D(_GrabScreenTex, sampler_GrabScreenTex, distortedUV); return color; } ENDHLSL } } }这个 Shader 有两个地方值得解释。一个是_NoiseTex的采样 uv 乘了_Freq相当于把噪声贴图平铺多份让扭曲的细节更丰富不然整块屏就一个大波浪。另一个是offsetDir noise - 0.5因为噪声纹理的采样结果在 0~1 之间减去 0.5 才能变成正负偏移不然扭曲只会往一个方向偏。3.3 让扭曲活起来的细节时间驱动与强度控制光有静态扭曲是不够的水波、热浪都是动态的。用_Time.y去驱动噪声贴图 UV 的滚动能够让扭曲图案持续流动起来。但滚动的速度不能太快实际项目里_Speed给到 0.3~1.0 之间比较自然太快了像开关干扰信号太慢了像隔着脏玻璃看东西。强度控制也是经验活。_Strength的单位是 UV 坐标的偏移量0.01 的偏移在 1080p 屏幕上大概是 10 个像素的错位视觉上已经很明显。建议把这个强度做成可以在运行期动态调整的公开参数比如做受击模糊失真时从 0.002 缓动到 0.02再缓动回来效果比固定值强得多。还有一个很容易忽略的细节扭曲边界。如果扭曲效果作用在整个屏幕上画面边缘会露出 RT 采样不到的区域表现为拉伸或者黑边。最简单的处理是在边缘加 fade用 uv 离边界的距离做一个 smoothstep 淡出把扭曲强度压到边缘为 0。4. 镜子效果UV翻转、视差与边缘过渡缺一不可4.1 伪镜子思路屏幕内容当作镜面反射先声明这里说的镜子不是那种摆一个镜面模型、用反射矩阵重新渲染场景的真镜子。真镜子要做 Reflection Probe 或者二次相机渲染开销大得多。如果你的场景里只有一块平整的镜面区域我用的方案是伪镜子把屏幕内容翻转一下贴到指定区域模拟镜面反射感。具体做法很粗暴正常情况下屏幕 UV 的 v 坐标是从下往上以纹理坐标系为准但镜面区域我们想要的效果是越靠近镜面上沿反射的是越靠下的场景内容所以直接把 v 翻转即float2 mirrorUV float2(uv.x, 1.0 - uv.y);。这样整个屏幕内容上下颠倒地贴到镜子面上看起来就有反射那味了。但这种伪镜子有个天然限制它反射的是屏幕外第三人称视角的画面不是真正的平面镜反射。如果镜头不能移动静态场景里效果过得去一旦镜头大幅度旋转镜面里的内容跟真实反射对不上。所以这个方案适合数字孪生看板、UI 装饰、水面倒影这类对物理真实性要求不高的场景。4.2 平台纹理坐标方向与翻转的坑镜像效果最坑的一个问题纹理坐标方向在平台之间不一致。在 DirectX 上纹理的 v 坐标原点在左上角在 OpenGL 平台上v 坐标原点在左下角。URP 内部做了很多翻转处理但你自己写 Pass 拷贝 RT 时这个差异可能会让镜像上下颠倒在某个平台失效。最稳妥的判断方式是用_ProjectionParams.xUnity 内置变量它在 DirectX 类平台是 -1在 OpenGL 类平台是 1。采样前做一个宏判断float2 uv input.uv; #if UNITY_UV_STARTS_AT_TOP if (_ProjectionParams.x 0) uv.y 1.0 - uv.y; #endif float2 mirrorUV float2(uv.x, 1.0 - uv.y);这段话翻译成大白话如果当前平台纹理方向反了先翻回来再做镜像的 v 翻转。这样不管在 PC、编辑器还是手机上镜子里看到的方向才是一致的。我在项目里就是漏了这步编辑器里调好效果发到 Android 测试机上上下颠倒排查了半天才想起来是平台差异。4.3 用视差和边缘过渡提升镜子质感纯 UV 翻转的镜子很假因为它没有任何立体感。可以加一个视差效果在采样镜像 RT 时根据镜面物体表面的法线方向或者一个简单的正弦偏移让不同位置的采样 UV 产生一个轻微的偏移。原理跟凹凸贴图类似虽然没真正改变几何但画面会出现镜面有曲率的错觉。伪镜子的另一个问题是边缘太硬。长方形镜面区域的边缘如果直接裁切过渡会很难看。可以基于 uv 计算边缘距离做透明渐变float edgeFade smoothstep(0.0, 0.08, uv.x) * smoothstep(0.0, 0.08, uv.y) * smoothstep(1.0, 0.92, uv.x) * smoothstep(1.0, 0.92, uv.y); return half4(color.rgb, edgeFade);这种处理在圆形镜面装饰汽车后视镜水面反光区域上都适用。另外提醒一句如果你需要在镜子里看到角色或者 UI抓屏的时间点要选在AfterRenderingTransparents之后否则镜子里只有不透明场景人物会消失。5. 全局模糊多级降采样双Pass模糊的正确姿势5.1 为什么不能靠加大采样半径硬刚新手做模糊最容易踩的坑就是想靠加大采样半径来提升模糊程度在 Shader 里写一个 9x9 或者更大的卷积一个像素要采样 81 次纹理。在低分辨率 UI 上还行一张 1080p 全屏贴图做 9x9 卷积移动端直接掉到十几帧PC 上 GPU 负载也不小。而且采样次数多了以后模糊效果并不会均匀变好反而容易产生栅栏状条纹。业界更通用的做法是多级降采样 双 Pass 模糊核心思想很朴素第一次把全屏 RT 缩小到 1/2 或 1/4这样后续每个采样点覆盖的屏幕区域变大模糊半径等效变大在低分辨率下做 2~3 轮模糊因为像素少采样开销急剧下降最后再把模糊结果放回全屏跟原图做插值混合。这样算下来模糊半径等效能达到几十个像素而采样次数只有高分辨率单 Pass 卷积的零头。5.2 Kawase模糊与降采样配合的落地流程Kawase Blur 是一个性能很好的模糊算法每个 Pass 只采样 4 次配合降采样循环使用效果接近高斯模糊但便宜得多。核心 Shader 片段half4 KawaseBlur(float2 uv, float2 texelSize, float offset) { float2 o texelSize * offset; half4 col 0; col SAMPLE_TEXTURE2D(_GrabScreenTex, sampler_GrabScreenTex, uv float2(-o.x, -o.y)); col SAMPLE_TEXTURE2D(_GrabScreenTex, sampler_GrabScreenTex, uv float2( o.x, -o.y)); col SAMPLE_TEXTURE2D(_GrabScreenTex, sampler_GrabScreenTex, uv float2(-o.x, o.y)); col SAMPLE_TEXTURE2D(_GrabScreenTex, sampler_GrabScreenTex, uv float2( o.x, o.y)); return col * 0.25; }配合 C# 侧的循环 Blit 才能跑出全局模糊public class BlurProcessor { private RenderTexture _rt1, _rt2; private Material _blurMat; public RenderTexture Process(RenderTexture source) { int w source.width / 2; int h source.height / 2; _rt1 RenderTexture.GetTemporary(w, h, 0); _rt2 RenderTexture.GetTemporary(w, h, 0); // 降采样一次 Graphics.Blit(source, _rt1); for (int i 0; i 3; i) { _blurMat.SetFloat(_Offset, 1f i); Graphics.Blit(_rt1, _rt2, _blurMat); ( _rt1, _rt2 ) ( _rt2, _rt1 ); } RenderTexture result RenderTexture.GetTemporary(source.width, source.height, 0); // 上采样回原尺寸 Graphics.Blit(_rt1, result, _blurMat); return result; } }这里要注意_Offset参数。Kawase 模糊的效果跟这个值强相关每次循环逐步增大 offset可以让模糊从局部细节模糊过渡到全局大面积模糊。如果全程固定一个值模糊效果会停留在某个中间状态看起来像失焦而不是高斯糊。另外在 URP 的 Render Pass 里做 Blur 时建议用 CommandBuffer 的GetTemporaryRT和Blit来管理中间 RT避免每帧用RenderTexture.GetTemporary在 GC 上留下垃圾。5.3 模糊RT的分配与释放全局模糊用的 RT 不止一张如果你的项目对内存敏感一定要用RenderTexture.GetTemporary/ReleaseTemporary的配对而不是直接 new RenderTexture。临时 RT 在内部是池化的可以被复用反复创建销毁反而可能触发 GC 和渲染目标切换。实践中最舒服的用法进入模糊状态时才申请 RT用完立刻释放。像弹窗打开时背景模糊暂停界面模糊这类场景并不需要每帧持续做模糊只在状态切换时渲染一帧模糊图就够了。我做那个看板项目时就是进入设置面板时生成一张模糊背景图作为 UI 的底图常驻关闭面板时再释放。这样模糊流程只在打开瞬间发生一次性能开销对帧率没有任何压力。另一个经验模糊中间过程可以固定在 1/4 甚至 1/8 分辨率只要最后上采样回原屏视觉上差异不大但性能和内存会好非常多。1080p 降到 480x270 做模糊三张中间 RT 加起来还没一帧全屏 RT 的一半大。6. URP抓屏的实测账单性能、版本与平台差异6.1 性能与显存一张表看懂开销抓屏 后处理效果最核心的性能成本就是 RT 的显存和带宽。我实际项目在 1080p 下测过一组数据资源分辨率格式显存占用说明全屏抓屏RT1920x1080RGBA8约 8.3 MB抓屏基础开销全屏抓屏RT1920x1080ARGBHalf约 16.6 MB开启 HDR 时默认可能翻倍1/2 降采样RT960x540RGBA8约 2.1 MB做模糊的中间层1/4 降采样RT480x270RGBA8约 0.5 MB模糊的最底层三张模糊RT合计--约 2.6 MB循环复用后内存可控全屏 RGBA8 的 8.3MB 看起来不吓人但在移动端带宽受限的设备上一次全屏拷贝可能占掉一轮渲染预算的 10% 以上。如果你的项目目标平台是手机强烈建议抓屏分辨率直接用半分辨率也就是 960x540然后用双线性过滤放大。扭曲和模糊这类效果本身就要做像素偏移半分辨率带来的细节损失几乎不可感知但带宽直接砍掉 75%。还有个大坑项目开了 HDR 时cameraTargetDescriptor默认的格式可能是 ARGBHalf全屏 RT 直接翻倍到 16.6MB。如果扭曲、镜子这些效果不需要 HDR 精度可以在分配 RT 时手动指定RenderTextureFormat.ARGB32前提是你的画面后续不需要处理大于 1 的亮度。模糊效果反过来最好在 HDR 空间做否则高亮部分会糊成一团灰色。6.2 URP版本变化从ScriptableRenderPass到RTHandle如果你的项目用的 URP 不是 14代码可能要做不少调整。URP 12Unity 2021里OnCameraSetup的参数签名是(CommandBuffer cmd, ref RenderingData renderingData)渲染目标用RenderTargetIdentifier。URP 14 之后引入了 RTHandle 机制相机的颜色目标变成了cameraColorTargetHandle如果你拿老代码硬套编译直接报错。版本差异最烦人的是 API 改名和参数变化但思路完全一致。给你一个避坑建议Unity 官方升级工具经常会把老的m_ActiveRenderPass改坏升级 URP 后优先去UniversalRenderer相关的类里检查编译错误别等运行时报空引用才排查。另外RenderPassEvent的注入时机也是经验活。抓屏抓早了UI 和后处理都还没画镜子里的图像缺东西抓晚了又可能出现抓到的画面其实已经被当前 Pass 自己污染的情况。我的调试点位原则是扭曲/热浪AfterRenderingOpaques让扭曲粒子参与场景渲染适合全屏扭曲镜子AfterRenderingTransparents镜面物体本身也是透明物体抓屏要包含场景里所有东西自然要在透明渲染之后全局模糊BeforeRenderingPostProcessing模糊应该作用于后处理之前的画面否则后处理本身的辉光、色调映射会跟模糊互相干扰。6.3 WebGL/移动端/XR项目的特殊处理这几个端都有各自的坑我按类型说一下。WebGL 项目发布到浏览器最常见的问题是 RenderTexture 格式支持不完整。部分浏览器不支持 R16G16B16A16_SFLOAT 这类格式抓屏 RT 直接变黑或者花屏。WebGL 下最稳的格式就是 ARGB32 或者 RGBA8HDR 管线在 WebGL 2.0 下也建议关闭省得后续还要做兼容判断。移动端的麻烦主要在分辨率适配和纹理坐标上。手机分辨率五花八门有些安卓机在全面屏下camera.pixelWidth在极端场景可能跟屏幕实际渲染分辨率不一致导致抓屏画面拉伸。建议以renderingData.cameraData.cameraTargetDescriptor.width/height为准分配 RT不要直接用Screen.width。另外做边缘淡出时不同宽高比下同一个 uv 边缘阈值效果会不一致需要按比例换算。XR 项目比如 PICO 这类一体机是最容易翻车的。VR 渲染时相机是双目标渲染左眼右眼各自渲染到纹理阵列你写一个普通的Blit(source, grabRT)抓到的可能只是单眼的视图而且分辨率减半。我在一个 Mobile VR 项目里就在这上面栽过跟头——抓屏 RT 只能看到半张画面后来查文档才知道 XR 下要特殊处理。如果你做的 Unity 版本支持 XR建议直接在代码里判断XRSettings.enabled必要时禁用抓屏后处理或者用RenderTextureDescriptor.vrUsage标记 RT 为 XR 专用。还有一个小技巧调试这类效果强烈推荐用 Frame Debugger打开 Window Analysis Frame Debugger逐帧查看每个 Draw Call 的 RT 切换情况能很直观地看到你的抓屏 Pass 在哪个环节拷贝了画面、有没有多余的 RT 分配。我排查 WebGL 黑屏问题时就是靠 Frame Debugger 确认了 RT 格式不受支持。另外内存层面用 Profiler 的 Memory Profiler 模块搜 RenderTexture 关键字能看到每个 RT 的分辨率、格式和占用那些被反复申请没释放的 RT 一眼就能揪出来。最后说一个个人体会折腾完这套抓屏方案后我对 URP 的项目架构理解深了一层。URP 没有 GrabPass 不是功能缺失而是它把抓屏幕这个需求还原成了管好渲染目标这个更底层的操作。只要掌握了 RenderTexture ScriptableRenderPass 这套组合拳不只是扭曲、镜子、模糊色散、扫描线、像素化、局部高亮这些效果本质上都是同一套思路的排列组合。尤其是后面再碰到URP 怎么做毛玻璃URP 怎么做屏幕碎裂这类问题先别急着搜插件回到我能不能在某个渲染节点把画面复制一份出来这个出发点去思考方案自然就有了。