// world/wall_material.js (Lane A) — the synthetic-scanner wall (ART_BIBLE.md §Rendering recipe). // No real-time lights anywhere in GUTS: the wall is biome tint x detail luminance + a fresnel // rim (the SEM edge glow) + exponential fog to the biome void. Peristalsis is vertex work. // // Attributes the geometry must supply (tube.js and arena.js both do): // position vec3 baked ring/shell vertex // aInward vec3 unit normal pointing into the playable space // aTangent vec3 unit centreline tangent at this vertex (for the wave's normal tilt) // uv vec2 (theta/2pi, s) — s in world units, NOT normalized: it's the wave's phase // axis and the tiling axis, and it must stay continuous across chunks // aPhase float K(s) = ∫k ds, baked (see spline.js) // aK float local wavenumber, for the analytic d(pulse)/ds // // Colorspace: like the stub, this writes its computed color straight out with no // conversion. That's a whole-game decision (it moves every color at // once) so it stays matched to F's round-0 look until F rules on it — see LANE_A_NOTES. import * as THREE from 'three'; export function createWallMaterial({ biome, omega, fog = biome.fog, // overridable: arenas size their own fog to the room (see index.js) waveAmp = biome.wave.amp, breatheAmp = biome.wave.breathe, detail = null, // THREE.Texture | null — Lane D's grayscale detail map tile = null, // [repeatsAroundCircumference, unitsOfSPerRepeat] radiusHint = 10, // used only to pick a square-ish default tiling side = THREE.FrontSide, }) { if (detail) { // defensive: we consume D's texture, so we set what we depend on detail.wrapS = detail.wrapT = THREE.RepeatWrapping; detail.needsUpdate = true; } const tileAround = tile ? tile[0] : 3; const tileAlong = tile ? tile[1] : Math.max(4, (2 * Math.PI * radiusHint) / tileAround); return new THREE.ShaderMaterial({ side, defines: detail ? { USE_DETAIL: '' } : {}, uniforms: { uTime: { value: 0 }, uTint: { value: new THREE.Color(biome.palette.tint) }, uRim: { value: new THREE.Color(biome.palette.rim) }, uVoid: { value: new THREE.Color(biome.palette.void) }, uFog: { value: fog }, uWaveA: { value: waveAmp }, uBreatheA: { value: breatheAmp }, uOmega: { value: omega }, uRimPow: { value: 2.2 }, uRimGain: { value: 0.9 }, uDetail: { value: detail }, uTile: { value: new THREE.Vector2(tileAround, tileAlong) }, }, vertexShader: /* glsl */` attribute vec3 aInward; attribute vec3 aTangent; attribute float aPhase; attribute float aK; uniform float uTime, uWaveA, uBreatheA, uOmega; varying vec2 vUv; varying vec3 vN; varying vec3 vView; varying float vPulse; void main() { vUv = uv; float phi = aPhase - uOmega * uTime; float sn = max(0.0, sin(phi)); float pulse = sn * sn * sn; // sharp crest, long trough: a muscle, not a sine float breathe = uBreatheA * sin(uv.y * 0.7 + uTime * 0.8) * sin(uv.x * 6.2831853 * 3.0); float disp = uWaveA * pulse + breathe; // Tilt the normal with the wave. Without this the crests are silhouette-only and the // rim light slides over them as if the wall were flat — the effective wall radius is // rho(s) = radius - disp, so the inward normal leans along the tangent by d(rho)/ds. float dPulse_ds = 3.0 * sn * sn * cos(phi) * aK; vec3 nIn = normalize(aInward - aTangent * (uWaveA * dPulse_ds)); vec4 mv = modelViewMatrix * vec4(position + aInward * disp, 1.0); vN = normalize(normalMatrix * nIn); vView = -mv.xyz; vPulse = pulse; gl_Position = projectionMatrix * mv; }`, fragmentShader: /* glsl */` uniform vec3 uTint, uRim, uVoid; uniform float uFog, uRimPow, uRimGain; #ifdef USE_DETAIL uniform sampler2D uDetail; uniform vec2 uTile; #endif varying vec2 vUv; varying vec3 vN; varying vec3 vView; varying float vPulse; void main() { #ifdef USE_DETAIL // Lane D authors grayscale; the biome tint is applied here, so one texture can serve // two biomes at different tints (ART_BIBLE §FLUX prompt kit). float detail = texture2D(uDetail, vec2(vUv.x * uTile.x, vUv.y / uTile.y)).r; detail = mix(0.5, 1.2, detail); #else // Assets-optional law: no texture is the *shipping* look until D lands, not an error // state. Ridged folds around theta + striation along s = a passable SEM stand-in. float folds = 0.55 + 0.45 * sin(vUv.x * 6.2831853 * 9.0 + sin(vUv.y * 0.9) * 2.0); float detail = folds * (0.85 + 0.15 * sin(vUv.y * 2.2)); #endif vec3 base = uTint * detail * 0.8; float fres = pow(1.0 - abs(dot(normalize(vN), normalize(vView))), uRimPow); // crest sheen: the wave is a gameplay tell (ride it for boost), so it gets a little // help beyond what its own geometry earns from the rim term vec3 col = base + uRim * fres * uRimGain + uRim * vPulse * 0.10; float d = length(vView); gl_FragColor = vec4(mix(col, uVoid, 1.0 - exp(-uFog * d * 0.55)), 1.0); }`, }); }