// === SCENE SETUP + LIGHTING + SHADOWS + STAR FIELD + CONSTELLATION LINES === import * as THREE from 'three'; import { NEXUS } from './constants.js'; // === SCENE SETUP === export const scene = new THREE.Scene(); export const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); camera.position.set(0, 6, 11); export const raycaster = new THREE.Raycaster(); export const forwardVector = new THREE.Vector3(); // === LIGHTING === export const ambientLight = new THREE.AmbientLight(0x0a1428, 1.4); scene.add(ambientLight); export const overheadLight = new THREE.SpotLight(0x8899bb, 0.6, 80, Math.PI / 3.5, 0.5, 1.0); overheadLight.position.set(0, 25, 0); overheadLight.target.position.set(0, 0, 0); overheadLight.castShadow = true; overheadLight.shadow.mapSize.set(2048, 2048); overheadLight.shadow.camera.near = 5; overheadLight.shadow.camera.far = 60; overheadLight.shadow.bias = -0.001; scene.add(overheadLight); scene.add(overheadLight.target); export const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setClearColor(0x000000, 0); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // === SHADOW SYSTEM === renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); // === STAR FIELD === const STAR_COUNT = 800; const STAR_SPREAD = 400; const CONSTELLATION_DISTANCE = 30; const starPositions = []; const starGeo = new THREE.BufferGeometry(); const posArray = new Float32Array(STAR_COUNT * 3); const sizeArray = new Float32Array(STAR_COUNT); for (let i = 0; i < STAR_COUNT; i++) { const x = (Math.random() - 0.5) * STAR_SPREAD; const y = (Math.random() - 0.5) * STAR_SPREAD; const z = (Math.random() - 0.5) * STAR_SPREAD; posArray[i * 3] = x; posArray[i * 3 + 1] = y; posArray[i * 3 + 2] = z; sizeArray[i] = Math.random() * 2.5 + 0.5; starPositions.push(new THREE.Vector3(x, y, z)); } starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); starGeo.setAttribute('size', new THREE.BufferAttribute(sizeArray, 1)); export const starMaterial = new THREE.PointsMaterial({ color: NEXUS.colors.starCore, size: 0.6, sizeAttenuation: true, transparent: true, opacity: 0.9, }); export const stars = new THREE.Points(starGeo, starMaterial); scene.add(stars); // Star pulse state export const STAR_BASE_OPACITY = 0.3; export const STAR_PEAK_OPACITY = 1.0; export const STAR_PULSE_DECAY = 0.012; // === CONSTELLATION LINES === function buildConstellationLines() { const linePositions = []; const MAX_CONNECTIONS_PER_STAR = 3; const connectionCount = new Array(STAR_COUNT).fill(0); for (let i = 0; i < STAR_COUNT; i++) { if (connectionCount[i] >= MAX_CONNECTIONS_PER_STAR) continue; const neighbors = []; for (let j = i + 1; j < STAR_COUNT; j++) { if (connectionCount[j] >= MAX_CONNECTIONS_PER_STAR) continue; const dist = starPositions[i].distanceTo(starPositions[j]); if (dist < CONSTELLATION_DISTANCE) { neighbors.push({ j, dist }); } } neighbors.sort((a, b) => a.dist - b.dist); const toConnect = neighbors.slice(0, MAX_CONNECTIONS_PER_STAR - connectionCount[i]); for (const { j } of toConnect) { linePositions.push( starPositions[i].x, starPositions[i].y, starPositions[i].z, starPositions[j].x, starPositions[j].y, starPositions[j].z ); connectionCount[i]++; connectionCount[j]++; } } const lineGeo = new THREE.BufferGeometry(); lineGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePositions), 3)); const lineMat = new THREE.LineBasicMaterial({ color: NEXUS.colors.constellationLine, transparent: true, opacity: 0.18, }); return new THREE.LineSegments(lineGeo, lineMat); } export const constellationLines = buildConstellationLines(); scene.add(constellationLines);