URP 渲染实战 / 第 29 章 性能优化 / 案例 29.5
案例 29.5Overdraw 热度图 Renderer Feature进阶
目标:写一个 Feature,把场景里每个像素被绘制的次数画成热度图,直观地找出 Overdraw 热点。
思路:用 DrawingSettings.overrideMaterial 把所有可见物体用一个“计数材质”重新画一遍到一张浮点 RT 上。计数材质用加法混合(Blend One One),每个片元输出 1,最终每个像素的值就是它被画了几层。最后用一个全屏 Pass 把计数映射成颜色。
Assets/Cookbook/Ch29/OverdrawCount.shader(节选:state)
Assets/Cookbook/Ch29/OverdrawFeature.cs(节选:execute)
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, profilingSampler)) { // ConfigureTarget/ConfigureClear 已经把 m_Count 设为目标并清成 0 context.ExecuteCommandBuffer(cmd); cmd.Clear(); if (m_Settings.opaque) Draw(context, ref renderingData, RenderQueueRange.opaque, SortingCriteria.CommonOpaque); if (m_Settings.transparent) Draw(context, ref renderingData, RenderQueueRange.transparent, SortingCriteria.CommonTransparent); Blitter.BlitCameraTexture(cmd, m_Count, m_CameraColor, m_Settings.heatmapMaterial, 0); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } void Draw(ScriptableRenderContext context, ref RenderingData renderingData, RenderQueueRange range, SortingCriteria sort) { var drawing = CreateDrawingSettings(s_Tags, ref renderingData, sort); drawing.overrideMaterial = m_Settings.countMaterial; // 所有物体都用计数材质画 drawing.overrideMaterialPassIndex = 0; var filtering = new FilteringSettings(range, m_Settings.layerMask); context.DrawRenderers(renderingData.cullResults, ref drawing, ref filtering); }
// 案例 29.5:Overdraw 可视化 Renderer Feature 的网页版(对应 Ch29/OverdrawFeature.cs)。 // 用 overrideMaterial 把可见物体全部重画一遍到一张 R16 浮点 RT(每层 +1),再用热度图画回相机颜色。 import { RenderPassEvent, featureMaterial, layerMask } from '../engine/features.js'; // 和 URP 自己画不透明/透明物体时用的 LightMode 列表一致 const TAGS = ['UniversalForward', 'UniversalForwardOnly', 'SRPDefaultUnlit', 'LightweightForward']; export default class OverdrawFeature { constructor(engine, data, scene) { this.engine = engine; this.scene = scene; const s = (data && data.settings) || {}; this.settings = { opaque: true, transparent: true, layerMask: -1, ...s }; } async init() { const s = this.settings; this.countMaterial = featureMaterial(this.scene, s.countMaterial); // Cookbook/Ch29/OverdrawCount this.heatmapMaterial = featureMaterial(this.scene, s.heatmapMaterial); // Cookbook/Ch29/OverdrawHeatmap } addPasses(frame, enqueue) { const s = this.settings; if (!this.countMaterial || !this.heatmapMaterial) return; enqueue(RenderPassEvent.AfterRenderingTransparents, (fr) => { const gl = fr.gl, E = this.engine; // OnCameraSetup:和相机同尺寸、不要 MSAA 和深度;计数可能超过 1,要浮点格式;点采样 const count = fr.getTemporaryRT('_OverdrawCount', fr.width, fr.height, { color: 'R16F', filter: 'Point' }); E.bindTarget(count); gl.colorMask(true, true, true, true); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); // ConfigureClear(ClearFlag.Color, Color.black) const mask = layerMask(s.layerMask); // Execute:所有物体都用计数材质画(overrideMaterial) if (s.opaque) fr.drawRenderers({ queue: 'opaque', lightModes: TAGS, layerMask: mask, overrideMaterial: this.countMaterial, overridePass: 0 }); if (s.transparent) fr.drawRenderers({ queue: 'transparent', lightModes: TAGS, layerMask: mask, overrideMaterial: this.countMaterial, overridePass: 0, sort: 'backToFront' }); // Blitter.BlitCameraTexture(cmd, m_Count, m_CameraColor, heatmapMaterial, 0) fr.blit(count.texture, 'camera', this.heatmapMaterial, 0); fr.bindCamera(); }, 'OverdrawView'); } }
Assets/Cookbook/Ch29/OverdrawHeatmap.shader(节选:ramp)
half3 HeatRamp(float n) { static const float3 c[8] = { float3(0, 0, 0), float3(0.05, 0.1, 0.55), float3(0.0, 0.6, 0.8), float3(0.1, 0.75, 0.2), float3(0.95, 0.9, 0.1), float3(1.0, 0.5, 0.05), float3(0.9, 0.08, 0.05), float3(1, 1, 1) }; static const float k[8] = { 0, 1, 2, 3, 4, 6, 8, 12 }; n = min(n, k[7] * _MaxCount / 12); float3 col = c[7]; for (int i = 0; i < 7; i++) { float a = k[i] * _MaxCount / 12, b = k[i + 1] * _MaxCount / 12; if (n >= a && n < b) { col = lerp(c[i], c[i + 1], (n - a) / (b - a)); break; } } return col; }
vec3 HeatRamp(float n) { const vec3 c[8] = vec3[8]( vec3(0, 0, 0), vec3(0.05, 0.1, 0.55), vec3(0.0, 0.6, 0.8), vec3(0.1, 0.75, 0.2), vec3(0.95, 0.9, 0.1), vec3(1.0, 0.5, 0.05), vec3(0.9, 0.08, 0.05), vec3(1, 1, 1)); const float k[8] = float[8](0.0, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0); n = min(n, k[7] * _MaxCount / 12.0); vec3 col = c[7]; for (int i = 0; i < 7; i++) { float a = k[i] * _MaxCount / 12.0, b = k[i + 1] * _MaxCount / 12.0; if (n >= a && n < b) { col = mix(c[i], c[i + 1], (n - a) / (b - a)); break; } } return col; }
几个设计点:
- ZTest Always 与 ZWrite Off:统计的是“光栅化产生了多少片元”,不考虑深度剔除,这是传统 Overdraw 视图的定义,能暴露出“被挡住但仍然在画”的浪费。如果想看“考虑 Early-Z 后实际着色的片元”,可以把材质的
_ZTest改为 LessEqual 并绑定相机深度——但那时得到的结果又依赖于绘制顺序,解读要小心。 - 计数纹理用 RHalf:8 位格式在加法混合下会饱和于 1。
- ShaderTagId 列表要与 URP 画物体时用的 LightMode 一致(UniversalForward、UniversalForwardOnly、SRPDefaultUnlit 等),否则有的物体不会被计入。
- 透明物体用
SortingCriteria.CommonTransparent排序,虽然加法混合与顺序无关,但保持与真实渲染一致是好习惯。

URP 14 的 Rendering Debugger 也内置了 Overdraw 视图( › Rendering › Rendering Debug › Overdraw Mode,可选 Opaque、Transparent、All,并可设置 Max Overdraw Count),日常排查直接用它即可;自写版本的价值在于能定制统计规则,并能在真机的 Development Build 里运行。
降低透明 Overdraw 的手段:粒子面片按贴图的不透明区域裁剪成多边形(Unity 的 Sprite 和粒子系统都支持紧凑网格),减少空白像素的光栅化;控制粒子数量和尺寸,大尺寸的半透明面片最昂贵;远处的粒子降低发射率;把多层叠加的效果烘焙进一张贴图;UI 中被全屏面板完全挡住的界面要真正关闭,而不是只是被盖住。