diff --git a/party-cathedral/src/scene/root.js b/party-cathedral/src/scene/root.js index 8cb9ecb..89e01f1 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -17,6 +17,7 @@ import { RoseWindowLight } from './rose-window-light.js'; import { RoseWindowLightshafts } from './rose-window-lightshafts.js'; import { StainedGlass } from './stained-glass-window.js'; import { MusicPlayer } from './music-player.js'; +import { WallCurtain } from './wall-curtain.js'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-cathedral/src/scene/wall-curtain.js b/party-cathedral/src/scene/wall-curtain.js new file mode 100644 index 0000000..62940c3 --- /dev/null +++ b/party-cathedral/src/scene/wall-curtain.js @@ -0,0 +1,90 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; +import curtainTextureUrl from '/textures/tapestry.png'; + +export class WallCurtain extends SceneFeature { + constructor() { + super(); + this.curtains = []; + sceneFeatureManager.register(this); + } + + init() { + // --- Curtain Properties --- + const naveWidth = 12; + const naveHeight = 7; + const stageHeight = 1.5; + const curtainWidth = naveWidth; // Span the width of the nave + const curtainHeight = naveHeight - stageHeight; // Hang from the ceiling down to the stage + const segmentsX = 50; // More segments for a smoother wave + const segmentsY = 50; + + // --- Texture --- + const texture = state.loader.load(curtainTextureUrl); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(5, 1); // Repeat the texture 5 times horizontally + + // --- Material --- + const material = new THREE.MeshStandardMaterial({ + map: texture, + side: THREE.DoubleSide, + roughness: 0.9, + metalness: 0.1, + }); + + // --- Create and Place Curtains --- + const createAndPlaceCurtain = (position, rotationY) => { + const geometry = new THREE.PlaneGeometry(curtainWidth, curtainHeight, segmentsX, segmentsY); + const originalPositions = geometry.attributes.position.clone(); + const curtainMesh = new THREE.Mesh(geometry, material); + curtainMesh.position.copy(position); + curtainMesh.rotation.y = rotationY; + curtainMesh.castShadow = true; + curtainMesh.receiveShadow = true; + state.scene.add(curtainMesh); + + this.curtains.push({ + mesh: curtainMesh, + originalPositions: originalPositions, + }); + }; + + // Place a single large curtain behind the stage + const backWallZ = -20; + const curtainY = stageHeight + curtainHeight / 2; + const curtainPosition = new THREE.Vector3(0, curtainY, backWallZ + 0.1); + + createAndPlaceCurtain(curtainPosition, 0); // No rotation needed + } + + update(deltaTime) { + if (!this.waving) { return; } + const time = state.clock.getElapsedTime(); + const waveSpeed = 1.5; + const waveFrequency = 0.5; + const waveAmplitude = 0.2; + + this.curtains.forEach(curtain => { + const positions = curtain.mesh.geometry.attributes.position; + const originalPos = curtain.originalPositions; + + for (let i = 0; i < positions.count; i++) { + const originalX = originalPos.getX(i); + // The wave now moves horizontally across the curtain + const zOffset = Math.sin(originalX * waveFrequency + time * waveSpeed) * waveAmplitude; + positions.setZ(i, originalPos.getZ(i) + zOffset); + } + + // Mark positions as needing an update + positions.needsUpdate = true; + + // Recalculate normals for correct lighting on the waving surface + curtain.mesh.geometry.computeVertexNormals(); + }); + } +} + +new WallCurtain(); \ No newline at end of file diff --git a/party-cathedral/textures/tapestry.png b/party-cathedral/textures/tapestry.png new file mode 100644 index 0000000..245f5ab Binary files /dev/null and b/party-cathedral/textures/tapestry.png differ