GameTech/URP 渲染实战
源码 / features/Cookbook.Ch26.PostFeatureBase.js
features/Cookbook.Ch26.PostFeatureBase.js
// 26.3 节:把每个后处理都要重复的部分抽成基类(对应 Ch26/PostFeatureBase.cs)。场景里不会直接用到它,
// 第 26 章其它 Feature 的网页版都 import 这里。
//   PostFeatureBase —— 管 Shader→材质、跳过没开后处理的相机、按 Volume 是否启用决定入队。
//   PostPassBase    —— 管 RT 分配工具、执行后恢复渲染目标。
//   SimplePostPass  —— 最常见的“相机颜色 →(材质)→ 临时 RT → 拷回相机颜色”。
// 另外提供 defineVolume:自定义 Volume 组件的默认值(C# 里参数构造时的值)要登记到 VOLUME_DEFAULTS,
// 这样 Volume 权重 < 1 时才能像 Unity 一样在“默认值 ↔ 覆盖值”之间插值。
import { Material } from '../engine/shader.js';
import { RenderPassEvent } from '../engine/features.js';
import { VOLUME_DEFAULTS } from '../engine/post.js';

// ScriptableRenderPassInput
export const PassInput = { None: 0, Depth: 1, Normal: 2, Color: 4, Motion: 8 };

export function defineVolume(type, defaults) { VOLUME_DEFAULTS[type] = defaults; }

// VolumeManager.instance.stack.GetComponent<T>():没有任何 Volume 提供该组件时返回默认值
export function stackComponent(frame, type) { return frame.getVolume(type) || { ...VOLUME_DEFAULTS[type] }; }

export class PostFeatureBase {
  constructor(engine, data, scene) {
    this.engine = engine;
    this.scene = scene;
    this.data = data || {};
    this.passEvent = this.data.passEvent ?? RenderPassEvent.BeforeRenderingPostProcessing;
  }

  get shaderName() { throw new Error('子类要实现 shaderName'); }
  createPass(material) { throw new Error('子类要实现 createPass'); }

  async init() {
    // shader 留空则按 ShaderName 查找
    const name = (this.data.shader && this.data.shader.shader) || this.shaderName;
    this.material = new Material(await this.engine.shaders.get(name));
    this.pass = this.createPass(this.material);
  }

  addPasses(frame, enqueue) {
    if (!this.pass) return;
    if (!frame.camera.postProcessing) return;          // 相机没勾 Post Processing 就不做
    // Prepare:读 Volume、写材质常量;返回 false 表示本帧无效果
    if (!this.pass.prepare(frame)) return;
    // ConfigureInput:深度/法线等输入必须在入队前声明
    const req = this.pass.requiredInputs;
    if (req & PassInput.Depth) frame.requireDepth = true;
    if (req & PassInput.Color) frame.requireOpaque = true;
    if (req & PassInput.Normal && this.pass.renderNormals) {
      // 前向路径下,Normal 输入会让 URP 在不透明物体之前多跑一遍 DepthNormals 预渲染
      enqueue(RenderPassEvent.BeforeRenderingOpaques, (fr) => this.pass.renderNormals(fr), 'DepthNormalPrepass');
    }
    enqueue(this.passEvent, (fr) => {
      this.pass.render(fr);
      fr.bindCamera();                                  // 还给 URP 一个“原样”的目标
    }, this.pass.name);
  }
}

export class PostPassBase {
  constructor(material, name) { this.material = material; this.name = name; }
  get requiredInputs() { return PassInput.None; }
  prepare(frame) { return false; }
  render(fr) {}

  // 相机颜色同格式、去掉 MSAA 与深度、按 downsample 缩小的临时 RT
  static colorRT(fr, name, downsample = 1) {
    return fr.getTemporaryRT(name, Math.max(1, Math.floor(fr.width / downsample)), Math.max(1, Math.floor(fr.height / downsample)));
  }
}

export class SimplePostPass extends PostPassBase {
  constructor(material, name, volumeType) { super(material, name); this.volumeType = volumeType; }
  isActive(v) { return false; }
  apply(v, frame) {}

  prepare(frame) {
    const v = frame.getVolume(this.volumeType);
    if (!v || !this.isActive(v)) return false;
    this.apply(v, frame);
    return true;
  }

  render(fr) {
    const temp = PostPassBase.colorRT(fr, '_Ch26Temp_' + this.volumeType.split('.').pop());
    fr.blit(fr.cameraColorTexture(), temp, this.material, 0);
    fr.blit(temp.texture, 'camera');
  }
}

// 第 26 章:深度 + 法线 预渲染(_CameraNormalsTexture),给需要 Normal 输入的 Pass 用。
// 和 URP 一样,有了这个预渲染,_CameraDepthTexture 也取自它(不带 MSAA,与法线逐像素对齐),
// 返回的 rt.depthTexture 由调用者在读深度前设成 _CameraDepthTexture。
export function renderDepthNormals(fr) {
  const gl = fr.gl;
  const rt = fr.getTemporaryRT('_CameraNormalsTexture', fr.width, fr.height, { color: 'RGBA16F', depth: 'DEPTH24' });
  fr.engine.bindTarget(rt);
  gl.colorMask(true, true, true, true); gl.depthMask(true);
  gl.clearColor(0, 0, 0, 0); gl.clearDepth(1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  fr.drawRenderers({ queue: 'opaque', lightModes: ['DepthNormals', 'DepthNormalsOnly'], layerMask: fr.scene.rendererData.m_OpaqueLayerMask ?? -1 });
  fr.setGlobal('_CameraNormalsTexture', rt.texture);
  fr.bindCamera();
  return rt;
}