Compare commits

...

9 Commits

Author SHA1 Message Date
Dejvino
cdd90a4c57 Feature: torches on stage 2025-11-21 23:44:02 +01:00
Dejvino
c944fb0f4d Feature: add textures to wall and floor, make pillars round 2025-11-21 23:28:02 +01:00
Dejvino
d9dd5a56d1 Fix: improved light effect 2025-11-21 23:07:31 +01:00
Dejvino
1d1c8da558 Feature: main window 2025-11-21 22:44:18 +01:00
Dejvino
ea813c88be Feature: more colored windows 2025-11-21 22:41:16 +01:00
Dejvino
2811fdbbd8 Feature: more colored light balls 2025-11-21 22:16:30 +01:00
Dejvino
a4e48d1d5a Feature: party guests 2025-11-21 22:07:38 +01:00
Dejvino
56ec41a802 Tweak: better jumping 2025-11-21 21:47:00 +01:00
Dejvino
d36313df37 Feature: band of different musicians 2025-11-21 21:42:32 +01:00
18 changed files with 480 additions and 80 deletions

View File

@ -7,13 +7,13 @@ function updateCamera() {
const globalTime = Date.now() * 0.0001;
const lookAtTime = Date.now() * 0.0002;
const camAmplitude = 1.0;
const camAmplitude = new THREE.Vector3(1.0, 0.1, 10.0);
const lookAmplitude = 8.0;
// Base Camera Position in front of the TV
const baseX = 0;
const baseY = 1.6;
const baseZ = -10.0;
const baseY = 2.6;
const baseZ = 0.0;
// Base LookAt target (Center of the screen)
const baseTargetX = 0;
@ -21,9 +21,9 @@ function updateCamera() {
const baseTargetZ = -30.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;
const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude.x;
const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude.y;
const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude.z;
state.camera.position.x = baseX + camOffsetX;
state.camera.position.y = baseY + camOffsetY;

View File

@ -3,58 +3,65 @@ import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
// --- Dimensions from room-walls.js for positioning ---
const naveWidth = 12;
const naveHeight = 15;
const length = 40;
export class LightBall extends SceneFeature {
constructor() {
super();
this.ball = null;
this.light = null;
this.lightBalls = [];
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;
const ballRadius = 0.4;
const lightIntensity = 5.0;
const lightColors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0x00ffff, 0xff00ff]; // Red, Green, Blue, Yellow
// --- 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;
lightColors.forEach(color => {
// --- Create the Ball ---
const ballGeometry = new THREE.SphereGeometry(ballRadius, 32, 32);
const ballMaterial = new THREE.MeshBasicMaterial({ color: color, emissive: color, emissiveIntensity: 1.0 });
const ball = new THREE.Mesh(ballGeometry, ballMaterial);
ball.castShadow = false;
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;
// --- Create the Light ---
const light = new THREE.PointLight(color, lightIntensity, length / 1.5);
// --- Initial Position ---
this.ball.position.set(0, naveHeight * 0.7, 0); // Near the ceiling
this.light.position.copy(this.ball.position);
// --- Initial Position ---
ball.position.set(
(Math.random() - 0.5) * naveWidth,
naveHeight * 0.6 + Math.random() * 4,
(Math.random() - 0.5) * length * 0.8
);
light.position.copy(ball.position);
state.scene.add(this.ball);
state.scene.add(this.light);
state.scene.add(ball);
state.scene.add(light);
this.lightBalls.push({
mesh: ball,
light: light,
driftSpeed: 0.2 + Math.random() * 0.2,
driftAmplitude: 4.0 + Math.random() * 4.0,
offset: Math.random() * Math.PI * 6,
});
});
}
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);
this.lightBalls.forEach(lb => {
const { mesh, light, driftSpeed, offset } = lb;
mesh.position.x = Math.sin(time * driftSpeed + offset) * naveWidth/2 * 0.8;
mesh.position.y = 10 + Math.cos(time * driftSpeed * 1.3 + offset) * naveHeight/2 * 0.6;
mesh.position.z = Math.cos(time * driftSpeed * 0.7 + offset) * length/2 * 0.8;
light.position.copy(mesh.position);
});
}
}

View File

