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

View File

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

View File

@ -2,7 +2,12 @@ import * as THREE from 'three';
import { state } from '../state.js'; import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js'; import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.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 --- // --- Stage dimensions for positioning ---
const stageHeight = 1.5; const stageHeight = 1.5;
@ -20,9 +25,8 @@ export class MedievalMusicians extends SceneFeature {
sceneFeatureManager.register(this); sceneFeatureManager.register(this);
} }
init() { async init() {
// Load the texture and create the material inside the callback const processTexture = (texture) => {
state.loader.load(musiciansTextureUrl, (texture) => {
// 1. Draw texture to canvas to process it // 1. Draw texture to canvas to process it
const image = texture.image; const image = texture.image;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -53,18 +57,23 @@ export class MedievalMusicians extends SceneFeature {
} }
context.putImageData(imageData, 0, 0); context.putImageData(imageData, 0, 0);
// 4. Create a new texture from the modified canvas return new THREE.CanvasTexture(canvas);
const processedTexture = new THREE.CanvasTexture(canvas); };
// 5. Create a standard material with the new texture // Load and process all textures, creating a material for each
const material = new THREE.MeshStandardMaterial({ 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, map: processedTexture,
side: THREE.DoubleSide, side: THREE.DoubleSide,
alphaTest: 0.5, // Treat pixels with alpha < 0.5 as fully transparent alphaTest: 0.5, // Treat pixels with alpha < 0.5 as fully transparent
roughness: 0.7, roughness: 0.7,
metalness: 0.1, metalness: 0.1,
}); });
}));
const createMusicians = () => {
// 6. Create and position the musicians // 6. Create and position the musicians
const geometry = new THREE.PlaneGeometry(musicianWidth, musicianHeight); 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(-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(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(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); const musician = new THREE.Mesh(geometry, material);
musician.position.copy(pos); musician.position.copy(pos);
state.scene.add(musician); state.scene.add(musician);
@ -97,7 +109,9 @@ export class MedievalMusicians extends SceneFeature {
jumpStartTime: 0, jumpStartTime: 0,
}); });
}); });
}); };
createMusicians();
} }
update(deltaTime) { update(deltaTime) {
@ -114,7 +128,9 @@ export class MedievalMusicians extends SceneFeature {
const planeJumpChance = 0.1; const planeJumpChance = 0.1;
const jumpChance = 0.005; const jumpChance = 0.005;
const jumpDuration = 0.5; const jumpDuration = 0.5;
const jumpHeight = 2.0; const jumpHeight = 1.0;
const jumpVariance = 1.0;
const jumpPlaneVariance = 2.0;
this.musicians.forEach(musicianObj => { this.musicians.forEach(musicianObj => {
const { mesh } = musicianObj; const { mesh } = musicianObj;
@ -131,7 +147,7 @@ export class MedievalMusicians extends SceneFeature {
// --- Decide to jump to the other plane --- // --- Decide to jump to the other plane ---
musicianObj.state = 'PREPARING_JUMP'; musicianObj.state = 'PREPARING_JUMP';
const targetX = (Math.random() - 0.5) * area.x; 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 { } else {
// --- Decide to move to a new spot on the current plane --- // --- Decide to move to a new spot on the current plane ---
const newTarget = new THREE.Vector3( const newTarget = new THREE.Vector3(
@ -161,6 +177,7 @@ export class MedievalMusicians extends SceneFeature {
} else { } else {
// --- Arrived at edge, start the plane jump --- // --- Arrived at edge, start the plane jump ---
musicianObj.state = 'JUMPING_PLANE'; musicianObj.state = 'JUMPING_PLANE';
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpPlaneVariance;
musicianObj.jumpStartPos = mesh.position.clone(); musicianObj.jumpStartPos = mesh.position.clone();
const targetPlane = musicianObj.currentPlane === 'stage' ? 'floor' : 'stage'; const targetPlane = musicianObj.currentPlane === 'stage' ? 'floor' : 'stage';
const targetArea = targetPlane === 'stage' ? stageArea : floorArea; const targetArea = targetPlane === 'stage' ? stageArea : floorArea;
@ -178,7 +195,7 @@ export class MedievalMusicians extends SceneFeature {
if (musicianObj.jumpProgress < 1) { if (musicianObj.jumpProgress < 1) {
// Determine base height based on which half of the jump we're in // 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 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 // Interpolate horizontal position
const horizontalProgress = musicianObj.jumpProgress; const horizontalProgress = musicianObj.jumpProgress;
@ -201,7 +218,7 @@ export class MedievalMusicians extends SceneFeature {
const jumpProgress = (time - musicianObj.jumpStartTime) / jumpDuration; const jumpProgress = (time - musicianObj.jumpStartTime) / jumpDuration;
if (jumpProgress < 1) { if (jumpProgress < 1) {
const baseHeight = area.y + musicianHeight/2; 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 { } else {
musicianObj.isJumping = false; musicianObj.isJumping = false;
mesh.position.y = area.y + musicianHeight / 2; mesh.position.y = area.y + musicianHeight / 2;
@ -209,6 +226,7 @@ export class MedievalMusicians extends SceneFeature {
} else { } else {
if (Math.random() < jumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') { if (Math.random() < jumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') {
musicianObj.isJumping = true; musicianObj.isJumping = true;
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
musicianObj.jumpStartTime = time; 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 --- // --- Geometry Definitions ---
const pillarGeo = new THREE.BoxGeometry(pillarSize, pillarHeight, pillarSize); const pillarGeo = new THREE.CylinderGeometry(pillarSize / 2, pillarSize / 2, pillarHeight, 24);
// --- Object Creation Functions --- // --- Object Creation Functions ---
const createMesh = (geometry, material, position, rotation = new THREE.Euler()) => { 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); const z = -length / 2 + pillarSpacing * (i + 0.5);
// Add wall sections between pillars // Add wall sections between pillars
if (i <= numPillars) { 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)); 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 { Pews } from './pews.js';
import { Stage } from './stage.js'; import { Stage } from './stage.js';
import { MedievalMusicians } from './medieval-musicians.js'; import { MedievalMusicians } from './medieval-musicians.js';
import { PartyGuests } from './party-guests.js';
import { StageTorches } from './stage-torches.js';
// Scene Features ^^^ // Scene Features ^^^
// --- Scene Modeling Function --- // --- 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 aisleWidth = 6;
const totalWidth = naveWidth + 2 * aisleWidth; const totalWidth = naveWidth + 2 * aisleWidth;
const aisleHeight = 8; const aisleHeight = 8;
const naveHeight = 15;
// --- Window Properties --- // --- Window Properties ---
const windowWidth = 3; const windowWidth = 3;
@ -31,18 +32,11 @@ export class StainedGlass extends SceneFeature {
side: THREE.DoubleSide, side: THREE.DoubleSide,
metalness: 0.1, // Glass is not very metallic metalness: 0.1, // Glass is not very metallic
roughness: 0.3, // Glass is smooth roughness: 0.3, // Glass is smooth
clearcoat: 1.0, emissive: 0x222222, // Set to white to use vertex colors for glow
emissive: 0x000000, // We will control emissiveness via update emissiveIntensity: 0.1, // Start with a base intensity
//blending: THREE.AdditiveBlending, // Additive blending for a bright, glowing effect
}); });
// --- Procedural Geometry Generation ---
const createProceduralWindowGeometry = () => {
const segmentsX = 8;
const segmentsY = 12;
const vertices = [];
const colors = [];
const normals = [];
const colorPalette = [ const colorPalette = [
new THREE.Color(0x6A0DAD), // Purple new THREE.Color(0x6A0DAD), // Purple
new THREE.Color(0x00008B), // Dark Blue new THREE.Color(0x00008B), // Dark Blue
@ -53,12 +47,42 @@ export class StainedGlass extends SceneFeature {
new THREE.Color(0x4B0082), // Indigo new THREE.Color(0x4B0082), // Indigo
]; ];
// --- Procedural Geometry Generation ---
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 randomnessFactor = 0.4; // How much to vary the normals const randomnessFactor = 0.4; // How much to vary the normals
const addTriangle = (v1, v2, v3) => { const addTriangle = (v1, v2, v3, isBorder = false) => {
const color = colorPalette[Math.floor(Math.random() * colorPalette.length)]; 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); 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 // Calculate the base normal for the flat triangle face
const edge1 = new THREE.Vector3().subVectors(v2, v1); 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 v2 = new THREE.Vector3(x2, y, 0);
const v3 = new THREE.Vector3(x, y2, 0); const v3 = new THREE.Vector3(x, y2, 0);
const v4 = new THREE.Vector3(x2, y2, 0); const v4 = new THREE.Vector3(x2, y2, 0);
addTriangle(v1, v2, v3); const isBorder = i === 0 || i === segmentsX - 1 || j === 0;
addTriangle(v2, v4, v3); addTriangle(v1, v2, v3, isBorder);
addTriangle(v2, v4, v3, isBorder);
} }
} }
@ -98,7 +123,7 @@ export class StainedGlass extends SceneFeature {
const v1 = archCenter; const v1 = archCenter;
const v2 = new THREE.Vector3(Math.cos(angle1) * -windowWidth / 2, Math.sin(angle1) * windowArchHeight + windowBaseHeight, 0); 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); 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(); const geometry = new THREE.BufferGeometry();
@ -108,9 +133,67 @@ export class StainedGlass extends SceneFeature {
return geometry; 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 --- // --- Create and Place Windows ---
const createAndPlaceWindow = (position, rotationY) => { 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); const windowMesh = new THREE.Mesh(geometry, material);
windowMesh.position.copy(position); windowMesh.position.copy(position);
windowMesh.rotation.y = rotationY; windowMesh.rotation.y = rotationY;
@ -127,13 +210,34 @@ export class StainedGlass extends SceneFeature {
// Right side // Right side
createAndPlaceWindow(new THREE.Vector3(totalWidth / 2 - 0.01, y, z), -Math.PI / 2); 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) { update(deltaTime) {
// Add a subtle pulsing glow to the windows // Add a subtle pulsing glow to the windows
const pulseSpeed = 0.5; const pulseSpeed = 0.5;
const minIntensity = 0.5; const minIntensity = 0.1; // Increased intensity for a stronger glow
const maxIntensity = 0.9; const maxIntensity = 0.2;
const intensity = minIntensity + (maxIntensity - minIntensity) * (0.5 * (1 + Math.sin(state.clock.getElapsedTime() * pulseSpeed))); 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 // 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