← back to Dw War Room

src/showroom/ShowroomPostProcess.ts

182 lines

// ShowroomPostProcess.ts — Photorealistic post-processing for the showroom
// Adds SSAO, DOF, and color grading on top of existing bloom/grain pipeline
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';

// ── Color Grading Shader ─────────────────────────────────────────────────────
// Warm architectural tones: slight orange lift in shadows, cool highlight compression
const ColorGradingShader = {
  name: 'ColorGradingShader',
  uniforms: {
    tDiffuse: { value: null as THREE.Texture | null },
    uSaturation: { value: 1.1 },
    uContrast: { value: 1.08 },
    uBrightness: { value: 0.02 },
    uWarmth: { value: 0.04 },
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    uniform sampler2D tDiffuse;
    uniform float uSaturation;
    uniform float uContrast;
    uniform float uBrightness;
    uniform float uWarmth;
    varying vec2 vUv;

    void main() {
      vec4 color = texture2D(tDiffuse, vUv);

      // Saturation adjustment
      float luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
      color.rgb = mix(vec3(luminance), color.rgb, uSaturation);

      // Contrast (pivot at mid-gray)
      color.rgb = (color.rgb - 0.5) * uContrast + 0.5;

      // Brightness lift
      color.rgb += uBrightness;

      // Warm tint — add slight orange to shadows, cool highlights slightly
      float shadowMask = 1.0 - smoothstep(0.0, 0.5, luminance);
      color.r += uWarmth * shadowMask;
      color.g += uWarmth * 0.5 * shadowMask;

      // Vignette — darkens corners for photographic depth
      vec2 vUvCenter = vUv - 0.5;
      float vignette = 1.0 - dot(vUvCenter, vUvCenter) * 0.5;
      color.rgb *= vignette;

      gl_FragColor = color;
    }
  `,
};

// ── Film Grain (reused from war room) ────────────────────────────────────────
const FilmGrainShader = {
  name: 'FilmGrainShader',
  uniforms: {
    tDiffuse: { value: null as THREE.Texture | null },
    uTime: { value: 0.0 },
    uOpacity: { value: 0.006 },
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;
    void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
  `,
  fragmentShader: /* glsl */ `
    uniform sampler2D tDiffuse;
    uniform float uTime;
    uniform float uOpacity;
    varying vec2 vUv;
    float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }
    void main() {
      vec4 color = texture2D(tDiffuse, vUv);
      float grain = hash(vUv + fract(uTime * 0.1));
      color.rgb += (grain - 0.5) * 2.0 * uOpacity;
      gl_FragColor = color;
    }
  `,
};

export class ShowroomPostProcess {
  private composer: EffectComposer;
  private ssaoPass: SSAOPass;
  private bloomPass: UnrealBloomPass;
  private bokehPass: BokehPass;
  private colorGradingPass: ShaderPass;
  private filmGrainPass: ShaderPass;
  private clock: THREE.Clock;

  constructor(
    renderer: THREE.WebGLRenderer,
    scene: THREE.Scene,
    camera: THREE.PerspectiveCamera
  ) {
    this.clock = new THREE.Clock();
    const size = renderer.getSize(new THREE.Vector2());

    this.composer = new EffectComposer(renderer);

    // Pass 1: Render
    this.composer.addPass(new RenderPass(scene, camera));

    // Pass 2: SSAO — ambient occlusion for depth
    this.ssaoPass = new SSAOPass(scene, camera, size.x, size.y);
    this.ssaoPass.kernelRadius = 0.5;
    this.ssaoPass.minDistance = 0.001;
    this.ssaoPass.maxDistance = 0.1;
    (this.ssaoPass as any).output = SSAOPass.OUTPUT.Default;
    this.composer.addPass(this.ssaoPass);

    // Pass 3: Bloom — subtle architectural glow
    this.bloomPass = new UnrealBloomPass(
      new THREE.Vector2(size.x, size.y),
      0.2,   // strength (slightly less than war room — showroom is brighter)
      0.4,   // radius
      0.7    // threshold
    );
    this.composer.addPass(this.bloomPass);

    // Pass 4: Depth of Field — cinematic focus
    this.bokehPass = new BokehPass(scene, camera, {
      focus: 15.0,
      aperture: 0.002,
      maxblur: 0.005,
    });
    this.composer.addPass(this.bokehPass);

    // Pass 5: Color Grading — warm architectural tones
    this.colorGradingPass = new ShaderPass(ColorGradingShader);
    this.composer.addPass(this.colorGradingPass);

    // Pass 6: Film Grain — organic texture
    this.filmGrainPass = new ShaderPass(FilmGrainShader);
    this.composer.addPass(this.filmGrainPass);
  }

  render(): void {
    this.filmGrainPass.uniforms['uTime'].value = this.clock.getElapsedTime();
    this.composer.render();
  }

  setFocusDistance(distance: number): void {
    // BokehPass stores uniforms on materialBokeh.uniforms
    const uniforms = (this.bokehPass as any).materialBokeh?.uniforms
      ?? (this.bokehPass as any).uniforms;
    if (uniforms?.focus) {
      uniforms.focus.value = distance;
    }
  }

  resize(width: number, height: number): void {
    this.composer.setSize(width, height);
    this.bloomPass.resolution.set(width, height);
    this.ssaoPass.setSize(width, height);
  }

  getComposer(): EffectComposer {
    return this.composer;
  }

  dispose(): void {
    this.composer.renderTarget1.dispose();
    this.composer.renderTarget2.dispose();
    this.composer.passes.forEach(pass => {
      if ('dispose' in pass && typeof pass.dispose === 'function') {
        pass.dispose();
      }
    });
  }
}