// modules/core/scene.js — Three.js scene setup import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { BokehPass } from 'three/addons/postprocessing/BokehPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { THEME } from './theme.js'; export let scene, camera, renderer, composer, orbitControls, bokehPass; export const raycaster = new THREE.Raycaster(); export const forwardVector = new THREE.Vector3(); export const clock = new THREE.Clock(); // Loading manager export const loadedAssets = new Map(); export const loadingManager = new THREE.LoadingManager(); // Placeholder texture let placeholderTexture; // Lights (exported for oath dimming) export let ambientLight, overheadLight; // Warp shader pass export let warpPass; 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 function initScene(onLoadComplete) { // Loading manager setup loadingManager.onLoad = () => { document.getElementById('loading-bar').style.width = '100%'; document.getElementById('loading').style.display = 'none'; if (onLoadComplete) onLoadComplete(); }; loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => { const progress = (itemsLoaded / itemsTotal) * 100; document.getElementById('loading-bar').style.width = `${progress}%`; }; // Placeholder texture const _placeholderCanvas = document.createElement('canvas'); _placeholderCanvas.width = 64; _placeholderCanvas.height = 64; const _placeholderCtx = _placeholderCanvas.getContext('2d'); _placeholderCtx.fillStyle = '#0a0a18'; _placeholderCtx.fillRect(0, 0, 64, 64); placeholderTexture = new THREE.CanvasTexture(_placeholderCanvas); loadedAssets.set('placeholder-texture', placeholderTexture); loadingManager.itemStart('placeholder-texture'); loadingManager.itemEnd('placeholder-texture'); // Scene scene = new THREE.Scene(); // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); camera.position.set(0, 6, 11); // Renderer — alpha:true so matrix rain canvas shows through renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setClearColor(0x000000, 0); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); // Lights ambientLight = new THREE.AmbientLight(0x0a1428, 1.4); scene.add(ambientLight); 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); // Post-processing composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); bokehPass = new BokehPass(scene, camera, { focus: 5.0, aperture: 0.00015, maxblur: 0.004, }); composer.addPass(bokehPass); // Warp pass warpPass = new ShaderPass(WarpShader); warpPass.enabled = false; composer.addPass(warpPass); // Controls orbitControls = new OrbitControls(camera, renderer.domElement); orbitControls.enableDamping = true; orbitControls.dampingFactor = 0.05; orbitControls.enabled = false; // Resize window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }); return { scene, camera, renderer, composer, orbitControls }; }