diff --git a/party-cathedral/src/scene/root.js b/party-cathedral/src/scene/root.js index d273540..b659329 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -9,6 +9,7 @@ import { Pews } from './pews.js'; import { Stage } from './stage.js'; import { MedievalMusicians } from './medieval-musicians.js'; import { PartyGuests } from './party-guests.js'; +import { StageTorches } from './stage-torches.js'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-cathedral/src/scene/stage-torches.js b/party-cathedral/src/scene/stage-torches.js new file mode 100644 index 0000000..0f28a8f --- /dev/null +++ b/party-cathedral/src/scene/stage-torches.js @@ -0,0 +1,117 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; +import sparkTextureUrl from '/textures/spark.png'; + +export class StageTorches extends SceneFeature { + constructor() { + super(); + this.torches = []; + sceneFeatureManager.register(this); + } + + init() { + // --- Stage Dimensions for positioning --- + const length = 40; + const naveWidth = 12; + const stageWidth = naveWidth - 1; + const stageHeight = 1.5; + const stageDepth = 5; + + const torchPositions = [ + new THREE.Vector3(-stageWidth / 2, stageHeight, -length / 2 + 0.5), + new THREE.Vector3(stageWidth / 2, stageHeight, -length / 2 + 0.5), + new THREE.Vector3(-stageWidth / 2, stageHeight, -length / 2 + stageDepth - 0.5), + new THREE.Vector3(stageWidth / 2, stageHeight, -length / 2 + stageDepth - 0.5), + ]; + + torchPositions.forEach(pos => { + const torch = this.createTorch(pos); + this.torches.push(torch); + state.scene.add(torch.group); + }); + } + + createTorch(position) { + const torchGroup = new THREE.Group(); + torchGroup.position.copy(position); + + // --- Torch Holder --- + const holderMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.6, metalness: 0.5 }); + const holderGeo = new THREE.CylinderGeometry(0.1, 0.15, 1.0, 12); + const holderMesh = new THREE.Mesh(holderGeo, holderMaterial); + holderMesh.position.y = 0.5; + holderMesh.castShadow = true; + holderMesh.receiveShadow = true; + torchGroup.add(holderMesh); + + // --- Point Light --- + const pointLight = new THREE.PointLight(0xffaa44, 2.5, 8); + pointLight.position.y = 1.2; + pointLight.castShadow = true; + pointLight.shadow.mapSize.width = 128; + pointLight.shadow.mapSize.height = 128; + torchGroup.add(pointLight); + + // --- Particle System for Fire --- + const particleCount = 50; + const particles = new THREE.BufferGeometry(); + const positions = []; + const particleData = []; + + const sparkTexture = state.loader.load(sparkTextureUrl); + const particleMaterial = new THREE.PointsMaterial({ + map: sparkTexture, + color: 0xffaa00, + size: 0.5, + blending: THREE.AdditiveBlending, + transparent: true, + depthWrite: false, + }); + + for (let i = 0; i < particleCount; i++) { + positions.push(0, 1, 0); + particleData.push({ + velocity: new THREE.Vector3((Math.random() - 0.5) * 0.2, Math.random() * 1.5, (Math.random() - 0.5) * 0.2), + life: Math.random() * 1.0, + }); + } + particles.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + const particleSystem = new THREE.Points(particles, particleMaterial); + torchGroup.add(particleSystem); + + return { group: torchGroup, light: pointLight, particles: particleSystem, particleData: particleData }; + } + + update(deltaTime) { + this.torches.forEach(torch => { + // --- Animate Particles --- + const positions = torch.particles.geometry.attributes.position.array; + for (let i = 0; i < torch.particleData.length; i++) { + const data = torch.particleData[i]; + data.life -= deltaTime; + + if (data.life <= 0) { + // Reset particle + positions[i * 3] = 0; + positions[i * 3 + 1] = 1; + positions[i * 3 + 2] = 0; + data.life = Math.random() * 1.0; + } else { + // Update position + positions[i * 3] += data.velocity.x * deltaTime; + positions[i * 3 + 1] += data.velocity.y * deltaTime; + positions[i * 3 + 2] += data.velocity.z * deltaTime; + } + } + torch.particles.geometry.attributes.position.needsUpdate = true; + + // --- Flicker Light --- + const flicker = Math.random() * 0.5; + torch.light.intensity = 2.0 + flicker; + }); + } +} + +new StageTorches(); \ No newline at end of file diff --git a/party-cathedral/textures/spark.png b/party-cathedral/textures/spark.png new file mode 100644 index 0000000..828a67f Binary files /dev/null and b/party-cathedral/textures/spark.png differ