// === WARP TUNNEL + CRYSTALS + LIGHTNING + BATCAVE + DUAL-BRAIN === import * as THREE from 'three'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { NEXUS } from './constants.js'; import { scene, camera, renderer } from './scene-setup.js'; import { composer } from './controls.js'; import { zoneIntensity } from './heatmap.js'; import { S } from './state.js'; // === WARP TUNNEL EFFECT === const WarpShader = { uniforms: { 'tDiffuse': { value: null }, 'time': { value: 0.0 }, 'progress': { value: 0.0 }, 'portalColor': { value: new THREE.Color(0x4488ff) }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform float time; uniform float progress; uniform vec3 portalColor; varying vec2 vUv; #define PI 3.14159265358979 void main() { vec2 uv = vUv; vec2 center = vec2(0.5, 0.5); vec2 dir = uv - center; float dist = length(dir); float angle = atan(dir.y, dir.x); float intensity = sin(progress * PI); float zoom = 1.0 + intensity * 3.0; vec2 zoomedUV = center + dir / zoom; float swirl = intensity * 5.0 * max(0.0, 1.0 - dist * 2.0); float twisted = angle + swirl; vec2 swirlUV = center + vec2(cos(twisted), sin(twisted)) * dist / (1.0 + intensity * 1.8); vec2 warpUV = mix(zoomedUV, swirlUV, 0.6); warpUV = clamp(warpUV, vec2(0.001), vec2(0.999)); float aber = intensity * 0.018; vec2 aberDir = normalize(dir + vec2(0.001)); float rVal = texture2D(tDiffuse, clamp(warpUV + aberDir * aber, vec2(0.0), vec2(1.0))).r; float gVal = texture2D(tDiffuse, warpUV).g; float bVal = texture2D(tDiffuse, clamp(warpUV - aberDir * aber, vec2(0.0), vec2(1.0))).b; vec4 color = vec4(rVal, gVal, bVal, 1.0); float numLines = 28.0; float lineAngleFrac = fract((angle / (2.0 * PI) + 0.5) * numLines + time * 4.0); float lineSharp = pow(max(0.0, 1.0 - abs(lineAngleFrac - 0.5) * 16.0), 3.0); float radialFade = max(0.0, 1.0 - dist * 2.2); float speedLine = lineSharp * radialFade * intensity * 1.8; float lineAngleFrac2 = fract((angle / (2.0 * PI) + 0.5) * 14.0 - time * 2.5); float lineSharp2 = pow(max(0.0, 1.0 - abs(lineAngleFrac2 - 0.5) * 12.0), 3.0); float speedLine2 = lineSharp2 * radialFade * intensity * 0.9; float rimDist = abs(dist - 0.08 * intensity); float rimGlow = pow(max(0.0, 1.0 - rimDist * 40.0), 2.0) * intensity; color.rgb = mix(color.rgb, portalColor, intensity * 0.45); color.rgb += portalColor * (speedLine + speedLine2); color.rgb += vec3(1.0) * rimGlow * 0.8; float bloom = pow(max(0.0, 1.0 - dist / (0.18 * intensity + 0.001)), 2.0) * intensity; color.rgb += portalColor * bloom * 2.5 + vec3(1.0) * bloom * 0.6; float vignette = smoothstep(0.5, 0.2, dist) * intensity * 0.5; color.rgb *= 1.0 - vignette * 0.4; float flash = smoothstep(0.82, 1.0, progress); color.rgb = mix(color.rgb, vec3(1.0), flash); gl_FragColor = color; } `, }; export const warpPass = new ShaderPass(WarpShader); warpPass.enabled = false; composer.addPass(warpPass); export function startWarp(portalMesh) { S.isWarping = true; S.warpNavigated = false; S.warpStartTime = clock.getElapsedTime(); warpPass.enabled = true; warpPass.uniforms['time'].value = 0.0; warpPass.uniforms['progress'].value = 0.0; if (portalMesh) { S.warpDestinationUrl = portalMesh.userData.destinationUrl || null; S.warpPortalColor = portalMesh.userData.portalColor ? portalMesh.userData.portalColor.clone() : new THREE.Color(0x4488ff); } else { S.warpDestinationUrl = null; S.warpPortalColor = new THREE.Color(0x4488ff); } warpPass.uniforms['portalColor'].value = S.warpPortalColor; } // clock is created here and exported export const clock = new THREE.Clock(); // === FLOATING CRYSTALS & LIGHTNING ARCS === const CRYSTAL_COUNT = 5; const CRYSTAL_BASE_POSITIONS = [ new THREE.Vector3(-4.5, 3.2, -3.8), new THREE.Vector3( 4.8, 2.8, -4.0), new THREE.Vector3(-5.5, 4.0, 1.5), new THREE.Vector3( 5.2, 3.5, 2.0), new THREE.Vector3( 0.0, 5.0, -5.5), ]; export const CRYSTAL_COLORS = [0xff6440, 0x40a0ff, 0x40ff8c, 0xc840ff, 0xffd700]; const crystalGroupObj = new THREE.Group(); scene.add(crystalGroupObj); export const crystals = []; for (let i = 0; i < CRYSTAL_COUNT; i++) { const geo = new THREE.OctahedronGeometry(0.35, 0); const color = CRYSTAL_COLORS[i]; const mat = new THREE.MeshStandardMaterial({ color, emissive: new THREE.Color(color).multiplyScalar(0.6), roughness: 0.05, metalness: 0.3, transparent: true, opacity: 0.88, }); const mesh = new THREE.Mesh(geo, mat); const basePos = CRYSTAL_BASE_POSITIONS[i].clone(); mesh.position.copy(basePos); mesh.userData.zoomLabel = 'Crystal'; crystalGroupObj.add(mesh); const light = new THREE.PointLight(color, 0.3, 6); light.position.copy(basePos); crystalGroupObj.add(light); crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2, flashStartTime: -999 }); } // Lightning arc pool export const LIGHTNING_POOL_SIZE = 6; const LIGHTNING_SEGMENTS = 8; export const LIGHTNING_REFRESH_MS = 130; export const lightningArcs = []; export const lightningArcMeta = []; for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) { const positions = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3); const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const mat = new THREE.LineBasicMaterial({ color: 0x88ccff, transparent: true, opacity: 0.0, blending: THREE.AdditiveBlending, depthWrite: false, }); const arc = new THREE.Line(geo, mat); scene.add(arc); lightningArcs.push(arc); lightningArcMeta.push({ active: false, baseOpacity: 0, srcIdx: 0, dstIdx: 0 }); } function buildLightningPath(start, end, jagAmount) { const out = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3); for (let s = 0; s <= LIGHTNING_SEGMENTS; s++) { const t = s / LIGHTNING_SEGMENTS; const x = start.x + (end.x - start.x) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0); const y = start.y + (end.y - start.y) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0); const z = start.z + (end.z - start.z) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0); out[s * 3] = x; out[s * 3 + 1] = y; out[s * 3 + 2] = z; } return out; } export function totalActivity() { const vals = Object.values(zoneIntensity); return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1); } export function lerpColor(colorA, colorB, t) { const ar = (colorA >> 16) & 0xff, ag = (colorA >> 8) & 0xff, ab = colorA & 0xff; const br = (colorB >> 16) & 0xff, bg = (colorB >> 8) & 0xff, bb = colorB & 0xff; const r = Math.round(ar + (br - ar) * t); const g = Math.round(ag + (bg - ag) * t); const b = Math.round(ab + (bb - ab) * t); return (r << 16) | (g << 8) | b; } export function updateLightningArcs(elapsed) { const activity = totalActivity(); const activeCount = Math.round(activity * LIGHTNING_POOL_SIZE); for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) { const arc = lightningArcs[i]; const meta = lightningArcMeta[i]; if (i >= activeCount) { arc.material.opacity = 0; meta.active = false; continue; } const a = Math.floor(Math.random() * CRYSTAL_COUNT); let b = Math.floor(Math.random() * (CRYSTAL_COUNT - 1)); if (b >= a) b++; const jagAmount = 0.45 + activity * 0.85; const path = buildLightningPath(crystals[a].mesh.position, crystals[b].mesh.position, jagAmount); const attr = arc.geometry.attributes.position; attr.array.set(path); attr.needsUpdate = true; arc.material.color.setHex(lerpColor(CRYSTAL_COLORS[a], CRYSTAL_COLORS[b], 0.5)); const base = (0.35 + Math.random() * 0.55) * Math.min(activity * 1.5, 1.0); arc.material.opacity = base; meta.active = true; meta.baseOpacity = base; meta.srcIdx = a; meta.dstIdx = b; crystals[a].flashStartTime = elapsed; crystals[b].flashStartTime = elapsed; } } // === BATCAVE AREA === const BATCAVE_ORIGIN = new THREE.Vector3(-10, 0, -8); export const batcaveGroup = new THREE.Group(); batcaveGroup.position.copy(BATCAVE_ORIGIN); scene.add(batcaveGroup); const batcaveProbeTarget = new THREE.WebGLCubeRenderTarget(128, { type: THREE.HalfFloatType, generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter, }); export const batcaveProbe = new THREE.CubeCamera(0.1, 80, batcaveProbeTarget); batcaveProbe.position.set(0, 1.2, -1); batcaveGroup.add(batcaveProbe); const batcaveFloorMat = new THREE.MeshStandardMaterial({ color: 0x0d1520, metalness: 0.92, roughness: 0.08, envMapIntensity: 1.4, }); const batcaveWallMat = new THREE.MeshStandardMaterial({ color: 0x0a1828, metalness: 0.85, roughness: 0.15, emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.03), envMapIntensity: 1.2, }); const batcaveConsoleMat = new THREE.MeshStandardMaterial({ color: 0x060e16, metalness: 0.95, roughness: 0.05, envMapIntensity: 1.6, }); export const batcaveMetallicMats = [batcaveFloorMat, batcaveWallMat, batcaveConsoleMat]; export const batcaveProbeTarget_texture = batcaveProbeTarget; const batcaveFloor = new THREE.Mesh(new THREE.BoxGeometry(6, 0.08, 6), batcaveFloorMat); batcaveFloor.position.y = -0.04; batcaveGroup.add(batcaveFloor); const batcaveBackWall = new THREE.Mesh(new THREE.BoxGeometry(6, 3, 0.1), batcaveWallMat); batcaveBackWall.position.set(0, 1.5, -3); batcaveGroup.add(batcaveBackWall); const batcaveLeftWall = new THREE.Mesh(new THREE.BoxGeometry(0.1, 3, 6), batcaveWallMat); batcaveLeftWall.position.set(-3, 1.5, 0); batcaveGroup.add(batcaveLeftWall); const batcaveConsoleBase = new THREE.Mesh(new THREE.BoxGeometry(3, 0.7, 1.2), batcaveConsoleMat); batcaveConsoleBase.position.set(0, 0.35, -1.5); batcaveGroup.add(batcaveConsoleBase); const batcaveScreenBezel = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.4, 0.06), batcaveConsoleMat); batcaveScreenBezel.position.set(0, 1.4, -2.08); batcaveScreenBezel.rotation.x = Math.PI * 0.08; batcaveGroup.add(batcaveScreenBezel); const batcaveScreenGlow = new THREE.Mesh( new THREE.PlaneGeometry(2.2, 1.1), new THREE.MeshBasicMaterial({ color: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.65), transparent: true, opacity: 0.82, }) ); batcaveScreenGlow.position.set(0, 1.4, -2.05); batcaveScreenGlow.rotation.x = Math.PI * 0.08; batcaveGroup.add(batcaveScreenGlow); const batcaveLight = new THREE.PointLight(NEXUS.colors.accent, 0.9, 14); batcaveLight.position.set(0, 2.8, -1); batcaveGroup.add(batcaveLight); const batcaveCeilingStrip = new THREE.Mesh( new THREE.BoxGeometry(4.2, 0.05, 0.14), new THREE.MeshStandardMaterial({ color: NEXUS.colors.accent, emissive: new THREE.Color(NEXUS.colors.accent), emissiveIntensity: 1.1, }) ); batcaveCeilingStrip.position.set(0, 2.95, -1.2); batcaveGroup.add(batcaveCeilingStrip); batcaveGroup.traverse(obj => { if (obj.isMesh) obj.userData.zoomLabel = 'Batcave'; });