mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 13:22:18 +00:00
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
219 lines
8.0 KiB
JavaScript
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 };
|
|
}
|