import * as THREE from 'three'; import { getQualityTier } from './quality.js'; let rainParticles; let rainPositions; let rainVelocities; let rainCount = 0; let skipFrames = 0; // 0 = update every frame, 1 = every 2nd frame let frameCounter = 0; export function initEffects(scene) { const tier = getQualityTier(); skipFrames = tier === 'low' ? 1 : 0; // Low tier: update rain every 2nd frame initMatrixRain(scene, tier); initStarfield(scene, tier); } function initMatrixRain(scene, tier) { // Scale particle count by quality tier rainCount = tier === 'low' ? 500 : tier === 'medium' ? 1200 : 2000; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(rainCount * 3); const velocities = new Float32Array(rainCount); const colors = new Float32Array(rainCount * 3); for (let i = 0; i < rainCount; i++) { positions[i * 3] = (Math.random() - 0.5) * 100; positions[i * 3 + 1] = Math.random() * 50 + 5; positions[i * 3 + 2] = (Math.random() - 0.5) * 100; velocities[i] = 0.05 + Math.random() * 0.15; const brightness = 0.3 + Math.random() * 0.7; colors[i * 3] = 0; colors[i * 3 + 1] = brightness; colors[i * 3 + 2] = 0; } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); rainPositions = positions; rainVelocities = velocities; const mat = new THREE.PointsMaterial({ size: tier === 'low' ? 0.16 : 0.12, vertexColors: true, transparent: true, opacity: 0.7, sizeAttenuation: true, }); rainParticles = new THREE.Points(geo, mat); scene.add(rainParticles); } function initStarfield(scene, tier) { const count = tier === 'low' ? 150 : tier === 'medium' ? 350 : 500; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); for (let i = 0; i < count; i++) { positions[i * 3] = (Math.random() - 0.5) * 300; positions[i * 3 + 1] = Math.random() * 80 + 10; positions[i * 3 + 2] = (Math.random() - 0.5) * 300; } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color: 0x003300, size: 0.08, transparent: true, opacity: 0.5, }); const stars = new THREE.Points(geo, mat); scene.add(stars); } export function updateEffects(_time) { if (!rainParticles) return; // On low tier, skip every other frame to halve iteration cost if (skipFrames > 0) { frameCounter++; if (frameCounter % (skipFrames + 1) !== 0) return; } // When skipping frames, multiply velocity to maintain visual speed const velocityMul = skipFrames > 0 ? (skipFrames + 1) : 1; for (let i = 0; i < rainCount; i++) { rainPositions[i * 3 + 1] -= rainVelocities[i] * velocityMul; if (rainPositions[i * 3 + 1] < -1) { rainPositions[i * 3 + 1] = 40 + Math.random() * 20; rainPositions[i * 3] = (Math.random() - 0.5) * 100; rainPositions[i * 3 + 2] = (Math.random() - 0.5) * 100; } } rainParticles.geometry.attributes.position.needsUpdate = true; } /** * Dispose all effect resources (used on world teardown). */ export function disposeEffects() { if (rainParticles) { rainParticles.geometry.dispose(); rainParticles.material.dispose(); rainParticles = null; } rainPositions = null; rainVelocities = null; rainCount = 0; frameCounter = 0; }