深入解析_CameraOpaqueTexture与ScriptableRendererFeature 从渲染管线原理到三种经典屏幕空间特效的完整实现路径。1为什么需要抓取屏幕许多视觉特效并非简单地叠加一张贴图而是依赖当前帧已渲染完成的像素作为输入。扭曲效果需要对背后场景的颜色进行偏移采样 镜子反射需要读取来自另一视角的渲染结果 全局模糊如景深、玻璃磨砂则需要对整块屏幕区域进行滤波。这三类需求共同的关键词是屏幕空间读取Screen-Space Sampling。 在传统 Built-in 管线中这通过GrabPass实现每次调用都会阻塞 GPU 并 复制一次完整的帧缓冲代价极高。URP 提供了更高效的替代方案—— 在不增加额外 DrawCall 的前提下将屏幕内容保留为可采样纹理。热浪扭曲利用法线贴图或噪声偏移 UV对屏幕颜色纹理进行扭曲采样模拟高温气流折射。平面镜反射通过第二台相机渲染翻转视角将结果写入 RenderTexture再在材质中投影采样。全局模糊使用 ScriptableRendererFeature 在不透明通道后进行多次 Blit 实现 Kawase / Gaussian 模糊。2URP 如何暴露屏幕纹理URP 为我们提供了两个内置的屏幕纹理无需手动 GrabPass纹理名称包含内容何时可用典型用途_CameraOpaqueTexture不透明通道渲染结果颜色Transparent Pass 期间扭曲、折射、磨砂玻璃_CameraDepthTexture场景深度值Transparent Pass 期间软粒子、景深、边缘光重要需要在 URP Asset 中开启路径Project Settings → Graphics → URP Asset → Rendering → Opaque Texture 勾选后 URP 才会在不透明通道后自动执行一次内部 CopyColor Pass 将结果写入_CameraOpaqueTexture。在 Shader 中声明与采样在任意使用 URP 的 HLSL Shader 里通过以下方式声明并采样屏幕纹理// 引入 URP 核心库必须 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl // 上面那个 include 已帮你声明了 _CameraOpaqueTexture 和 sampler // 若手动声明TEXTURE2D(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture); half4 frag(Varyings IN) : SV_Target { // 将顶点齐次坐标转换为屏幕 UV (0~1) float2 screenUV IN.positionHCS.xy / IN.positionHCS.w; screenUV screenUV * 0.5 0.5; // NDC → [0,1] #if UNITY_UV_STARTS_AT_TOP screenUV.y 1.0 - screenUV.y; // D3D 平台翻转 Y #endif half3 sceneColor SampleSceneColor(screenUV); return half4(sceneColor, 1.0); } 推荐使用 SampleSceneColor()DeclareOpaqueTexture.hlsl提供了封装函数SampleSceneColor(uv) 它内部处理了平台差异GL/DX UV 方向推荐优先使用它而非手动 SAMPLE_TEXTURE2D。3项目基础配置在开始任何效果之前确认以下项目设置已就绪步骤 A开启 Opaque TextureEdit → Project Settings → Graphics └─ Scriptable Render Pipeline Settings → [你的 URP Asset] └─ Rendering → Opaque Texture ✓ 勾选 └─ Depth Texture ✓ 勾选景深/软粒子可选步骤 B透明材质 Queue 设置使用屏幕抓取纹理的材质必须处于Transparent渲染队列Queue ≥ 3000。 若材质设为 Opaque会在不透明通道中渲染此时_CameraOpaqueTexture尚未生成采样结果将为黑色。SubShader { Tags { RenderType Transparent Queue Transparent // 确保在 Opaque Texture 生成后渲染 RenderPipeline UniversalPipeline } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha }效果一热浪扭曲Heatwave Distortion热浪效果的原理是使用一张流动的法线贴图Normal Map或噪声纹理 计算出 UV 偏移量再以偏移后的坐标采样_CameraOpaqueTexture 使背景画面产生动态的波浪折射感。Shader Custom/URP/HeatwaveDistortion { Properties { _NoiseTex (Distortion Noise, 2D) bump {} _Strength (Distortion Strength, Range(0,0.05)) 0.01 _Speed (Flow Speed, Range(0,2)) 0.4 _Tiling (Noise Tiling, Float) 3.0 } SubShader { Tags { RenderTypeTransparent QueueTransparent RenderPipelineUniversalPipeline } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Pass { Name HeatwavePass HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl CBUFFER_START(UnityPerMaterial) TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex); float4 _NoiseTex_ST; float _Strength, _Speed, _Tiling; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float4 screenPos : TEXCOORD0; float2 uv : TEXCOORD1; }; Varyings vert(Attributes IN) { Varyings OUT; OUT.positionHCS TransformObjectToHClip(IN.positionOS.xyz); OUT.screenPos ComputeScreenPos(OUT.positionHCS); // 关键屏幕坐标 OUT.uv IN.uv * _Tiling; return OUT; } half4 frag(Varyings IN) : SV_Target { // 1. 流动噪声 UV float2 noiseUV IN.uv float2(0, _Time.y * _Speed); half2 noise SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).rg; half2 offset (noise - 0.5) * 2.0 * _Strength; // 2. 计算屏幕 UV 并施加偏移 float2 screenUV IN.screenPos.xy / IN.screenPos.w offset; // 3. 采样屏幕颜色 half3 col SampleSceneColor(screenUV); return half4(col, 1.0); } ENDHLSL } } }⚠️ ComputeScreenPos vs positionHCS直接用positionHCS.xy / positionHCS.w在 OpenGL 和 Direct3D 上 Y 轴方向不同。ComputeScreenPos()内部已处理该差异 强烈建议使用它而非手算。5效果二平面镜反射Planar Mirror平面镜反射需要一个镜像视角——以反射平面通常是 Y0为对称轴 将主相机翻转后进行渲染结果写入RenderTexture 再在镜面材质中以投影坐标采样该纹理。using UnityEngine; using UnityEngine.Rendering.Universal; public class PlanarMirror : MonoBehaviour { public Camera mainCamera; public Material mirrorMaterial; // 镜面使用的材质 public int rtSize 512; // RT 分辨率 public LayerMask reflectionLayers; RenderTexture _rt; Camera _reflCam; static readonly int ReflTex Shader.PropertyToID(_ReflectionTex); void Awake() { _rt new RenderTexture(rtSize, rtSize, 16, RenderTextureFormat.Default); _rt.antiAliasing 2; // 创建隐藏的反射相机 var go new GameObject([ReflectionCamera]); go.hideFlags HideFlags.HideAndDontSave; _reflCam go.AddComponentCamera(); _reflCam.targetTexture _rt; _reflCam.cullingMask reflectionLayers; _reflCam.enabled false; // 手动控制渲染时机 mirrorMaterial.SetTexture(ReflTex, _rt); } void OnWillRenderObject() { var reflMatrix CalculateReflectionMatrix(transform.up, transform.position); _reflCam.worldToCameraMatrix mainCamera.worldToCameraMatrix * reflMatrix; // 斜裁剪面消除镜面以下的伪影 var clipPlane CameraSpacePlane(_reflCam, transform.position, transform.up, 1f); _reflCam.projectionMatrix mainCamera.CalculateObliqueMatrix(clipPlane); _reflCam.Render(); } static Matrix4x4 CalculateReflectionMatrix(Vector3 normal, Vector3 pos) { float d -Vector3.Dot(normal, pos); var m Matrix4x4.identity; m.m00 1 - 2*normal.x*normal.x; m.m01 -2*normal.x*normal.y; m.m02 -2*normal.x*normal.z; m.m03 -2*d*normal.x; m.m10 -2*normal.x*normal.y; m.m11 1 - 2*normal.y*normal.y; m.m12 -2*normal.y*normal.z; m.m13 -2*d*normal.y; m.m20 -2*normal.x*normal.z; m.m21 -2*normal.y*normal.z; m.m22 1 - 2*normal.z*normal.z; m.m23 -2*d*normal.z; return m; } static Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign) { Matrix4x4 wtoc cam.worldToCameraMatrix; Vector3 cpos wtoc.MultiplyPoint(pos); Vector3 cnrm wtoc.MultiplyVector(normal).normalized * sideSign; return new Vector4(cnrm.x, cnrm.y, cnrm.z, -Vector3.Dot(cpos, cnrm)); } void OnDestroy() { if (_rt) _rt.Release(); if (_reflCam) Destroy(_reflCam.gameObject); } }镜面 Shader投影采样TEXTURE2D(_ReflectionTex); SAMPLER(sampler_ReflectionTex); float _Roughness; // 模糊程度粗糙镜面 float _Fresnel; // 菲涅尔强度 half4 frag(Varyings IN) : SV_Target { // 投影坐标将齐次裁剪坐标转为 [0,1] UV float2 projUV IN.screenPos.xy / IN.screenPos.w; // 粗糙度用 mip 级别模拟反射模糊 half3 refl SAMPLE_TEXTURE2D_LOD( _ReflectionTex, sampler_ReflectionTex, projUV, _Roughness * 5.0).rgb; // 菲涅尔视线越平反射越强 half3 viewDir normalize(IN.viewDirWS); half fresnel pow(1.0 - saturate(dot(viewDir, IN.normalWS)), _Fresnel); return half4(refl * fresnel, fresnel); }6效果三全局模糊Global Blur全局模糊如磨砂玻璃、UI 背景虚化需要对整个屏幕区域进行滤波 单次采样附近像素代价高昂通常用双通道分离高斯Two-pass Gaussian或Kawase 模糊多次 Blit每次偏移半像素来达成高质量低代价的效果。在 URP 中最佳实践是通过ScriptableRendererFeature注入自定义 Pass 将模糊结果写入全局纹理供后续 Pass 或材质使用。ScriptableRendererFeature 注册 Blur Passusing UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class GlobalBlurFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public RenderPassEvent renderPassEvent RenderPassEvent.AfterRenderingTransparents; [Range(1, 8)] public int iterations 4; // Kawase 迭代次数 [Range(1, 8)] public int downSample 2; // 降采样倍数性能关键 public string globalTexName _GlobalBlurTex; } public Settings settings new Settings(); KawaseBlurPass _blurPass; public override void Create() { _blurPass new KawaseBlurPass(settings); _blurPass.renderPassEvent settings.renderPassEvent; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(_blurPass); } }KawaseBlurPass 核心逻辑class KawaseBlurPass : ScriptableRenderPass { readonly GlobalBlurFeature.Settings _settings; Material _blurMat; RTHandle _buf0, _buf1; // 双缓冲 ping-pong static readonly int OffsetPID Shader.PropertyToID(_Offset); static readonly int GlobalBlur Shader.PropertyToID(_GlobalBlurTex); public KawaseBlurPass(GlobalBlurFeature.Settings s) { _settings s; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData rd) { var desc rd.cameraData.cameraTargetDescriptor; desc.width / _settings.downSample; desc.height / _settings.downSample; desc.depthBufferBits 0; RenderingUtils.ReAllocateIfNeeded(ref _buf0, desc, FilterMode.Bilinear, name: _BlurBuf0); RenderingUtils.ReAllocateIfNeeded(ref _buf1, desc, FilterMode.Bilinear, name: _BlurBuf1); } public override void Execute(ScriptableRenderContext ctx, ref RenderingData rd) { if (_blurMat null) _blurMat CoreUtils.CreateEngineMaterial(Hidden/KawaseBlur); CommandBuffer cmd CommandBufferPool.Get(KawaseBlur); using (new ProfilingScope(cmd, new ProfilingSampler(KawaseBlur))) { // 第一步降采样复制到 buf0 Blitter.BlitCameraTexture(cmd, rd.cameraData.renderer.cameraColorTargetHandle, _buf0); // Kawase 多次 ping-pong blit RTHandle src _buf0, dst _buf1; for (int i 0; i _settings.iterations; i) { _blurMat.SetFloat(OffsetPID, i 0.5f); // Kawase 核心偏移 Blitter.BlitCameraTexture(cmd, src, dst, _blurMat, 0); (src, dst) (dst, src); // swap } // 将结果设为全局纹理供场景中任意材质采样 cmd.SetGlobalTexture(GlobalBlur, src); } ctx.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }Kawase 模糊 Shader 核心float _Offset; float4 _BlitTexture_TexelSize; // URP Blit 内置(1/w, 1/h, w, h) half4 frag(Varyings IN) : SV_Target { float2 uv IN.texcoord; float2 ts _BlitTexture_TexelSize.xy * _Offset; // Kawase对角四点采样取平均 half4 col SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv float2(-ts.x, -ts.y)); col SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv float2( ts.x, -ts.y)); col SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv float2(-ts.x, ts.y)); col SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv float2( ts.x, ts.y)); return col * 0.25; }7性能注意事项方案额外 DrawCall带宽压力移动端友好推荐场景_CameraOpaqueTexture扭曲1 CopyColor Pass低✓实时扭曲、折射平面镜独立相机整个场景 ×2高✗ 慎用PC/主机镜面数量 ≤ 2Kawase Blur降采样×2iterations 次 Blit中✓UI 磨砂、景深前景 降采样是模糊性能的核心杠杆将 RT 缩小为 1/2downSample2后像素数量减少 75% 配合 Bilinear 采样视觉质量几乎无损但 Blit 耗时大幅下降。 移动端建议 downSample4、iterations3。⚠️ 避免每帧 GrabPass在 Built-in 管线中GrabPass {}会在该帧对应位置强制 Blit 整个帧缓冲 多个材质使用时会触发多次GPU stall 风险极高。迁移到 URP 后一律使用_CameraOpaqueTexture或自定义 RendererFeature。8总结URP 通过_CameraOpaqueTexture和ScriptableRendererFeature提供了完整的屏幕内容访问机制无需再依赖性能低下的 GrabPass。