// flight/emissive.js (Lane B) — the one material behind every glowing thing B draws. // // ART_BIBLE: gameplay-relevant things glow, and the scene has NO lights (walls fake theirs // with fresnel). So our primitives are unlit: a core colour with a rim that hots up at // grazing angles, which reads as a volume against the dark tint instead of a flat decal. // One material per (colour, rim) pair + InstancedMesh = 1 draw call per enemy/projectile // type, which is how the whole combat layer fits in its 80-draw budget. // // Lives in flight/ rather than combat/ to keep the dependency arrow one-way: combat imports // flight (it needs the player), flight never imports combat. import * as THREE from 'three'; // ART_BIBLE §Emissive code — the readability law. Never hand-pick a colour outside this. export const EMISSIVE = { hostile: { core: 0xff5a2a, rim: 0xffb488 }, // hot amber -> red core hostileShot: { core: 0xffe9d0, rim: 0xffffff }, // white-hot neutral: { core: 0x39e6ff, rim: 0xdff2ff }, // cyan (player + flora + interactive) ally: { core: 0x33ffbe, rim: 0xd6fff0 }, // teal — an active friendly unit (phage). // distinct from cyan "don't shoot" flora: // this one HELPS, and reads as its own thing. pickup: { core: 0x7dffb0, rim: 0xdaffe9 }, gate: { core: 0xb06aff, rim: 0xe6d0ff }, acid: { core: 0xc8ff3a, rim: 0xeaffb0 }, }; export function emissiveMat({ core, rim }, { additive = false, opacity = 1 } = {}) { return new THREE.ShaderMaterial({ transparent: opacity < 1 || additive, depthWrite: !additive, blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending, uniforms: { uCore: { value: new THREE.Color(core) }, uRim: { value: new THREE.Color(rim) }, uOpacity: { value: opacity }, uFlash: { value: 0 }, // driven on hit: 0..1 blows the whole body to rim colour }, vertexShader: /* glsl */` varying vec3 vN; varying vec3 vV; void main() { // three declares the instanceMatrix attribute for us whenever the drawn object is // an InstancedMesh (USE_INSTANCING); this same material still works on a plain Mesh. // (Careful: no backticks in here — this is a JS template literal.) #ifdef USE_INSTANCING mat4 m = modelViewMatrix * instanceMatrix; #else mat4 m = modelViewMatrix; #endif vec4 mv = m * vec4(position, 1.0); // mat3(m) is only a true normal matrix under uniform scale — every instance we // emit is uniformly scaled, and normalize() eats the leftover constant. vN = normalize(mat3(m) * normal); vV = -mv.xyz; gl_Position = projectionMatrix * mv; }`, fragmentShader: /* glsl */` varying vec3 vN; varying vec3 vV; uniform vec3 uCore; uniform vec3 uRim; uniform float uOpacity; uniform float uFlash; void main() { float f = pow(1.0 - abs(dot(normalize(vN), normalize(vV))), 2.0); vec3 col = mix(uCore, uRim, f) + uRim * f * 0.6; col = mix(col, uRim * 1.6, uFlash); gl_FragColor = vec4(col, uOpacity); // MANDATORY for any custom ShaderMaterial in this project. ColorManagement is on, // so THREE.Color(0xff5a2a) is converted to LINEAR working space on the way in, but // a hand-written fragment shader gets no automatic conversion on the way out — it // would write linear straight into the sRGB target. Measured: hostile amber landed // as rgb(255,26,6) blood-red instead of rgb(255,90,42), quietly breaking the // ART_BIBLE emissive readability law. This chunk applies linear -> sRGB. #include }`, }); }