@ -2,7 +2,12 @@ import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
import musiciansTextureUrl from '/textures/musician1.png';
const musicianTextureUrls = [
'/textures/musician1.png',
'/textures/musician2.png',
'/textures/musician3.png',
'/textures/musician4.png',
];
// --- Stage dimensions for positioning ---
const stageHeight = 1.5;
@ -20,9 +25,8 @@ export class MedievalMusicians extends SceneFeature {
sceneFeatureManager.register(this);
}
init() {
// Load the texture and create the material inside the callback
state.loader.load(musiciansTextureUrl, (texture) => {
async init() {
const processTexture = (texture) => {
// 1. Draw texture to canvas to process it
const image = texture.image;
const canvas = document.createElement('canvas');
@ -53,18 +57,23 @@ export class MedievalMusicians extends SceneFeature {
}
context.putImageData(imageData, 0, 0);
// 4. Create a new texture from the modified canvas
const processedTexture = new THREE.CanvasTexture(canvas);
return new THREE.CanvasTexture(canvas);
};
// 5. Create a standard material with the new texture
const material = new THREE.MeshStandardMaterial({
// Load and process all textures, creating a material for each
const materials = await Promise.all(musicianTextureUrls.map(async (url) => {
const texture = await state.loader.loadAsync(url);
const processedTexture = processTexture(texture);
return new THREE.MeshStandardMaterial({
map: processedTexture,
side: THREE.DoubleSide,
alphaTest: 0.5, // Treat pixels with alpha < 0.5 as fully transparent
roughness: 0.7,
metalness: 0.1,
});
}));
const createMusicians = () => {
// 6. Create and position the musicians
const geometry = new THREE.PlaneGeometry(musicianWidth, musicianHeight);
@ -72,9 +81,12 @@ export class MedievalMusicians extends SceneFeature {
new THREE.Vector3(-2, stageHeight + musicianHeight / 2, -length / 2 + stageDepth / 2 - 1),
new THREE.Vector3(0, stageHeight + musicianHeight / 2, -length / 2 + stageDepth / 2 - 1.5),
new THREE.Vector3(2.5, stageHeight + musicianHeight / 2, -length / 2 + stageDepth / 2 - 1.2),
new THREE.Vector3(1.2, stageHeight + musicianHeight / 2, -length / 2 + stageDepth / 2 - 0.4),
];
musicianPositions.forEach(pos => {
musicianPositions.forEach((pos, index) => {
// Randomly pick one of the created materials
const material = materials[Math.floor(index % materials.length)];
const musician = new THREE.Mesh(geometry, material);
musician.position.copy(pos);
state.scene.add(musician);
@ -97,7 +109,9 @@ export class MedievalMusicians extends SceneFeature {
jumpStartTime: 0,
});
});
});
};
createMusicians();
}
update(deltaTime) {
@ -114,7 +128,9 @@ export class MedievalMusicians extends SceneFeature {
const planeJumpChance = 0.1;
const jumpChance = 0.005;
const jumpDuration = 0.5;
const jumpHeight = 2.0;
const jumpHeight = 1.0;
const jumpVariance = 1.0;
const jumpPlaneVariance = 2.0;
this.musicians.forEach(musicianObj => {
const { mesh } = musicianObj;
@ -131,7 +147,7 @@ export class MedievalMusicians extends SceneFeature {
// --- Decide to jump to the other plane ---
musicianObj.state = 'PREPARING_JUMP';
const targetX = (Math.random() - 0.5) * area.x;
musicianObj.targetPosition = new THREE.Vector3(targetX, mesh.position.y, planeEdgeZ);
musicianObj.targetPosition = new THREE.Vector3(targetX, area.y + musicianHeight/2, planeEdgeZ);
} else {
// --- Decide to move to a new spot on the current plane ---
const newTarget = new THREE.Vector3(
@ -161,6 +177,7 @@ export class MedievalMusicians extends SceneFeature {
} else {
// --- Arrived at edge, start the plane jump ---
musicianObj.state = 'JUMPING_PLANE';
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpPlaneVariance;
musicianObj.jumpStartPos = mesh.position.clone();
const targetPlane = musicianObj.currentPlane === 'stage' ? 'floor' : 'stage';
const targetArea = targetPlane === 'stage' ? stageArea : floorArea;
@ -178,7 +195,7 @@ export class MedievalMusicians extends SceneFeature {
if (musicianObj.jumpProgress < 1) {
// Determine base height based on which half of the jump we're in
const baseHeight = musicianObj.jumpProgress < 0.5 ? musicianObj.jumpStartPos.y : musicianObj.jumpEndPos.y;
const arcHeight = Math.sin(musicianObj.jumpProgress * Math.PI) * jumpHeight;
const arcHeight = Math.sin(musicianObj.jumpProgress * Math.PI) * musicianObj.jumpHeight;
// Interpolate horizontal position
const horizontalProgress = musicianObj.jumpProgress;
@ -201,7 +218,7 @@ export class MedievalMusicians extends SceneFeature {
const jumpProgress = (time - musicianObj.jumpStartTime) / jumpDuration;
if (jumpProgress < 1) {
const baseHeight = area.y + musicianHeight/2;
mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * jumpHeight;
mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * musicianObj.jumpHeight;
} else {
musicianObj.isJumping = false;
mesh.position.y = area.y + musicianHeight / 2;
@ -209,6 +226,7 @@ export class MedievalMusicians extends SceneFeature {
} else {
if (Math.random() < jumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') {
musicianObj.isJumping = true;
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
musicianObj.jumpStartTime = time;
}
}

View File

@ -0,0 +1,152 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
const guestTextureUrls = [
'/textures/guest1.png',
'/textures/guest2.png',
'/textures/guest3.png',
'/textures/guest4.png',
];
// --- Scene dimensions for positioning ---
const stageHeight = 1.5;
const stageDepth = 5;
const length = 44;
// --- Billboard Properties ---
const guestHeight = 2.5;
const guestWidth = 2.5;
export class PartyGuests extends SceneFeature {
constructor() {
super();
this.guests = [];
sceneFeatureManager.register(this);
}
async init() {
const processTexture = (texture) => {
const image = texture.image;
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
const keyPixelData = context.getImageData(0, 0, 1, 1).data;
const keyColor = { r: keyPixelData[0], g: keyPixelData[1], b: keyPixelData[2] };
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const threshold = 20;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2];
const distance = Math.sqrt(Math.pow(r - keyColor.r, 2) + Math.pow(g - keyColor.g, 2) + Math.pow(b - keyColor.b, 2));
if (distance < threshold) data[i + 3] = 0;
}
context.putImageData(imageData, 0, 0);
return new THREE.CanvasTexture(canvas);
};
const materials = await Promise.all(guestTextureUrls.map(async (url) => {
const texture = await state.loader.loadAsync(url);
const processedTexture = processTexture(texture);
return new THREE.MeshStandardMaterial({
map: processedTexture,
side: THREE.DoubleSide,
alphaTest: 0.5,
roughness: 0.7,
metalness: 0.1,
});
}));
const createGuests = () => {
const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight);
const numGuests = 80;
for (let i = 0; i < numGuests; i++) {
const material = materials[i % materials.length];
const guest = new THREE.Mesh(geometry, material);
const pos = new THREE.Vector3(
(Math.random() - 0.5) * 10,
guestHeight / 2,
(Math.random() * 20) - 12 // Position them in the main hall
);
guest.position.copy(pos);
state.scene.add(guest);
this.guests.push({
mesh: guest,
state: 'WAITING',
targetPosition: pos.clone(),
waitStartTime: 0,
waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds
isJumping: false,
jumpStartTime: 0,
});
}
};
createGuests();
}
update(deltaTime) {
if (this.guests.length === 0) return;
const cameraPosition = new THREE.Vector3();
state.camera.getWorldPosition(cameraPosition);
const time = state.clock.getElapsedTime();
const moveSpeed = 1.0; // Move slower
const movementArea = { x: 10, z: 30, y: 0, centerZ: 0 };
const jumpChance = 0.05; // Jump way more
const jumpDuration = 0.5;
const jumpHeight = 0.1;
const jumpVariance = 0.5;
this.guests.forEach(guestObj => {
const { mesh } = guestObj;
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
if (guestObj.state === 'WAITING') {
if (time > guestObj.waitStartTime + guestObj.waitTime) {
const newTarget = new THREE.Vector3(
(Math.random() - 0.5) * movementArea.x,
movementArea.y + guestHeight / 2,
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
);
guestObj.targetPosition = newTarget;
guestObj.state = 'MOVING';
}
} else if (guestObj.state === 'MOVING') {
const distance = mesh.position.distanceTo(guestObj.targetPosition);
if (distance > 0.1) {
const direction = guestObj.targetPosition.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
} else {
guestObj.state = 'WAITING';
guestObj.waitStartTime = time;
guestObj.waitTime = 3 + Math.random() * 4;
}
}
if (guestObj.isJumping) {
const jumpProgress = (time - guestObj.jumpStartTime) / jumpDuration;
if (jumpProgress < 1) {
const baseHeight = movementArea.y + guestHeight / 2;
mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * guestObj.jumpHeight;
} else {
guestObj.isJumping = false;
mesh.position.y = movementArea.y + guestHeight / 2;
}
} else {
if (Math.random() < jumpChance) {
guestObj.isJumping = true;
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
guestObj.jumpStartTime = time;
}
}
});
}
}
new PartyGuests();

View File

@ -40,7 +40,7 @@ export class RoomWalls extends SceneFeature {
});
// --- Geometry Definitions ---
const pillarGeo = new THREE.BoxGeometry(pillarSize, pillarHeight, pillarSize);
const pillarGeo = new THREE.CylinderGeometry(pillarSize / 2, pillarSize / 2, pillarHeight, 24);
// --- Object Creation Functions ---
const createMesh = (geometry, material, position, rotation = new THREE.Euler()) => {
@ -89,7 +89,7 @@ export class RoomWalls extends SceneFeature {
const z = -length / 2 + pillarSpacing * (i + 0.5);
// Add wall sections between pillars
if (i <= numPillars) {
createMesh(arcadeWallGeo, arcadeWallMat, new THREE.Vector3(-naveWidth / 2, pillarHeight + arcadeWallHeight / 2, z));
createMesh(arcadeWallGeo, arcadeWallMat, new THREE.Vector3(-naveWidth / 2 , pillarHeight + arcadeWallHeight / 2, z));
createMesh(arcadeWallGeo, arcadeWallMat, new THREE.Vector3(naveWidth / 2, pillarHeight + arcadeWallHeight / 2, z));
}

View File

@ -8,6 +8,8 @@ import { LightBall } from './light-ball.js';
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 ---

View File

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

View File

@ -17,6 +17,7 @@ export class StainedGlass extends SceneFeature {
const aisleWidth = 6;
const totalWidth = naveWidth + 2 * aisleWidth;
const aisleHeight = 8;
const naveHeight = 15;
// --- Window Properties ---
const windowWidth = 3;
@ -31,34 +32,57 @@ export class StainedGlass extends SceneFeature {
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
emissive: 0x222222, // Set to white to use vertex colors for glow
emissiveIntensity: 0.1, // Start with a base intensity
//blending: THREE.AdditiveBlending, // Additive blending for a bright, glowing effect
});
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
];
// --- Procedural Geometry Generation ---
const createProceduralWindowGeometry = () => {
const createProceduralWindowGeometry = (mainColor) => {
const secondColor = new THREE.Color((Math.random()), (Math.random()), (Math.random()));
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)];
const addTriangle = (v1, v2, v3, isBorder = false) => {
let segmentColor;
const rand = Math.random();
if (rand < 0.05) { // 5% chance for a "lead line"
segmentColor = secondColor.clone();
} else if (isBorder && rand < 0.6) { // 60% chance for border segments to be the main color
segmentColor = mainColor.clone().offsetHSL(0, 0, -0.1); // Slightly darker border
} else if (isBorder) { // Remaining chance for border to be an accent
segmentColor = colorPalette[Math.floor(Math.random() * colorPalette.length)].clone().offsetHSL(0, 0, -0.1);
}
else { // Inner panels
if (rand < 0.65) { // 65% chance (after lead lines) to be a variation of the main color
segmentColor = mainColor.clone();
// Slightly shift hue, saturation, and lightness
segmentColor.offsetHSL((Math.random() - 0.5) * 0.2, (Math.random() - 0.5) * 0.2, (Math.random() - 0.5) * 0.4);
} else if (rand < 0.8) {
segmentColor = secondColor.clone().offsetHSL((Math.random() - 0.5) * 0.2, (Math.random() - 0.5) * 0.2, (Math.random() - 0.5) * 0.4);
} else { // Remaining chance for a random accent color
segmentColor = new THREE.Color((Math.random()), (Math.random()), (Math.random()));
}
}
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);
colors.push(segmentColor.r, segmentColor.g, segmentColor.b, segmentColor.r, segmentColor.g, segmentColor.b, segmentColor.r, segmentColor.g, segmentColor.b);
// Calculate the base normal for the flat triangle face
const edge1 = new THREE.Vector3().subVectors(v2, v1);
@ -85,8 +109,9 @@ export class StainedGlass extends SceneFeature {
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);
const isBorder = i === 0 || i === segmentsX - 1 || j === 0;
addTriangle(v1, v2, v3, isBorder);
addTriangle(v2, v4, v3, isBorder);
}
}
@ -98,7 +123,7 @@ export class StainedGlass extends SceneFeature {
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);
addTriangle(v1, v2, v3, true); // Treat all arch segments as part of the border
}
const geometry = new THREE.BufferGeometry();
@ -108,9 +133,67 @@ export class StainedGlass extends SceneFeature {
return geometry;
};
const createProceduralRoseWindowGeometry = (mainColor, radius) => {
const segments = 32; // Number of radial segments
const rings = 5;
const vertices = [];
const colors = [];
const normals = [];
const randomnessFactor = 0.4;
const addTriangle = (v1, v2, v3, isBorder = false) => {
let segmentColor;
const rand = Math.random();
if (rand < 0.05) { // 5% chance for a "lead line"
segmentColor = new THREE.Color(0x333333);
} else if (isBorder && rand < 0.6) {
segmentColor = mainColor.clone().offsetHSL(0, 0, -0.1);
} else if (isBorder) {
segmentColor = colorPalette[Math.floor(Math.random() * colorPalette.length)].clone().offsetHSL(0, 0, -0.1);
} else {
if (rand < 0.85) {
segmentColor = mainColor.clone();
segmentColor.offsetHSL((Math.random() - 0.5) * 0.1, (Math.random() - 0.5) * 0.3, (Math.random() - 0.5) * 0.2);
} else {
segmentColor = 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(segmentColor.r, segmentColor.g, segmentColor.b, segmentColor.r, segmentColor.g, segmentColor.b, segmentColor.r, segmentColor.g, segmentColor.b);
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();
const randomVec = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
faceNormal.add(randomVec.multiplyScalar(randomnessFactor)).normalize();
normals.push(faceNormal.x, faceNormal.y, faceNormal.z, faceNormal.x, faceNormal.y, faceNormal.z, faceNormal.x, faceNormal.y, faceNormal.z);
};
for (let r = 0; r < rings; r++) {
for (let s = 0; s < segments; s++) {
const angle1 = (s / segments) * Math.PI * 2;
const angle2 = ((s + 1) / segments) * Math.PI * 2;
const v1 = new THREE.Vector3(Math.cos(angle1) * (r * radius / rings), Math.sin(angle1) * (r * radius / rings), 0);
const v2 = new THREE.Vector3(Math.cos(angle2) * (r * radius / rings), Math.sin(angle2) * (r * radius / rings), 0);
const v3 = new THREE.Vector3(Math.cos(angle1) * ((r + 1) * radius / rings), Math.sin(angle1) * ((r + 1) * radius / rings), 0);
const v4 = new THREE.Vector3(Math.cos(angle2) * ((r + 1) * radius / rings), Math.sin(angle2) * ((r + 1) * radius / rings), 0);
addTriangle(v1, v2, v3, r === rings - 1);
addTriangle(v2, v4, v3, r === rings - 1);
}
}
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
// Pick a main color for this entire window
const mainColor = colorPalette[Math.floor(Math.random() * colorPalette.length)];
const geometry = createProceduralWindowGeometry(mainColor); // Generate unique geometry for each window
const windowMesh = new THREE.Mesh(geometry, material);
windowMesh.position.copy(position);
windowMesh.rotation.y = rotationY;
@ -127,13 +210,34 @@ export class StainedGlass extends SceneFeature {
// Right side
createAndPlaceWindow(new THREE.Vector3(totalWidth / 2 - 0.01, y, z), -Math.PI / 2);
}
// --- Add Windows to the Nave/Clerestory Walls ---
for (let i = 0; i < numWindowsPerSide * 2; i++) {
const z = -length / 2 + windowSpacing / 2 * (i + 0.5);
const y = aisleHeight + (naveHeight - aisleHeight - (windowBaseHeight + windowArchHeight)) / 2; // Center them vertically in the clerestory
// Left side of Nave
createAndPlaceWindow(new THREE.Vector3(-naveWidth / 2 + 0.01, y, z), Math.PI / 2);
// Right side of Nave
createAndPlaceWindow(new THREE.Vector3(naveWidth / 2 - 0.01, y, z), -Math.PI / 2);
}
// --- Add Huge Rose Window behind the stage ---
const roseWindowRadius = naveWidth / 2 - 1; // Almost as wide as the nave
const roseWindowMainColor = colorPalette[Math.floor(Math.random() * colorPalette.length)];
const roseWindowGeo = createProceduralRoseWindowGeometry(roseWindowMainColor, roseWindowRadius);
const roseWindowMesh = new THREE.Mesh(roseWindowGeo, material);
roseWindowMesh.position.set(0, naveHeight - 2, -length / 2 + 0.05);
state.scene.add(roseWindowMesh);
this.windows.push(roseWindowMesh);
}
update(deltaTime) {
// Add a subtle pulsing glow to the windows
const pulseSpeed = 0.5;
const minIntensity = 0.5;
const maxIntensity = 0.9;
const minIntensity = 0.1; // Increased intensity for a stronger glow
const maxIntensity = 0.2;
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB