GameTech/URP 渲染实战
源码 / features/Cookbook.Ch26.SaturationFeature.js
features/Cookbook.Ch26.SaturationFeature.js
// 案例 26.1:读取 Volume 的后处理 Feature(对应 Ch26/SaturationFeature.cs + SaturationVolume.cs,不依赖基类)。
import { Material } from '../engine/shader.js';
import { RenderPassEvent } from '../engine/features.js';
import { defineVolume } from './Cookbook.Ch26.PostFeatureBase.js';

// 默认值要等于“无效果”的值:没有任何 Volume 覆盖时,栈里拿到的就是默认值
const VOLUME = 'Cookbook.Ch26.SaturationVolume';
defineVolume(VOLUME, { saturation: 1, contrast: 1 });
const isActive = (v) => v.saturation !== 1 || v.contrast !== 1;

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

  async init() {
    const name = (this.data.shader && this.data.shader.shader) || 'Cookbook/Ch26/Saturation';
    this.material = new Material(await this.engine.shaders.get(name));
  }

  addPasses(frame, enqueue) {
    if (!this.material) return;
    // 和 URP 自带后处理保持一致:相机没勾 Post Processing 就不做
    if (!frame.camera.postProcessing) return;
    // 按优先级、权重混合后的 Volume 参数
    const volume = frame.getVolume(VOLUME);
    if (!volume || !isActive(volume)) return;   // 没效果就不入队

    // SaturationPass.Setup:.value 是混合后的最终值
    this.material.setFloat('_Saturation', volume.saturation);
    this.material.setFloat('_Contrast', volume.contrast);
    enqueue(this.passEvent, (fr) => {
      const temp = fr.getTemporaryRT('_Ch26SaturationTemp', fr.width, fr.height);
      fr.blit(fr.cameraColorTexture(), temp, this.material, 0);
      fr.blit(temp.texture, 'camera');
      fr.bindCamera();
    }, 'Ch26 Saturation');
  }
}