Feature: glass windows

This commit is contained in:
Dejvino 2025-11-21 20:00:24 +01:00
parent bc961119b6
commit 3b97978a50
6 changed files with 225 additions and 12 deletions

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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
});

View File

@ -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);
}

View File

@ -1 +1,147 @@
// This file will contain the Three.js code for creating colorful stained glass windows with light effects.
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();