diff --git a/app.js b/app.js index cbc33f1..627498b 100644 --- a/app.js +++ b/app.js @@ -1201,6 +1201,133 @@ function updateLightningArcs() { } } +// === BATCAVE AREA === +// Dark metallic workshop terminal positioned back-left of the main glass platform. +// A CubeCamera reflection probe captures the local environment and applies it +// to all high-metalness surfaces in this area for physically-based reflections. + +const BATCAVE_ORIGIN = new THREE.Vector3(-10, 0, -8); + +const batcaveGroup = new THREE.Group(); +batcaveGroup.position.copy(BATCAVE_ORIGIN); +scene.add(batcaveGroup); + +// Reflection probe — 128-px cube render target for PBR metallic surfaces +const batcaveProbeTarget = new THREE.WebGLCubeRenderTarget(128, { + type: THREE.HalfFloatType, + generateMipmaps: true, + minFilter: THREE.LinearMipmapLinearFilter, +}); +const batcaveProbe = new THREE.CubeCamera(0.1, 80, batcaveProbeTarget); +batcaveProbe.position.set(0, 1.2, -1); // centred above the console +batcaveGroup.add(batcaveProbe); + +// Brushed-steel floor panels +const batcaveFloorMat = new THREE.MeshStandardMaterial({ + color: 0x0d1520, + metalness: 0.92, + roughness: 0.08, + envMapIntensity: 1.4, +}); + +// Anodised wall panels with faint accent tint +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, +}); + +// High-gloss terminal console surface +const batcaveConsoleMat = new THREE.MeshStandardMaterial({ + color: 0x060e16, + metalness: 0.95, + roughness: 0.05, + envMapIntensity: 1.6, +}); + +// All metallic mats that receive the reflection probe envMap +const batcaveMetallicMats = [batcaveFloorMat, batcaveWallMat, batcaveConsoleMat]; + +// Floor slab +const batcaveFloor = new THREE.Mesh( + new THREE.BoxGeometry(6, 0.08, 6), + batcaveFloorMat +); +batcaveFloor.position.y = -0.04; +batcaveGroup.add(batcaveFloor); + +// Back wall +const batcaveBackWall = new THREE.Mesh( + new THREE.BoxGeometry(6, 3, 0.1), + batcaveWallMat +); +batcaveBackWall.position.set(0, 1.5, -3); +batcaveGroup.add(batcaveBackWall); + +// Left side wall +const batcaveLeftWall = new THREE.Mesh( + new THREE.BoxGeometry(0.1, 3, 6), + batcaveWallMat +); +batcaveLeftWall.position.set(-3, 1.5, 0); +batcaveGroup.add(batcaveLeftWall); + +// Console desk base +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); + +// Console screen bezel +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); + +// Screen glow face +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); + +// Workshop point light — cool blue for metallic ambience +const batcaveLight = new THREE.PointLight(NEXUS.colors.accent, 0.9, 14); +batcaveLight.position.set(0, 2.8, -1); +batcaveGroup.add(batcaveLight); + +// Ceiling strip emissive bar +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'; +}); + +// Probe state — timestamp of last reflection capture (seconds) +let batcaveProbeLastUpdate = -999; + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -1244,6 +1371,18 @@ function animate() { // Subtle pulse on constellation opacity constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + // Batcave reflection probe — refresh every 2 s to capture dynamic scene changes + if (elapsed - batcaveProbeLastUpdate > 2.0) { + batcaveProbeLastUpdate = elapsed; + batcaveGroup.visible = false; // hide self to avoid self-capture artefacts + batcaveProbe.update(renderer, scene); + batcaveGroup.visible = true; + for (const mat of batcaveMetallicMats) { + mat.envMap = batcaveProbeTarget.texture; + mat.needsUpdate = true; + } + } + // Glass platform — ripple edge glow outward from centre for (const { mat, distFromCenter } of glassEdgeMaterials) { const phase = elapsed * 1.1 - distFromCenter * 0.18;