Files
gods-eye-view/intro-sequence/src/post.js
T
Claude e9ddc37788 fix(intro): punch exposure on impacts instead of adding a grey veil
Additive flashes lifted the blacks to visible grey after gamma; scale the
exposure instead so only lit pixels brighten. Also drop the scratch GL test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KqP857pippaUs6konSDfe8
2026-10-18 03:37:49 +00:00

219 lines
8.0 KiB
JavaScript

import * as THREE from 'three';
// HDR scene -> HUD composited in -> dual-filter bloom -> lens + film finish.
const QUAD_VS = /* glsl */ `
varying vec2 vUv;
void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
`;
export function createPost(renderer, W, H, hudCanvas) {
const rtOpts = { type: THREE.HalfFloatType, format: THREE.RGBAFormat, depthBuffer: false };
const scene = new THREE.WebGLRenderTarget(W, H, { type: THREE.HalfFloatType, samples: 4 });
const merged = new THREE.WebGLRenderTarget(W, H, rtOpts);
const levels = [];
let w = W;
let h = H;
for (let i = 0; i < 6; i++) {
w = Math.max(2, Math.floor(w / 2));
h = Math.max(2, Math.floor(h / 2));
levels.push({ down: new THREE.WebGLRenderTarget(w, h, rtOpts), up: new THREE.WebGLRenderTarget(w, h, rtOpts) });
}
const hudTex = new THREE.CanvasTexture(hudCanvas);
hudTex.colorSpace = THREE.SRGBColorSpace;
hudTex.minFilter = THREE.LinearFilter;
hudTex.generateMipmaps = false;
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2));
const qScene = new THREE.Scene();
qScene.add(quad);
const qCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const mergeMat = new THREE.ShaderMaterial({
uniforms: { tScene: { value: null }, tHud: { value: hudTex }, uHudGain: { value: 1.25 } },
vertexShader: QUAD_VS,
fragmentShader: /* glsl */ `
uniform sampler2D tScene, tHud;
uniform float uHudGain;
varying vec2 vUv;
void main() {
vec3 s = texture2D(tScene, vUv).rgb;
vec4 hud = texture2D(tHud, vUv);
// canvas uploads un-premultiplied: straight-alpha over-composite
vec3 c = s * (1.0 - hud.a) + hud.rgb * hud.a * uHudGain;
gl_FragColor = vec4(c, 1.0);
}
`,
depthTest: false,
});
const downMat = new THREE.ShaderMaterial({
uniforms: { tSrc: { value: null }, uTexel: { value: new THREE.Vector2() }, uThresh: { value: 0 }, uFirst: { value: 0 } },
vertexShader: QUAD_VS,
fragmentShader: /* glsl */ `
uniform sampler2D tSrc;
uniform vec2 uTexel;
uniform float uThresh, uFirst;
varying vec2 vUv;
vec3 pre(vec3 c) {
if (uFirst < 0.5) return c;
float l = max(c.r, max(c.g, c.b));
float k = max(l - uThresh, 0.0) / max(l, 1e-4);
return min(c * k, vec3(40.0));
}
void main() {
vec2 o = uTexel;
vec3 c = pre(texture2D(tSrc, vUv).rgb) * 4.0;
c += pre(texture2D(tSrc, vUv + vec2(-o.x, -o.y)).rgb);
c += pre(texture2D(tSrc, vUv + vec2( o.x, -o.y)).rgb);
c += pre(texture2D(tSrc, vUv + vec2(-o.x, o.y)).rgb);
c += pre(texture2D(tSrc, vUv + vec2( o.x, o.y)).rgb);
gl_FragColor = vec4(c / 8.0, 1.0);
}
`,
depthTest: false,
});
const upMat = new THREE.ShaderMaterial({
uniforms: { tSrc: { value: null }, tBase: { value: null }, uTexel: { value: new THREE.Vector2() } },
vertexShader: QUAD_VS,
fragmentShader: /* glsl */ `
uniform sampler2D tSrc, tBase;
uniform vec2 uTexel;
varying vec2 vUv;
void main() {
vec2 o = uTexel;
vec3 c = texture2D(tSrc, vUv + vec2(-o.x * 2.0, 0.0)).rgb;
c += texture2D(tSrc, vUv + vec2(-o.x, o.y)).rgb * 2.0;
c += texture2D(tSrc, vUv + vec2(0.0, o.y * 2.0)).rgb;
c += texture2D(tSrc, vUv + vec2(o.x, o.y)).rgb * 2.0;
c += texture2D(tSrc, vUv + vec2(o.x * 2.0, 0.0)).rgb;
c += texture2D(tSrc, vUv + vec2(o.x, -o.y)).rgb * 2.0;
c += texture2D(tSrc, vUv + vec2(0.0, -o.y * 2.0)).rgb;
c += texture2D(tSrc, vUv + vec2(-o.x, -o.y)).rgb * 2.0;
gl_FragColor = vec4(c / 12.0 + texture2D(tBase, vUv).rgb, 1.0);
}
`,
depthTest: false,
});
const finalMat = new THREE.ShaderMaterial({
uniforms: {
tSrc: { value: null },
tBloom: { value: null },
uBloom: { value: 0.9 },
uExposure: { value: 1 },
uTime: { value: 0 },
uFrame: { value: 0 },
uAberr: { value: 0.0015 },
uGrain: { value: 0.045 },
uVignette: { value: 0.55 },
uGlitch: { value: 0 },
uScan: { value: 0 },
uFlash: { value: 0 },
uFade: { value: 1 },
uRes: { value: new THREE.Vector2(W, H) },
},
vertexShader: QUAD_VS,
fragmentShader: /* glsl */ `
uniform sampler2D tSrc, tBloom;
uniform float uBloom, uExposure, uTime, uFrame, uAberr, uGrain, uVignette, uGlitch, uScan, uFlash, uFade;
uniform vec2 uRes;
varying vec2 vUv;
float h21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
vec3 aces(vec3 x) {
const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}
void main() {
vec2 uv = vUv;
// block glitch: horizontal slices displaced on transitions
if (uGlitch > 0.0) {
float row = floor(uv.y * 48.0);
float blk = floor(uv.x * 6.0);
float r = h21(vec2(row, floor(uFrame * 0.5)));
float r2 = h21(vec2(blk, row + floor(uFrame)));
if (r < uGlitch * 0.55) uv.x += (r2 - 0.5) * 0.08 * uGlitch;
if (r2 < uGlitch * 0.12) uv.y += (r - 0.5) * 0.02;
}
vec2 d = uv - 0.5;
float rr = dot(d, d);
vec2 ab = d * rr * uAberr * 7.0 + vec2(uGlitch * 0.006, 0.0);
vec3 col;
col.r = texture2D(tSrc, uv + ab).r;
col.g = texture2D(tSrc, uv).g;
col.b = texture2D(tSrc, uv - ab).b;
vec3 bl;
bl.r = texture2D(tBloom, uv + ab * 1.5).r;
bl.g = texture2D(tBloom, uv).g;
bl.b = texture2D(tBloom, uv - ab * 1.5).b;
col += bl * uBloom;
// flashes punch exposure instead of adding a veil, so blacks stay black
col *= uExposure * (1.0 + uFlash * 6.0);
col = aces(col);
// vignette
float vig = smoothstep(0.95, 0.18, length(d * vec2(uRes.x / uRes.y, 1.0) * 0.82));
col *= mix(1.0, vig, uVignette);
// scanlines (boot / CRT moments)
col *= 1.0 - uScan * 0.22 * (0.5 + 0.5 * sin(gl_FragCoord.y * 3.14159 * 0.5));
col = pow(col, vec3(1.0 / 2.2));
// grain after encoding so it lives in the shadows too
float g = h21(gl_FragCoord.xy + fract(uFrame * 0.6180339) * 913.0) - 0.5;
col += g * uGrain * (1.0 - 0.6 * dot(col, vec3(0.33)));
col *= uFade;
gl_FragColor = vec4(col, 1.0);
}
`,
depthTest: false,
});
function pass(mat, target) {
quad.material = mat;
renderer.setRenderTarget(target);
renderer.render(qScene, qCam);
}
function render(sceneObj, camera, params) {
renderer.setRenderTarget(scene);
renderer.setClearColor(0x000000, 1);
renderer.clear();
renderer.render(sceneObj, camera);
hudTex.needsUpdate = true;
mergeMat.uniforms.tScene.value = scene.texture;
mergeMat.uniforms.uHudGain.value = params.hudGain ?? 1.2;
pass(mergeMat, merged);
let src = merged;
for (let i = 0; i < levels.length; i++) {
downMat.uniforms.tSrc.value = src.texture;
downMat.uniforms.uTexel.value.set(0.5 / src.width, 0.5 / src.height);
downMat.uniforms.uFirst.value = i === 0 ? 1 : 0;
downMat.uniforms.uThresh.value = params.bloomThresh ?? 0.55;
pass(downMat, levels[i].down);
src = levels[i].down;
}
let up = levels[levels.length - 1].down;
for (let i = levels.length - 2; i >= 0; i--) {
upMat.uniforms.tSrc.value = up.texture;
upMat.uniforms.tBase.value = levels[i].down.texture;
upMat.uniforms.uTexel.value.set(0.5 / up.width, 0.5 / up.height);
pass(upMat, levels[i].up);
up = levels[i].up;
}
const u = finalMat.uniforms;
u.tSrc.value = merged.texture;
u.tBloom.value = up.texture;
for (const k of ['bloom', 'exposure', 'aberr', 'grain', 'vignette', 'glitch', 'scan', 'flash', 'fade']) {
const key = 'u' + k[0].toUpperCase() + k.slice(1);
if (params[k] !== undefined) u[key].value = params[k];
}
u.uTime.value = params.time;
u.uFrame.value = params.frame;
pass(finalMat, null);
}
return { render };
}