Spaces:
Running
Running
File size: 22,700 Bytes
217da1a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | // CyberMax 3D stage: the shared Three.js (r170, ./vendor, MIT) look for CyberMax games, built from Soaklark 3D's
// renderer patterns. Toon materials with a rim light, HDR scene target with MSAA, two-level bloom, ACES tone map,
// colour grading, vignette, hit flash, chromatic kick on impacts, camera shake (trauma), a GPU-light particle pool,
// gradient sky domes and adaptive resolution. Each game builds its own scene with it and keeps its rules in core.js;
// the game's 2D canvas renderer stays as the no-WebGL2 fallback. Copied into each 3D game by scripts/games/sync_kit.py.
// (c) 2026 CyberMax. All rights reserved.
import * as THREE from 'three';
export { THREE };
export const col = (c) => new THREE.Color(c);
export const clamp01 = (t) => Math.max(0, Math.min(1, t));
export const lerp = (a, b, t) => a + (b - a) * t;
export const ease = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
export const damp = (a, b, k, dt) => b + (a - b) * Math.exp(-k * dt);
/** True when this browser can run the 3D stage (WebGL2). `?2d=1` forces the 2D renderer. */
export function canRun3d() {
try {
if (/[?&]2d=1/.test(location.search)) return false;
const c = document.createElement('canvas');
return !!c.getContext('webgl2');
} catch { return false; }
}
const NOISE = `
float h21(vec2 p){ p = fract(p*vec2(123.34, 456.21)); p += dot(p, p+45.32); return fract(p.x*p.y); }
float vnoise(vec2 p){ vec2 i=floor(p); vec2 f=fract(p); f=f*f*(3.-2.*f);
return mix(mix(h21(i),h21(i+vec2(1,0)),f.x), mix(h21(i+vec2(0,1)),h21(i+vec2(1,1)),f.x), f.y); }`;
const FSQ_V = 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }';
const BRIGHT_F = `uniform sampler2D t; uniform float uTh; uniform vec2 uPx; varying vec2 vUv;
void main(){ vec3 c = vec3(0.);
c += texture2D(t, vUv + uPx*vec2(-1.,-1.)).rgb; c += texture2D(t, vUv + uPx*vec2(1.,-1.)).rgb;
c += texture2D(t, vUv + uPx*vec2(-1.,1.)).rgb; c += texture2D(t, vUv + uPx*vec2(1.,1.)).rgb; c *= 0.25;
float l = max(c.r, max(c.g, c.b)); float k = smoothstep(uTh, uTh + 0.8, l); gl_FragColor = vec4(min(c * k, vec3(24.)), 1.); }`;
const BLUR_F = `uniform sampler2D t; uniform vec2 uDir; varying vec2 vUv;
void main(){ vec3 c = texture2D(t, vUv).rgb * 0.227027;
c += texture2D(t, vUv + uDir*1.3846).rgb * 0.3162162; c += texture2D(t, vUv - uDir*1.3846).rgb * 0.3162162;
c += texture2D(t, vUv + uDir*3.2308).rgb * 0.0702703; c += texture2D(t, vUv - uDir*3.2308).rgb * 0.0702703;
gl_FragColor = vec4(c, 1.); }`;
const COMP_F = `uniform sampler2D tScene, tB1, tB2; uniform float uBloom, uExp, uSat, uCon, uVig, uTime, uFlash, uLdr, uAb, uGrain;
uniform vec3 uLift, uGain, uFlashCol; varying vec2 vUv; ${NOISE}
vec3 aces(vec3 x){ return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), 0., 1.); }
void main(){
vec2 uv = vUv; vec2 d = uv - 0.5;
vec3 c;
if (uAb > 0.0005) { vec2 o = d * uAb; c = vec3(texture2D(tScene, uv + o).r, texture2D(tScene, uv).g, texture2D(tScene, uv - o).b); }
else c = texture2D(tScene, uv).rgb;
c += (texture2D(tB1, uv).rgb * 0.75 + texture2D(tB2, uv).rgb * 1.0) * uBloom;
c *= uExp;
c = mix(aces(c), clamp(c, 0., 1.), uLdr);
c = pow(c, vec3(1./2.2));
float l = dot(c, vec3(0.299, 0.587, 0.114)); c = mix(vec3(l), c, uSat);
c = (c - 0.5) * uCon + 0.5; c = c * uGain + uLift * (1. - c);
c *= 1. - uVig * dot(d, d) * 1.7;
c += (h21(uv * 913.7 + fract(uTime * 7.31)) - 0.5) * uGrain;
c = mix(c, uFlashCol, uFlash);
gl_FragColor = vec4(clamp(c, 0., 1.), 1.);
}`;
const SKY_V = 'varying vec3 vP; void main(){ vP = normalize(position); vec4 p = projectionMatrix * modelViewMatrix * vec4(position, 1.); gl_Position = p.xyww; }';
const SKY_F = `uniform vec3 uTop, uHor, uLow, uGlowCol; uniform vec3 uGlowDir; uniform float uStars, uTime, uGlow, uCurve; varying vec3 vP; ${NOISE}
void main(){ vec3 p = normalize(vP); float y = p.y;
vec3 c = y > 0. ? mix(uHor, uTop, pow(y, uCurve)) : mix(uHor, uLow, pow(-y, uCurve * 0.85));
float s = max(dot(p, normalize(uGlowDir)), 0.); c += uGlowCol * (pow(s, 8.) * 0.6 + pow(s, 90.) * 2.5) * uGlow;
if (uStars > 0.) { vec2 g = vec2(atan(p.z, p.x) * 60., p.y * 80.); vec2 id = floor(g); float r = h21(id);
float st = step(0.975, r) * smoothstep(0.45, 0., length(fract(g) - 0.5)); c += vec3(st) * (0.55 + 0.45 * sin(uTime * 2. + r * 40.)) * uStars * smoothstep(-0.1, 0.3, y) * 2.2; }
gl_FragColor = vec4(c, 1.); }`;
const PART_V = `attribute vec3 aCol; attribute float aSize; attribute float aA; varying vec3 vC; varying float vA; uniform float uScale;
void main(){ vC = aCol; vA = aA; vec4 mv = modelViewMatrix * vec4(position, 1.); gl_Position = projectionMatrix * mv; gl_PointSize = aSize * uScale / max(-mv.z, 0.1); }`;
const PART_F = `varying vec3 vC; varying float vA; void main(){ vec2 q = gl_PointCoord - 0.5; float d = length(q); if (d > 0.5) discard;
float k = smoothstep(0.5, 0.0, d); gl_FragColor = vec4(vC * (0.6 + 1.6 * k * k), vA * k); }`;
/** A pool of glowing particles (sparks, dust, confetti), updated on the CPU and drawn as one additive point cloud. */
class Particles {
constructor(n = 1600) {
this.n = n; this.i = 0; this.live = new Float32Array(n * 10); // x y z vx vy vz life max size drag
this.cr = new Float32Array(n * 3);
const g = new THREE.BufferGeometry();
this.pos = new THREE.Float32BufferAttribute(new Float32Array(n * 3), 3); this.pos.setUsage(THREE.DynamicDrawUsage);
this.colA = new THREE.Float32BufferAttribute(new Float32Array(n * 3), 3); this.colA.setUsage(THREE.DynamicDrawUsage);
this.size = new THREE.Float32BufferAttribute(new Float32Array(n), 1); this.size.setUsage(THREE.DynamicDrawUsage);
this.al = new THREE.Float32BufferAttribute(new Float32Array(n), 1); this.al.setUsage(THREE.DynamicDrawUsage);
g.setAttribute('position', this.pos); g.setAttribute('aCol', this.colA); g.setAttribute('aSize', this.size); g.setAttribute('aA', this.al);
g.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e6);
this.U = { uScale: { value: 300 } };
this.mesh = new THREE.Points(g, new THREE.ShaderMaterial({ vertexShader: PART_V, fragmentShader: PART_F, uniforms: this.U, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
this.mesh.frustumCulled = false; this.mesh.renderOrder = 10;
this.grav = 0;
}
/** Burst `n` particles at (x,y,z). colors: CSS colours; speed in world units/s; life in s; up: extra upward kick. */
burst(x, y, z, { n = 30, colors = ['#FFFFFF'], speed = 30, life = 0.8, size = 3, drag = 2.2, up = 0, spread = 1, hdr = 2.2, dir = null } = {}) {
const cs = colors.map((c) => col(c).multiplyScalar(hdr));
for (let k = 0; k < n; k++) {
const i = this.i; this.i = (this.i + 1) % this.n; const o = i * 10;
let vx = Math.random() * 2 - 1, vy = Math.random() * 2 - 1, vz = Math.random() * 2 - 1; const l = Math.hypot(vx, vy, vz) || 1;
const s = speed * (0.35 + Math.random() * 0.65) * spread;
vx = vx / l * s; vy = vy / l * s + up; vz = vz / l * s;
if (dir) { vx += dir[0]; vy += dir[1]; vz += dir[2]; }
const L = life * (0.6 + Math.random() * 0.6);
this.live.set([x, y, z, vx, vy, vz, L, L, size * (0.6 + Math.random() * 0.8), drag], o);
const c = cs[k % cs.length]; this.cr[i * 3] = c.r; this.cr[i * 3 + 1] = c.g; this.cr[i * 3 + 2] = c.b;
}
}
update(dt) {
const P = this.pos.array, C = this.colA.array, S = this.size.array, A = this.al.array, L = this.live;
for (let i = 0; i < this.n; i++) {
const o = i * 10;
if (L[o + 6] <= 0) { A[i] = 0; S[i] = 0; continue; }
L[o + 6] -= dt; const f = Math.exp(-L[o + 9] * dt);
L[o + 3] *= f; L[o + 4] = L[o + 4] * f - this.grav * dt; L[o + 5] *= f;
L[o] += L[o + 3] * dt; L[o + 1] += L[o + 4] * dt; L[o + 2] += L[o + 5] * dt;
const t = Math.max(0, L[o + 6] / L[o + 7]);
P[i * 3] = L[o]; P[i * 3 + 1] = L[o + 1]; P[i * 3 + 2] = L[o + 2];
C[i * 3] = this.cr[i * 3]; C[i * 3 + 1] = this.cr[i * 3 + 1]; C[i * 3 + 2] = this.cr[i * 3 + 2];
S[i] = L[o + 8] * (0.4 + 0.6 * t); A[i] = Math.min(1, t * 1.6);
}
this.pos.needsUpdate = this.colA.needsUpdate = this.size.needsUpdate = this.al.needsUpdate = true;
}
clear() { this.live.fill(0); }
}
// A tube bent along an arc in the vertex shader, so one geometry serves every blade/zone/progress length.
export function arcGeo(seg = 72, around = 8, flat = false) {
const pos = [], idx = [];
for (let i = 0; i <= seg; i++) for (let j = 0; j <= around; j++) {
const v = (j / around) * Math.PI * 2;
pos.push(i / seg, flat ? (j / around) * 2 - 1 : Math.cos(v), flat ? 0 : Math.sin(v));
}
for (let i = 0; i < seg; i++) for (let j = 0; j < around; j++) {
const a = i * (around + 1) + j, b = a + around + 1;
idx.push(a, b, a + 1, b, b + 1, a + 1);
}
const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3)); g.setIndex(idx);
g.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 20);
return g;
}
export const TUBE = arcGeo(72, 8), RIBBON = arcGeo(72, 1, true);
/** Unlit HDR material for TUBE/RIBBON (always transparent: arc materials share one program, and an opaque one would
* force alpha 1 on every other one): setArc(mesh, startAngle, length, radius, thickness, y) bends it (XZ plane). */
export function arcMat(color, k = 2.5, { opacity = 1, additive = false, dash = 0 } = {}) {
const U = { uA0: { value: 0 }, uLen: { value: 1 }, uR: { value: 1 }, uTube: { value: 0.05 }, uY: { value: 0 }, uDash: { value: dash }, uTime: { value: 0 } };
const m = new THREE.MeshBasicMaterial({ color: col(color).multiplyScalar(k), transparent: true, opacity, depthWrite: !(additive || opacity < 1), blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending, side: THREE.DoubleSide });
m.onBeforeCompile = (s) => {
Object.assign(s.uniforms, U);
s.vertexShader = s.vertexShader.replace('#include <common>', '#include <common>\nuniform float uA0, uLen, uR, uTube, uY; varying float vS;')
.replace('#include <begin_vertex>', 'float th = uA0 + position.x * uLen; float rr = uR + position.y * uTube; vec3 transformed = vec3(cos(th) * rr, uY + position.z * uTube, sin(th) * rr); vS = position.x * uLen;');
s.fragmentShader = s.fragmentShader.replace('#include <common>', '#include <common>\nuniform float uDash, uTime; varying float vS;')
.replace('#include <opaque_fragment>', 'if (uDash > 0. && fract(vS * uDash - uTime * 2.) > 0.5) discard;\n#include <opaque_fragment>');
};
m.customProgramCacheKey = () => `arc${dash > 0 ? 'd' : ''}`;
m.forceSinglePass = true;
m.U = U; return m;
}
export function setArc(m, a0, len, r, tube, y = 0) { const U = m.material.U; U.uA0.value = a0; U.uLen.value = len; U.uR.value = r; U.uTube.value = tube; U.uY.value = y; }
export class Stage {
/**
* canvas: the WebGL canvas. opts: { tier: 'high'|'low', record, bloom, threshold, exposure, sat, con, vig, lift, gain, grain, fov }
*/
constructor(canvas, opts = {}) {
this.canvas = canvas;
const gl = canvas.getContext('webgl2', { antialias: false, alpha: false, depth: true, stencil: false, powerPreference: 'high-performance', preserveDrawingBuffer: !!opts.record });
if (!gl) throw new Error('webgl2');
this.r = new THREE.WebGLRenderer({ canvas, context: gl, antialias: false });
this.r.outputColorSpace = THREE.LinearSRGBColorSpace; this.r.toneMapping = THREE.NoToneMapping;
this.r.shadowMap.enabled = !!opts.shadows; this.r.shadowMap.type = THREE.PCFSoftShadowMap;
this.r.setPixelRatio(1);
const mobile = /Android|iPhone|iPad|Mobile/i.test(navigator.userAgent) && !opts.record;
this.tier = opts.tier || (mobile ? 'low' : 'high');
this.hdr = this.r.extensions.has('EXT_color_buffer_float') || this.r.extensions.has('EXT_color_buffer_half_float');
this.q = { scale: 1, msaa: this.tier === 'high' ? 4 : 2, maxPr: this.tier === 'high' ? 2 : 1.75, bloom: true };
this.look = { bloom: 0.8, threshold: 0.9, exposure: 1, sat: 1.12, con: 1.05, vig: 0.55, lift: [0.01, 0.0, 0.03], gain: [1, 1, 1], grain: 0.025, ...opts };
this.scene = new THREE.Scene();
this.cam = new THREE.PerspectiveCamera(opts.fov || 50, 1, 0.1, 4000);
this.camBase = new THREE.Vector3(); this.camLook = new THREE.Vector3();
this.gradient = (() => { const d = new Uint8Array([110, 110, 110, 255, 170, 170, 170, 255, 225, 225, 225, 255, 255, 255, 255, 255]); const t = new THREE.DataTexture(d, 4, 1, THREE.RGBAFormat); t.minFilter = t.magFilter = THREE.NearestFilter; t.needsUpdate = true; return t; })();
this.rimU = { value: col(opts.rim || '#8FA8FF').multiplyScalar(0.5) };
this.t = 0; this.trauma = 0; this.flashA = 0; this.flashCol = col('#FFFFFF'); this.kick = 0;
this.parts = new Particles(opts.particles || 1600); this.scene.add(this.parts.mesh);
this.post();
this.ft = 16; this.slow = 0; this.fast = 0; this.frames = 0; this.strikes = 0; this.tooSlow = false;
this.auto = !opts.fixedQuality && !opts.record && !/[?&](fixedq|3d)=1/.test(location.search);
}
// ---------- materials
/** Cel-shaded material with a rim light (Soaklark look). */
toon(opts = {}, rim = 1) {
const m = new THREE.MeshToonMaterial({ gradientMap: this.gradient, ...opts });
const R = this.rimU;
m.onBeforeCompile = (s) => {
s.uniforms.uRim = R;
s.fragmentShader = s.fragmentShader.replace('#include <common>', '#include <common>\nuniform vec3 uRim;')
.replace('#include <opaque_fragment>', `float rimF = 1.0 - max(dot(normalize(vViewPosition), normal), 0.0);\noutgoingLight += uRim * ${rim.toFixed(2)} * smoothstep(0.6, 0.95, rimF);\n#include <opaque_fragment>`);
};
return m;
}
/** Unlit HDR colour: anything brighter than ~1 blooms. */
glow(color, k = 2.5, o = {}) { return new THREE.MeshBasicMaterial({ color: col(color).multiplyScalar(k), toneMapped: false, ...o }); }
/** Back-face outline shell for a mesh (inverted hull). */
outline(mesh, t = 0.06, color = '#0A0B14') {
const m = new THREE.Mesh(mesh.geometry, new THREE.MeshBasicMaterial({ color, side: THREE.BackSide }));
m.scale.setScalar(1 + t); mesh.add(m); return m;
}
/** Gradient sky dome (top / horizon / below), optional glow (sun or moon) and twinkling stars. */
sky({ top = '#0A0B2A', hor = '#3A1E6E', low = '#05060F', glow = '#FF8A5C', glowDir = [0.3, 0.25, -1], glowK = 1, stars = 0, curve = 0.6 } = {}) {
if (this.skyMesh) { this.scene.remove(this.skyMesh); this.skyMesh.material.dispose(); }
const U = { uTop: { value: col(top) }, uHor: { value: col(hor) }, uLow: { value: col(low) }, uGlowCol: { value: col(glow) }, uGlowDir: { value: new THREE.Vector3(...glowDir) }, uStars: { value: stars }, uTime: { value: 0 }, uGlow: { value: glowK }, uCurve: { value: curve } };
const m = new THREE.Mesh(new THREE.SphereGeometry(1000, 32, 16), new THREE.ShaderMaterial({ vertexShader: SKY_V, fragmentShader: SKY_F, uniforms: U, side: THREE.BackSide, depthWrite: false }));
m.frustumCulled = false; m.renderOrder = -10; this.skyU = U; this.skyMesh = m; this.scene.add(m);
return m;
}
// ---------- post-processing
post() {
const type = this.hdr ? THREE.HalfFloatType : THREE.UnsignedByteType;
this.rtS = new THREE.WebGLRenderTarget(4, 4, { type, samples: this.q.msaa, depthBuffer: true });
this.rtB = [0, 1, 2, 3].map(() => new THREE.WebGLRenderTarget(4, 4, { type, depthBuffer: false }));
const tri = new THREE.BufferGeometry();
tri.setAttribute('position', new THREE.Float32BufferAttribute([-1, -1, 0, 3, -1, 0, -1, 3, 0], 3));
tri.setAttribute('uv', new THREE.Float32BufferAttribute([0, 0, 2, 0, 0, 2], 2));
const mk = (f, u) => new THREE.ShaderMaterial({ vertexShader: FSQ_V, fragmentShader: f, uniforms: u, depthTest: false, depthWrite: false });
this.mBright = mk(BRIGHT_F, { t: { value: null }, uTh: { value: 1 }, uPx: { value: new THREE.Vector2() } });
this.mBlur = mk(BLUR_F, { t: { value: null }, uDir: { value: new THREE.Vector2() } });
this.mComp = mk(COMP_F, { tScene: { value: null }, tB1: { value: null }, tB2: { value: null }, uBloom: { value: 0.8 }, uExp: { value: 1 }, uSat: { value: 1.1 }, uCon: { value: 1.05 },
uVig: { value: 0.55 }, uTime: { value: 0 }, uFlash: { value: 0 }, uLdr: { value: this.hdr ? 0 : 1 }, uAb: { value: 0 }, uGrain: { value: 0.02 },
uLift: { value: new THREE.Vector3() }, uGain: { value: new THREE.Vector3(1, 1, 1) }, uFlashCol: { value: new THREE.Color() } });
this.fsq = new THREE.Mesh(tri, this.mComp); this.fsq.frustumCulled = false;
this.fsScene = new THREE.Scene(); this.fsScene.add(this.fsq); this.fsCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
}
resize(cssW, cssH, dpr = window.devicePixelRatio || 1) {
this.cssW = cssW; this.cssH = cssH; this.dpr = dpr;
const pr = Math.min(dpr, this.q.maxPr) * this.q.scale;
const w = Math.max(2, Math.round(cssW * pr)), h = Math.max(2, Math.round(cssH * pr));
if (w === this.w && h === this.h) return;
this.r.setSize(w, h, false); this.w = w; this.h = h;
this.rtS.setSize(w, h);
const bw = Math.max(2, w >> 2), bh = Math.max(2, h >> 2);
this.rtB[0].setSize(bw, bh); this.rtB[1].setSize(bw, bh); this.rtB[2].setSize(Math.max(2, bw >> 1), Math.max(2, bh >> 1)); this.rtB[3].setSize(Math.max(2, bw >> 1), Math.max(2, bh >> 1));
this.cam.aspect = w / h; this.cam.updateProjectionMatrix();
this.parts.U.uScale.value = h * 0.9;
}
/** Keep the drawing buffer matched to the canvas's CSS box. */
fit() { const r = this.canvas.getBoundingClientRect(); if (r.width > 1 && r.height > 1) this.resize(r.width, r.height); }
setQuality(scale) { this.q.scale = Math.max(0.5, Math.min(1, scale)); const w = this.cssW, h = this.cssH; this.w = 0; this.resize(w, h, this.dpr); }
// ---------- juice
shake(amount = 0.5) { this.trauma = Math.min(1, this.trauma + amount); }
flash(color = '#FFFFFF', a = 0.5) { this.flashCol.set(color); this.flashA = Math.max(this.flashA, a); }
punch(k = 0.006) { this.kick = Math.max(this.kick, k); }
burst(v, o) { this.parts.burst(v.x, v.y, v.z, o); }
/**
* Draw one frame. dt in seconds (fixed when recording). Shake moves the camera a little around where the game put it
* (call setCam(pos, look) each frame before render, or set stage.cam yourself and leave camBase empty).
*/
render(dt) {
dt = Math.min(0.1, Math.max(0, dt));
this.t += dt;
this.parts.update(dt);
if (this.skyU) this.skyU.uTime.value = this.t;
// camera shake (trauma^2 noise, decays)
const tr = this.trauma * this.trauma; this.trauma = Math.max(0, this.trauma - dt * 1.6);
const cam = this.cam;
if (this.camSet) { cam.position.copy(this.camBase); cam.lookAt(this.camLook); }
if (tr > 0.0001) {
const n = (k) => Math.sin(this.t * (37 + k * 11) + k * 7) * 0.6 + Math.sin(this.t * (71 + k * 5) + k) * 0.4;
const s = this.shakeScale || 1;
cam.position.x += n(1) * tr * s; cam.position.y += n(2) * tr * s; cam.rotation.z += n(3) * tr * 0.04;
}
// adaptive quality from real frame times: lower the resolution, then bloom; a device that still can't keep up gets
// tooSlow, and the game swaps back to its 2D renderer (same rules, same run) so it always stays playable.
const nowR = performance.now(), real = this.lastR ? nowR - this.lastR : 16; this.lastR = nowR;
if (this.auto && real < 1000 && ++this.frames > 8) {
this.ft = this.ft * 0.85 + real * 0.15;
if (this.ft > 34) {
this.fast = 0;
if (++this.slow > 12) {
this.slow = 0;
if (this.q.scale > 0.55) this.setQuality(this.q.scale - 0.2);
else if (this.q.bloom) this.q.bloom = false;
else if (++this.strikes > 2) this.tooSlow = true;
}
} else if (this.ft < 18 && this.q.scale < 1) { this.slow = 0; if (++this.fast > 300) { this.fast = 0; this.setQuality(this.q.scale + 0.1); } }
else this.slow = Math.max(0, this.slow - 1);
}
const r = this.r, L = this.look, U = this.mComp.uniforms;
r.setRenderTarget(this.rtS); r.render(this.scene, cam);
let b1 = this.rtB[0].texture, b2 = this.rtB[2].texture;
if (this.q.bloom && L.bloom > 0) {
const pass = (mat, out) => { this.fsq.material = mat; r.setRenderTarget(out); r.render(this.fsScene, this.fsCam); };
this.mBright.uniforms.t.value = this.rtS.texture; this.mBright.uniforms.uTh.value = L.threshold; this.mBright.uniforms.uPx.value.set(1 / this.w, 1 / this.h);
pass(this.mBright, this.rtB[0]);
const blur = (src, tmp, dst, w, h) => {
this.mBlur.uniforms.t.value = src.texture; this.mBlur.uniforms.uDir.value.set(1 / w, 0); pass(this.mBlur, tmp);
this.mBlur.uniforms.t.value = tmp.texture; this.mBlur.uniforms.uDir.value.set(0, 1 / h); pass(this.mBlur, dst);
};
blur(this.rtB[0], this.rtB[1], this.rtB[0], this.rtB[0].width, this.rtB[0].height);
this.mBlur.uniforms.t.value = this.rtB[0].texture; this.mBlur.uniforms.uDir.value.set(0, 0); pass(this.mBlur, this.rtB[2]);
blur(this.rtB[2], this.rtB[3], this.rtB[2], this.rtB[2].width, this.rtB[2].height);
blur(this.rtB[2], this.rtB[3], this.rtB[2], this.rtB[2].width, this.rtB[2].height);
b1 = this.rtB[0].texture; b2 = this.rtB[2].texture; U.uBloom.value = L.bloom;
} else U.uBloom.value = 0;
U.tScene.value = this.rtS.texture; U.tB1.value = b1; U.tB2.value = b2;
U.uExp.value = L.exposure; U.uSat.value = L.sat; U.uCon.value = L.con; U.uVig.value = L.vig; U.uGrain.value = L.grain;
U.uLift.value.set(...L.lift); U.uGain.value.set(...L.gain); U.uTime.value = this.t;
U.uFlash.value = this.flashA; U.uFlashCol.value.copy(this.flashCol); this.flashA *= Math.exp(-dt * 7); if (this.flashA < 0.004) this.flashA = 0;
U.uAb.value = this.kick + tr * 0.012; this.kick *= Math.exp(-dt * 8);
this.fsq.material = this.mComp; r.setRenderTarget(null); r.render(this.fsScene, this.fsCam);
}
setCam(pos, look) { this.camSet = true; this.camBase.copy(pos); this.camLook.copy(look); }
dispose() { this.r.dispose(); }
}
/**
* Mount a 3D layer behind a game's 2D canvas: a WebGL canvas the same size, in the same box. The 2D canvas stays on
* top (HUD text, taps, focus, tests) and the game draws it transparent while 3D is on.
*/
export function mountBehind(cv2d, cls = 'cmx3d') {
const host = cv2d.parentElement;
const wrap = document.createElement('div'); wrap.className = `${cls}-wrap`;
host.insertBefore(wrap, cv2d); wrap.appendChild(cv2d);
const c = document.createElement('canvas'); c.className = cls; c.setAttribute('aria-hidden', 'true');
wrap.insertBefore(c, cv2d);
return c;
}
|