GameTech/URP 渲染实战
源码 / features/Cookbook.Ch28.SimpleSSAOFeature.js
features/Cookbook.Ch28.SimpleSSAOFeature.js
// 案例 28.4:简化版 SSAO 的 Renderer Feature(对应 Ch28/SimpleSSAOFeature.cs)。
//   IntoLighting        —— 像 URP 自带 SSAO 一样,在不透明物体之前算好 AO,作为 _ScreenSpaceOcclusionTexture 交给光照;
//   MultiplyAfterOpaque —— 不透明物体画完后直接把 AO 乘到颜色上;
//   ShowAO              —— 只显示 AO(调试)。
import { featureMaterial, RenderPassEvent } from '../engine/features.js';
import { renderDepthNormals } from './Cookbook.Ch28.ScreenSpaceUtil.js';

const Mode = { IntoLighting: 0, MultiplyAfterOpaque: 1, ShowAO: 2 };

export default class SimpleSSAOFeature {
  constructor(engine, data, scene) {
    this.engine = engine;
    this.scene = scene;
    this.settings = { material: null, mode: Mode.IntoLighting, blur: true, halfResolution: false, directLightingStrength: 0.25, ...((data && data.settings) || {}) };
  }

  async init() { this.material = featureMaterial(this.scene, this.settings.material); }

  addPasses(frame, enqueue) {
    const s = this.settings, mat = this.material;
    if (!mat) return;
    // 进光照:必须在不透明物体之前,且要等深度/法线预通道画完(+1 与 URP 自带 SSAO 一致)
    const event = s.mode === Mode.IntoLighting ? RenderPassEvent.AfterRenderingPrePasses + 1 : RenderPassEvent.AfterRenderingOpaques;
    // ConfigureInput(Depth | Normal):前向路径下 URP 会执行 DepthNormals 预通道
    enqueue(RenderPassEvent.AfterRenderingPrePasses, (fr) => renderDepthNormals(fr), 'DepthNormalPrepass');
    enqueue(event, (fr) => {
      const W = s.halfResolution ? Math.floor(fr.width / 2) : fr.width, H = s.halfResolution ? Math.floor(fr.height / 2) : fr.height;
      const ao = fr.getTemporaryRT('_SimpleSSAO', W, H, { color: 'RGBA8' });
      const aoTemp = fr.getTemporaryRT('_SimpleSSAOTemp', W, H, { color: 'RGBA8' });
      mat.setVector('_SSAOTexelSize', [1 / W, 1 / H, W, H]);
      // 1) AO:没有输入纹理,直接画全屏三角形
      fr.blit(null, ao, mat, 0);
      // 2) 可分离双边模糊:AO → Temp(横)→ AO(竖)
      if (s.blur) {
        fr.blit(ao.texture, aoTemp, mat, 1);
        fr.blit(aoTemp.texture, ao, mat, 2);
      }
      if (s.mode === Mode.IntoLighting) {
        // 3a) 按 URP 的约定把结果交给光照:纹理 + 参数(w = 直接光受 AO 影响的比例)
        fr.setGlobal('_ScreenSpaceOcclusionTexture', ao.texture);
        fr.setGlobal('_AmbientOcclusionParam', [1, 0, 0, s.directLightingStrength]);
      } else {
        // 3b) 直接乘到相机颜色上(或只显示 AO)
        fr.setGlobal('_SSAOResult', ao.texture);
        const copy = fr.getTemporaryRT('_SimpleSSAOColor', fr.width, fr.height, { filter: 'Point' });
        fr.blit(fr.cameraColorTexture(), copy);
        fr.blit(copy.texture, 'camera', mat, s.mode === Mode.ShowAO ? 4 : 3);
      }
      fr.bindCamera();
    }, 'SimpleSSAO');
  }
}