From 3b97978a504399a1b1e51bac2b0e63a841a45ccc Mon Sep 17 00:00:00 2001 From: Dejvino Date: Fri, 21 Nov 2025 20:00:24 +0100 Subject: [PATCH] Feature: glass windows --- party-cathedral/src/core/animate.js | 16 +- party-cathedral/src/main.js | 1 + party-cathedral/src/scene/light-ball.js | 61 ++++++++ party-cathedral/src/scene/room-walls.js | 4 +- party-cathedral/src/scene/root.js | 7 +- .../src/scene/stained-glass-window.js | 148 +++++++++++++++++- 6 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 party-cathedral/src/scene/light-ball.js diff --git a/party-cathedral/src/core/animate.js b/party-cathedral/src/core/animate.js index f08300d..2b2bf32 100644 --- a/party-cathedral/src/core/animate.js +++ b/party-cathedral/src/core/animate.js @@ -7,32 +7,32 @@ function updateCamera() { const globalTime = Date.now() * 0.0001; const lookAtTime = Date.now() * 0.0002; - const camAmplitude = 4.0; - const lookAmplitude = 15.4; + const camAmplitude = 1.0; + const lookAmplitude = 8.0; // Base Camera Position in front of the TV const baseX = 0; const baseY = 1.6; - const baseZ = 0.0; + const baseZ = 10.0; // Base LookAt target (Center of the screen) const baseTargetX = 0; const baseTargetY = 1.6; - const baseTargetZ = 5.0; + const baseTargetZ = -10.0; // Camera Position Offsets (Drift) const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude; const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.1; - const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude * 1.2; + const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude; state.camera.position.x = baseX + camOffsetX; state.camera.position.y = baseY + camOffsetY; state.camera.position.z = baseZ + camOffsetZ; // LookAt Target Offsets (Subtle Gaze Shift) - const lookOffsetX = Math.sin(lookAtTime * 1.5) * lookAmplitude * 4; - const lookOffsetZ = Math.cos(lookAtTime * 2.5) * lookAmplitude * 4; - const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude; + const lookOffsetX = Math.sin(lookAtTime * 1.5) * lookAmplitude; + const lookOffsetZ = Math.cos(lookAtTime * 2.5) * lookAmplitude; + const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude * 0.5; // Apply lookAt to the subtly shifted target state.camera.lookAt(baseTargetX + lookOffsetX, baseTargetY + lookOffsetY, baseTargetZ + lookOffsetZ); diff --git a/party-cathedral/src/main.js b/party-cathedral/src/main.js index a19d709..eae66cb 100644 --- a/party-cathedral/src/main.js +++ b/party-cathedral/src/main.js @@ -1,5 +1,6 @@ import * as THREE from 'three'; import { init } from './core/init.js'; +import { StainedGlass } from './scene/stained-glass-window.js'; // Start everything init(); \ No newline at end of file diff --git a/party-cathedral/src/scene/light-ball.js b/party-cathedral/src/scene/light-ball.js new file mode 100644 index 0000000..a5df2ca --- /dev/null +++ b/party-cathedral/src/scene/light-ball.js @@ -0,0 +1,61 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; + +export class LightBall extends SceneFeature { + constructor() { + super(); + this.ball = null; + this.light = null; + sceneFeatureManager.register(this); + } + + init() { + // --- Dimensions from room-walls.js for positioning --- + const naveWidth = 12; + const naveHeight = 15; + const length = 40; + + // --- Ball Properties --- + const ballRadius = 1.0; + const ballColor = 0xffffff; // White light + const lightIntensity = 6.0; + + // --- Create the Ball --- + const ballGeometry = new THREE.SphereGeometry(ballRadius, 32, 32); + const ballMaterial = new THREE.MeshBasicMaterial({ color: ballColor, emissive: ballColor, emissiveIntensity: 1.0 }); + this.ball = new THREE.Mesh(ballGeometry, ballMaterial); + this.ball.castShadow = false; + this.ball.receiveShadow = false; + + // --- Create the Light --- + this.light = new THREE.PointLight(ballColor, lightIntensity, length / 2); // Adjust range to cathedral size + this.light.castShadow = true; + this.light.shadow.mapSize.width = 512; + this.light.shadow.mapSize.height = 512; + this.light.shadow.camera.near = 0.1; + this.light.shadow.camera.far = length / 2; + + // --- Initial Position --- + this.ball.position.set(0, naveHeight * 0.7, 0); // Near the ceiling + this.light.position.copy(this.ball.position); + + state.scene.add(this.ball); + state.scene.add(this.light); + } + + update(deltaTime) { + // --- Animate the Ball --- + const time = state.clock.getElapsedTime(); + const driftSpeed = 0.5; + const driftAmplitude = 10.0; + + this.ball.position.x = Math.sin(time * driftSpeed) * driftAmplitude; + this.ball.position.y = 10 + Math.cos(time * driftSpeed * 1.3) * driftAmplitude * 0.5; // bobbing + this.ball.position.z = Math.cos(time * driftSpeed * 0.7) * driftAmplitude; + this.light.position.copy(this.ball.position); + } +} + +new LightBall(); \ No newline at end of file diff --git a/party-cathedral/src/scene/room-walls.js b/party-cathedral/src/scene/room-walls.js index 6d79b6e..b48b9d9 100644 --- a/party-cathedral/src/scene/room-walls.js +++ b/party-cathedral/src/scene/room-walls.js @@ -30,10 +30,12 @@ export class RoomWalls extends SceneFeature { wallTexture.wrapS = THREE.RepeatWrapping; wallTexture.wrapT = THREE.RepeatWrapping; - const wallMaterial = new THREE.MeshPhongMaterial({ + const wallMaterial = new THREE.MeshStandardMaterial({ map: wallTexture, side: THREE.DoubleSide, shininess: 5, + roughness: 0.2, + metalness: 0.1, specular: 0x111111 }); diff --git a/party-cathedral/src/scene/root.js b/party-cathedral/src/scene/root.js index d9863ed..29c0139 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -2,7 +2,10 @@ import * as THREE from 'three'; import { state } from '../state.js'; import floorTextureUrl from '/textures/stone_floor.png'; import sceneFeatureManager from './SceneFeatureManager.js'; +// Scene Features registered here: import { RoomWalls } from './room-walls.js'; +import { LightBall } from './light-ball.js'; +// Scene Features ^^^ // --- Scene Modeling Function --- export function createSceneObjects() { @@ -26,11 +29,11 @@ export function createSceneObjects() { state.scene.add(floor); // 3. Lighting (Minimal and focused) - const ambientLight = new THREE.AmbientLight(0x606060, 1.5); // Increased ambient light for a larger space + const ambientLight = new THREE.AmbientLight(0x606060, 0.1); // Increased ambient light for a larger space state.scene.add(ambientLight); // Add a HemisphereLight for more natural, general illumination in a large space. - const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.7); + const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.2); state.scene.add(hemisphereLight); } diff --git a/party-cathedral/src/scene/stained-glass-window.js b/party-cathedral/src/scene/stained-glass-window.js index 7bcc407..7aa7f82 100644 --- a/party-cathedral/src/scene/stained-glass-window.js +++ b/party-cathedral/src/scene/stained-glass-window.js @@ -1 +1,147 @@ -// This file will contain the Three.js code for creating colorful stained glass windows with light effects. \ No newline at end of file +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; + +export class StainedGlass extends SceneFeature { + constructor() { + super(); + this.windows = []; + sceneFeatureManager.register(this); + } + + init() { + // --- Dimensions from room-walls.js for positioning --- + const length = 40; + const naveWidth = 12; + const aisleWidth = 6; + const totalWidth = naveWidth + 2 * aisleWidth; + const aisleHeight = 8; + + // --- Window Properties --- + const windowWidth = 3; + const windowBaseHeight = 5; + const windowArchHeight = 1.5; + const numWindowsPerSide = 4; + const windowSpacing = length / numWindowsPerSide; + + // --- Procedural Material --- + const material = new THREE.MeshStandardMaterial({ + vertexColors: true, // Use colors assigned to vertices + side: THREE.DoubleSide, + metalness: 0.1, // Glass is not very metallic + roughness: 0.3, // Glass is smooth + clearcoat: 1.0, + emissive: 0x000000, // We will control emissiveness via update + }); + + // --- Procedural Geometry Generation --- + const createProceduralWindowGeometry = () => { + const segmentsX = 8; + const segmentsY = 12; + const vertices = []; + const colors = []; + const normals = []; + + const colorPalette = [ + new THREE.Color(0x6A0DAD), // Purple + new THREE.Color(0x00008B), // Dark Blue + new THREE.Color(0xB22222), // Firebrick Red + new THREE.Color(0xFFD700), // Gold + new THREE.Color(0x006400), // Dark Green + new THREE.Color(0x8B0000), // Dark Red + new THREE.Color(0x4B0082), // Indigo + ]; + + const randomnessFactor = 0.4; // How much to vary the normals + + const addTriangle = (v1, v2, v3) => { + const color = colorPalette[Math.floor(Math.random() * colorPalette.length)]; + vertices.push(v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z); + colors.push(color.r, color.g, color.b, color.r, color.g, color.b, color.r, color.g, color.b); + + // Calculate the base normal for the flat triangle face + const edge1 = new THREE.Vector3().subVectors(v2, v1); + const edge2 = new THREE.Vector3().subVectors(v3, v1); + const faceNormal = new THREE.Vector3().crossVectors(edge1, edge2).normalize(); + + // Introduce a random vector to alter the normal + const randomVec = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); + faceNormal.add(randomVec.multiplyScalar(randomnessFactor)).normalize(); + + // Apply the same randomized normal to all 3 vertices for a faceted look + normals.push(faceNormal.x, faceNormal.y, faceNormal.z, faceNormal.x, faceNormal.y, faceNormal.z, faceNormal.x, faceNormal.y, faceNormal.z); + }; + + // Create rectangular part + for (let i = 0; i < segmentsX; i++) { + for (let j = 0; j < segmentsY; j++) { + const x = -windowWidth / 2 + (i * windowWidth) / segmentsX; + const y = (j * windowBaseHeight) / segmentsY; + const x2 = x + windowWidth / segmentsX; + const y2 = y + windowBaseHeight / segmentsY; + + const v1 = new THREE.Vector3(x, y, 0); + const v2 = new THREE.Vector3(x2, y, 0); + const v3 = new THREE.Vector3(x, y2, 0); + const v4 = new THREE.Vector3(x2, y2, 0); + addTriangle(v1, v2, v3); + addTriangle(v2, v4, v3); + } + } + + // Create arch part + const archCenter = new THREE.Vector3(0, windowBaseHeight, 0); + for (let i = 0; i < segmentsX * 2; i++) { + const angle1 = (i / (segmentsX * 2)) * Math.PI; + const angle2 = ((i + 1) / (segmentsX * 2)) * Math.PI; + const v1 = archCenter; + const v2 = new THREE.Vector3(Math.cos(angle1) * -windowWidth / 2, Math.sin(angle1) * windowArchHeight + windowBaseHeight, 0); + const v3 = new THREE.Vector3(Math.cos(angle2) * -windowWidth / 2, Math.sin(angle2) * windowArchHeight + windowBaseHeight, 0); + addTriangle(v1, v2, v3); + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); + return geometry; + }; + + // --- Create and Place Windows --- + const createAndPlaceWindow = (position, rotationY) => { + const geometry = createProceduralWindowGeometry(); // Generate unique geometry for each window + const windowMesh = new THREE.Mesh(geometry, material); + windowMesh.position.copy(position); + windowMesh.rotation.y = rotationY; + state.scene.add(windowMesh); + this.windows.push(windowMesh); + }; + + for (let i = 0; i < numWindowsPerSide; i++) { + const z = -length / 2 + windowSpacing * (i + 0.5); + const y = 0; // Place them starting from the floor + + // Left side + createAndPlaceWindow(new THREE.Vector3(-totalWidth / 2 + 0.01, y, z), Math.PI / 2); + // Right side + createAndPlaceWindow(new THREE.Vector3(totalWidth / 2 - 0.01, y, z), -Math.PI / 2); + } + } + + update(deltaTime) { + // Add a subtle pulsing glow to the windows + const pulseSpeed = 0.5; + const minIntensity = 0.5; + const maxIntensity = 0.9; + const intensity = minIntensity + (maxIntensity - minIntensity) * (0.5 * (1 + Math.sin(state.clock.getElapsedTime() * pulseSpeed))); + + // To make the glow match the vertex colors, we set the emissive color to white + // and modulate its intensity. The final glow color will be vertexColor * emissive * emissiveIntensity. + this.windows.forEach(w => { + w.material.emissiveIntensity = intensity; + }); + } +} + +new StainedGlass(); \ No newline at end of file