GameTech/URP 渲染实战

案例 29.5Overdraw 热度图 Renderer Feature进阶

目标:写一个 Feature,把场景里每个像素被绘制的次数画成热度图,直观地找出 Overdraw 热点。

思路:用 DrawingSettings.overrideMaterial 把所有可见物体用一个“计数材质”重新画一遍到一张浮点 RT 上。计数材质用加法混合(Blend One One),每个片元输出 1,最终每个像素的值就是它被画了几层。最后用一个全屏 Pass 把计数映射成颜色。

Assets/Cookbook/Ch29/OverdrawCount.shader(节选:state)
Blend One One          // 累加
ZWrite Off
ZTest [_ZTest]
Cull [_Cull]
Blend One One          // 累加
ZWrite Off
ZTest [_ZTest]
Cull [_Cull]
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;
}

几个设计点:

图 29-3左:正常画面,墙前有一团由 40 张半透明面片组成的烟;右:Overdraw 热度图(深蓝 1 层、青 2 层、绿 3 层、黄 4 层、橙 6 层、红 8 层、白 12 层以上),烟团中心超过 12 层,而这片区域在左图里看起来只是一团淡淡的烟

URP 14 的 Rendering Debugger 也内置了 Overdraw 视图(Window › Analysis › Rendering Debugger › Rendering › Rendering Debug › Overdraw Mode,可选 Opaque、Transparent、All,并可设置 Max Overdraw Count),日常排查直接用它即可;自写版本的价值在于能定制统计规则,并能在真机的 Development Build 里运行。

降低透明 Overdraw 的手段:粒子面片按贴图的不透明区域裁剪成多边形(Unity 的 Sprite 和粒子系统都支持紧凑网格),减少空白像素的光栅化;控制粒子数量和尺寸,大尺寸的半透明面片最昂贵;远处的粒子降低发射率;把多层叠加的效果烘焙进一张贴图;UI 中被全屏面板完全挡住的界面要真正关闭,而不是只是被盖住。