← 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();
}
});
}
}