Compare commits
7 Commits
383ba8baf1
...
37f0d30fee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37f0d30fee | ||
|
|
c612b38574 | ||
|
|
1c8eb8534e | ||
|
|
8abdb94182 | ||
|
|
390189e116 | ||
|
|
09a2dd03bc | ||
|
|
612a1bf501 |
@ -33,17 +33,6 @@ export function init() {
|
|||||||
|
|
||||||
// 6. Initialize all visual effects via the manager
|
// 6. Initialize all visual effects via the manager
|
||||||
state.effectsManager = new EffectsManager(state.scene);
|
state.effectsManager = new EffectsManager(state.scene);
|
||||||
|
|
||||||
// --- 8. Debug Visualization Helpers ---
|
|
||||||
// Visual aids for the light source positions
|
|
||||||
if (state.debugLight && THREE.PointLightHelper) {
|
|
||||||
const screenHelper = new THREE.PointLightHelper(state.screenLight, 0.1, 0xff0000); // Red for screen
|
|
||||||
state.scene.add(screenHelper);
|
|
||||||
|
|
||||||
// Lamp Helper will now work since lampLight is added to the scene
|
|
||||||
const lampHelperPoint = new THREE.PointLightHelper(state.lampLightPoint, 0.1, 0x00ff00); // Green for lamp
|
|
||||||
state.scene.add(lampHelperPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Event Listeners
|
// 9. Event Listeners
|
||||||
window.addEventListener('resize', onWindowResize, false);
|
window.addEventListener('resize', onWindowResize, false);
|
||||||
|
|||||||
181
party-cathedral/src/scene/dancers.js
Normal file
181
party-cathedral/src/scene/dancers.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
const dancerTextureUrls = [
|
||||||
|
'/textures/dancer1.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Scene dimensions for positioning ---
|
||||||
|
const stageHeight = 1.5;
|
||||||
|
const stageDepth = 5;
|
||||||
|
const length = 40;
|
||||||
|
|
||||||
|
// --- Billboard Properties ---
|
||||||
|
const dancerHeight = 2.5;
|
||||||
|
const dancerWidth = 2.5;
|
||||||
|
|
||||||
|
export class Dancers extends SceneFeature {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.dancers = [];
|
||||||
|
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(dancerTextureUrls.map(async (url) => {
|
||||||
|
const texture = await state.loader.loadAsync(url);
|
||||||
|
const processedTexture = processTexture(texture);
|
||||||
|
|
||||||
|
// Configure texture for a 2x2 sprite sheet
|
||||||
|
processedTexture.repeat.set(0.5, 0.5);
|
||||||
|
|
||||||
|
return new THREE.MeshStandardMaterial({
|
||||||
|
map: processedTexture,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
alphaTest: 0.5,
|
||||||
|
roughness: 0.7,
|
||||||
|
metalness: 0.1,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createDancers = () => {
|
||||||
|
const geometry = new THREE.PlaneGeometry(dancerWidth, dancerHeight);
|
||||||
|
const dancerPositions = [
|
||||||
|
new THREE.Vector3(-4, stageHeight + dancerHeight / 2, -length / 2 + stageDepth / 2 - 2),
|
||||||
|
new THREE.Vector3(4.5, stageHeight + dancerHeight / 2, -length / 2 + stageDepth / 2 - 1.8),
|
||||||
|
];
|
||||||
|
|
||||||
|
dancerPositions.forEach((pos, index) => {
|
||||||
|
const material = materials[index % materials.length];
|
||||||
|
const dancer = new THREE.Mesh(geometry, material);
|
||||||
|
dancer.position.copy(pos);
|
||||||
|
state.scene.add(dancer);
|
||||||
|
|
||||||
|
this.dancers.push({
|
||||||
|
mesh: dancer,
|
||||||
|
baseY: pos.y,
|
||||||
|
// --- Movement State ---
|
||||||
|
state: 'WAITING',
|
||||||
|
targetPosition: pos.clone(),
|
||||||
|
waitStartTime: 0,
|
||||||
|
waitTime: 1 + Math.random() * 2, // Wait 1-3 seconds
|
||||||
|
// --- Animation State ---
|
||||||
|
currentFrame: Math.floor(Math.random() * 4), // Start on a random frame
|
||||||
|
isMirrored: false,
|
||||||
|
canChangePose: true, // Flag to ensure pose changes only once per beat
|
||||||
|
// --- Jumping State ---
|
||||||
|
isJumping: false,
|
||||||
|
jumpStartTime: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createDancers();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
|
if (this.dancers.length === 0) return;
|
||||||
|
|
||||||
|
const cameraPosition = new THREE.Vector3();
|
||||||
|
state.camera.getWorldPosition(cameraPosition);
|
||||||
|
|
||||||
|
const time = state.clock.getElapsedTime();
|
||||||
|
const jumpDuration = 0.5;
|
||||||
|
const jumpHeight = 2.0;
|
||||||
|
const moveSpeed = 2.0;
|
||||||
|
const movementArea = { x: 10, z: 4, centerZ: -length / 2 + stageDepth / 2 };
|
||||||
|
|
||||||
|
this.dancers.forEach(dancerObj => {
|
||||||
|
const { mesh } = dancerObj;
|
||||||
|
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
|
||||||
|
|
||||||
|
// --- Point-to-Point Movement Logic ---
|
||||||
|
if (dancerObj.state === 'WAITING') {
|
||||||
|
if (time > dancerObj.waitStartTime + dancerObj.waitTime) {
|
||||||
|
// Time to find a new spot
|
||||||
|
const newTarget = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * movementArea.x,
|
||||||
|
dancerObj.baseY,
|
||||||
|
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
|
||||||
|
);
|
||||||
|
dancerObj.targetPosition = newTarget;
|
||||||
|
dancerObj.state = 'MOVING';
|
||||||
|
}
|
||||||
|
} else if (dancerObj.state === 'MOVING') {
|
||||||
|
const distance = mesh.position.distanceTo(dancerObj.targetPosition);
|
||||||
|
if (distance > 0.1) {
|
||||||
|
const direction = dancerObj.targetPosition.clone().sub(mesh.position).normalize();
|
||||||
|
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
|
||||||
|
} else {
|
||||||
|
// Arrived at destination
|
||||||
|
dancerObj.state = 'WAITING';
|
||||||
|
dancerObj.waitStartTime = time;
|
||||||
|
dancerObj.waitTime = 1 + Math.random() * 2; // Set new wait time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Spritesheet Animation ---
|
||||||
|
if (state.music) {
|
||||||
|
if (state.music.beatIntensity > 0.8 && dancerObj.canChangePose) {
|
||||||
|
// On the beat, select a new random frame and mirroring state
|
||||||
|
dancerObj.currentFrame = Math.floor(Math.random() * 4); // Select a random frame on the beat
|
||||||
|
dancerObj.isMirrored = Math.random() < 0.5;
|
||||||
|
|
||||||
|
const frameX = dancerObj.currentFrame % 2;
|
||||||
|
const frameY = Math.floor(dancerObj.currentFrame / 2);
|
||||||
|
|
||||||
|
// Adjust repeat and offset for mirroring
|
||||||
|
mesh.material.map.repeat.x = dancerObj.isMirrored ? -0.5 : 0.5;
|
||||||
|
mesh.material.map.offset.x = dancerObj.isMirrored ? (frameX * 0.5) + 0.5 : frameX * 0.5;
|
||||||
|
|
||||||
|
// The Y offset is inverted because UV coordinates start from the bottom-left
|
||||||
|
mesh.material.map.offset.y = (1 - frameY) * 0.5;
|
||||||
|
|
||||||
|
dancerObj.canChangePose = false; // Prevent changing again on this same beat
|
||||||
|
} else if (state.music.beatIntensity < 0.2) {
|
||||||
|
dancerObj.canChangePose = true; // Reset the flag when the beat is over
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Jumping Logic ---
|
||||||
|
if (dancerObj.isJumping) {
|
||||||
|
const jumpProgress = (time - dancerObj.jumpStartTime) / jumpDuration;
|
||||||
|
if (jumpProgress < 1.0) {
|
||||||
|
mesh.position.y = dancerObj.baseY + Math.sin(jumpProgress * Math.PI) * jumpHeight;
|
||||||
|
} else {
|
||||||
|
dancerObj.isJumping = false;
|
||||||
|
mesh.position.y = dancerObj.baseY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state.music && state.music.beatIntensity > 0.8 && Math.random() < 0.2) {
|
||||||
|
dancerObj.isJumping = true;
|
||||||
|
dancerObj.jumpStartTime = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Dancers();
|
||||||
@ -61,6 +61,15 @@ export class LightBall extends SceneFeature {
|
|||||||
mesh.position.y = 10 + Math.cos(time * driftSpeed * 1.3 + offset) * naveHeight/2 * 0.6;
|
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;
|
mesh.position.z = Math.cos(time * driftSpeed * 0.7 + offset) * length/2 * 0.8;
|
||||||
light.position.copy(mesh.position);
|
light.position.copy(mesh.position);
|
||||||
|
|
||||||
|
// --- Music Visualization ---
|
||||||
|
if (state.music) {
|
||||||
|
const baseIntensity = 4.0;
|
||||||
|
light.intensity = baseIntensity + state.music.beatIntensity * 3.0;
|
||||||
|
|
||||||
|
const baseScale = 1.0;
|
||||||
|
mesh.scale.setScalar(baseScale + state.music.beatIntensity * 0.5);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,6 +103,8 @@ export class MedievalMusicians extends SceneFeature {
|
|||||||
jumpStartPos: null,
|
jumpStartPos: null,
|
||||||
jumpEndPos: null,
|
jumpEndPos: null,
|
||||||
jumpProgress: 0,
|
jumpProgress: 0,
|
||||||
|
isMirrored: false,
|
||||||
|
canChangePose: true,
|
||||||
|
|
||||||
// --- State for jumping in place ---
|
// --- State for jumping in place ---
|
||||||
isJumping: false,
|
isJumping: false,
|
||||||
@ -138,6 +140,18 @@ export class MedievalMusicians extends SceneFeature {
|
|||||||
// We only want to rotate on the Y axis to keep them upright
|
// We only want to rotate on the Y axis to keep them upright
|
||||||
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
|
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
|
||||||
|
|
||||||
|
// --- Mirroring on Beat ---
|
||||||
|
if (state.music) {
|
||||||
|
if (state.music.beatIntensity > 0.8 && musicianObj.canChangePose) {
|
||||||
|
musicianObj.isMirrored = Math.random() < 0.5;
|
||||||
|
mesh.material.map.repeat.x = musicianObj.isMirrored ? -1 : 1;
|
||||||
|
mesh.material.map.offset.x = musicianObj.isMirrored ? 1 : 0;
|
||||||
|
musicianObj.canChangePose = false;
|
||||||
|
} else if (state.music.beatIntensity < 0.2) {
|
||||||
|
musicianObj.canChangePose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Main State Machine ---
|
// --- Main State Machine ---
|
||||||
const area = musicianObj.currentPlane === 'stage' ? stageArea : floorArea;
|
const area = musicianObj.currentPlane === 'stage' ? stageArea : floorArea;
|
||||||
const otherArea = musicianObj.currentPlane === 'stage' ? floorArea : stageArea;
|
const otherArea = musicianObj.currentPlane === 'stage' ? floorArea : stageArea;
|
||||||
@ -224,7 +238,12 @@ export class MedievalMusicians extends SceneFeature {
|
|||||||
mesh.position.y = area.y + musicianHeight / 2;
|
mesh.position.y = area.y + musicianHeight / 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Math.random() < jumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') {
|
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
||||||
|
if (state.music && state.music.beatIntensity > 0.8) {
|
||||||
|
currentJumpChance = 0.1; // High, fixed chance on the beat
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() < currentJumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') {
|
||||||
musicianObj.isJumping = true;
|
musicianObj.isJumping = true;
|
||||||
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
||||||
musicianObj.jumpStartTime = time;
|
musicianObj.jumpStartTime = time;
|
||||||
|
|||||||
39
party-cathedral/src/scene/music-visualizer.js
Normal file
39
party-cathedral/src/scene/music-visualizer.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { state } from '../state.js';
|
||||||
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
|
||||||
|
export class MusicVisualizer extends SceneFeature {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
sceneFeatureManager.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Initialize music state
|
||||||
|
state.music = {
|
||||||
|
bpm: 120,
|
||||||
|
beatDuration: 60 / 120,
|
||||||
|
measureDuration: (60 / 120) * 4,
|
||||||
|
beatIntensity: 0,
|
||||||
|
measurePulse: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
|
if (!state.music) return;
|
||||||
|
|
||||||
|
const time = state.clock.getElapsedTime();
|
||||||
|
|
||||||
|
// --- Calculate Beat Intensity (pulses every beat) ---
|
||||||
|
// This creates a sharp attack and slower decay (0 -> 1 -> 0)
|
||||||
|
const beatProgress = (time % state.music.beatDuration) / state.music.beatDuration;
|
||||||
|
state.music.beatIntensity = Math.pow(1.0 - beatProgress, 2);
|
||||||
|
|
||||||
|
// --- Calculate Measure Pulse (spikes every 4 beats) ---
|
||||||
|
// This creates a very sharp spike for the torch flame effect
|
||||||
|
const measureProgress = (time % state.music.measureDuration) / state.music.measureDuration;
|
||||||
|
state.music.measurePulse = measureProgress < 0.2 ? Math.sin(measureProgress * Math.PI * 5) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new MusicVisualizer();
|
||||||
@ -80,6 +80,8 @@ export class PartyGuests extends SceneFeature {
|
|||||||
targetPosition: pos.clone(),
|
targetPosition: pos.clone(),
|
||||||
waitStartTime: 0,
|
waitStartTime: 0,
|
||||||
waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds
|
waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds
|
||||||
|
isMirrored: false,
|
||||||
|
canChangePose: true,
|
||||||
isJumping: false,
|
isJumping: false,
|
||||||
jumpStartTime: 0,
|
jumpStartTime: 0,
|
||||||
});
|
});
|
||||||
@ -97,7 +99,7 @@ export class PartyGuests extends SceneFeature {
|
|||||||
|
|
||||||
const time = state.clock.getElapsedTime();
|
const time = state.clock.getElapsedTime();
|
||||||
const moveSpeed = 1.0; // Move slower
|
const moveSpeed = 1.0; // Move slower
|
||||||
const movementArea = { x: 10, z: 30, y: 0, centerZ: 0 };
|
const movementArea = { x: 10, z: 30, y: 0, centerZ: 5 };
|
||||||
const jumpChance = 0.05; // Jump way more
|
const jumpChance = 0.05; // Jump way more
|
||||||
const jumpDuration = 0.5;
|
const jumpDuration = 0.5;
|
||||||
const jumpHeight = 0.1;
|
const jumpHeight = 0.1;
|
||||||
@ -107,6 +109,18 @@ export class PartyGuests extends SceneFeature {
|
|||||||
const { mesh } = guestObj;
|
const { mesh } = guestObj;
|
||||||
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
|
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
|
||||||
|
|
||||||
|
// --- Mirroring on Beat ---
|
||||||
|
if (state.music) {
|
||||||
|
if (state.music.beatIntensity > 0.8 && guestObj.canChangePose) {
|
||||||
|
guestObj.isMirrored = Math.random() < 0.5;
|
||||||
|
mesh.material.map.repeat.x = guestObj.isMirrored ? -1 : 1;
|
||||||
|
mesh.material.map.offset.x = guestObj.isMirrored ? 1 : 0;
|
||||||
|
guestObj.canChangePose = false;
|
||||||
|
} else if (state.music.beatIntensity < 0.2) {
|
||||||
|
guestObj.canChangePose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (guestObj.state === 'WAITING') {
|
if (guestObj.state === 'WAITING') {
|
||||||
if (time > guestObj.waitStartTime + guestObj.waitTime) {
|
if (time > guestObj.waitStartTime + guestObj.waitTime) {
|
||||||
const newTarget = new THREE.Vector3(
|
const newTarget = new THREE.Vector3(
|
||||||
@ -139,7 +153,12 @@ export class PartyGuests extends SceneFeature {
|
|||||||
mesh.position.y = movementArea.y + guestHeight / 2;
|
mesh.position.y = movementArea.y + guestHeight / 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Math.random() < jumpChance) {
|
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
||||||
|
if (state.music && state.music.beatIntensity > 0.8) {
|
||||||
|
currentJumpChance = 0.1; // High, fixed chance on the beat
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() < currentJumpChance) {
|
||||||
guestObj.isJumping = true;
|
guestObj.isJumping = true;
|
||||||
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
||||||
guestObj.jumpStartTime = time;
|
guestObj.jumpStartTime = time;
|
||||||
|
|||||||
0
party-cathedral/src/scene/pillar-candles.js
Normal file
0
party-cathedral/src/scene/pillar-candles.js
Normal file
@ -10,6 +10,11 @@ import { Stage } from './stage.js';
|
|||||||
import { MedievalMusicians } from './medieval-musicians.js';
|
import { MedievalMusicians } from './medieval-musicians.js';
|
||||||
import { PartyGuests } from './party-guests.js';
|
import { PartyGuests } from './party-guests.js';
|
||||||
import { StageTorches } from './stage-torches.js';
|
import { StageTorches } from './stage-torches.js';
|
||||||
|
import { Dancers } from './dancers.js';
|
||||||
|
import { MusicVisualizer } from './music-visualizer.js';
|
||||||
|
import { RoseWindowLight } from './rose-window-light.js';
|
||||||
|
import { RoseWindowLightshafts } from './rose-window-lightshafts.js';
|
||||||
|
|
||||||
// Scene Features ^^^
|
// Scene Features ^^^
|
||||||
|
|
||||||
// --- Scene Modeling Function ---
|
// --- Scene Modeling Function ---
|
||||||
@ -34,11 +39,18 @@ export function createSceneObjects() {
|
|||||||
state.scene.add(floor);
|
state.scene.add(floor);
|
||||||
|
|
||||||
// 3. Lighting (Minimal and focused)
|
// 3. Lighting (Minimal and focused)
|
||||||
const ambientLight = new THREE.AmbientLight(0x606060, 0.1); // Increased ambient light for a larger space
|
const ambientLight = new THREE.AmbientLight(0x606060, 0.2); // Increased ambient light for a larger space
|
||||||
state.scene.add(ambientLight);
|
state.scene.add(ambientLight);
|
||||||
|
|
||||||
// Add a HemisphereLight for more natural, general illumination in a large space.
|
// Add a HemisphereLight for more natural, general illumination in a large space.
|
||||||
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.2);
|
const hemisphereLight = new THREE.HemisphereLight(0xffddcc, 0x444455, 0.5);
|
||||||
|
|
||||||
|
// Visual aids for the light source positions
|
||||||
|
if (state.debugLight && THREE.HemisphereLightHelper) {
|
||||||
|
// Lamp Helper will now work since lampLight is added to the scene
|
||||||
|
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.1, 0x00ff00); // Green for lamp
|
||||||
|
state.scene.add(hemisphereLightHelper);
|
||||||
|
}
|
||||||
|
|
||||||
state.scene.add(hemisphereLight);
|
state.scene.add(hemisphereLight);
|
||||||
}
|
}
|
||||||
|
|||||||
66
party-cathedral/src/scene/rose-window-light.js
Normal file
66
party-cathedral/src/scene/rose-window-light.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
|
||||||
|
export class RoseWindowLight extends SceneFeature {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.spotlight = null;
|
||||||
|
this.helper = null;
|
||||||
|
sceneFeatureManager.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// --- Dimensions for positioning ---
|
||||||
|
const length = 40;
|
||||||
|
const naveHeight = 15;
|
||||||
|
const stageDepth = 5;
|
||||||
|
|
||||||
|
// --- Create the spotlight ---
|
||||||
|
this.spotlight = new THREE.SpotLight(0xffffff, 100.0); // White light, high intensity
|
||||||
|
this.spotlight.position.set(0, naveHeight, -length / 2 + 10); // Position it at the rose window
|
||||||
|
this.spotlight.angle = Math.PI / 9; // A reasonably focused beam
|
||||||
|
this.spotlight.penumbra = 0.3; // Soft edges
|
||||||
|
this.spotlight.decay = 0.7;
|
||||||
|
this.spotlight.distance = 30;
|
||||||
|
|
||||||
|
this.spotlight.castShadow = false;
|
||||||
|
this.spotlight.shadow.mapSize.width = 1024;
|
||||||
|
this.spotlight.shadow.mapSize.height = 1024;
|
||||||
|
this.spotlight.shadow.camera.near = 1;
|
||||||
|
this.spotlight.shadow.camera.far = 30;
|
||||||
|
this.spotlight.shadow.focus = 1;
|
||||||
|
|
||||||
|
// --- Create a target for the spotlight to aim at ---
|
||||||
|
const targetObject = new THREE.Object3D();
|
||||||
|
targetObject.position.set(0, 0, -length / 2 + stageDepth); // Aim at the center of the stage
|
||||||
|
state.scene.add(targetObject);
|
||||||
|
this.spotlight.target = targetObject;
|
||||||
|
|
||||||
|
state.scene.add(this.spotlight);
|
||||||
|
|
||||||
|
// --- Add a debug helper ---
|
||||||
|
if (state.debugLight) {
|
||||||
|
this.helper = new THREE.SpotLightHelper(this.spotlight);
|
||||||
|
state.scene.add(this.helper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
|
if (!this.spotlight) return;
|
||||||
|
|
||||||
|
// Make the light pulse with the music
|
||||||
|
if (state.music) {
|
||||||
|
const baseIntensity = 4.0;
|
||||||
|
this.spotlight.intensity = baseIntensity + state.music.beatIntensity * 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the helper if it exists
|
||||||
|
if (this.helper) {
|
||||||
|
this.helper.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new RoseWindowLight();
|
||||||
149
party-cathedral/src/scene/rose-window-lightshafts.js
Normal file
149
party-cathedral/src/scene/rose-window-lightshafts.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
|
||||||
|
export class RoseWindowLightshafts extends SceneFeature {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.shafts = [];
|
||||||
|
sceneFeatureManager.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// --- Dimensions for positioning ---
|
||||||
|
const length = 40;
|
||||||
|
const naveWidth = 12;
|
||||||
|
const naveHeight = 15;
|
||||||
|
const stageDepth = 5;
|
||||||
|
const stageWidth = naveWidth - 1;
|
||||||
|
|
||||||
|
const roseWindowRadius = naveWidth / 2 - 2;
|
||||||
|
const roseWindowCenter = new THREE.Vector3(0, naveHeight - 2, -length / 2 + 0.1);
|
||||||
|
|
||||||
|
// --- Procedural Noise Texture for Light Shafts ---
|
||||||
|
const createNoiseTexture = () => {
|
||||||
|
const width = 64;
|
||||||
|
const height = 512;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const imageData = context.createImageData(width, height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
// Create vertical streaks of noise
|
||||||
|
const y = Math.floor((i / 4) / width);
|
||||||
|
const noise = Math.pow(Math.random(), 2.5) * (1 - y / height) * 255;
|
||||||
|
data[i] = noise; // R
|
||||||
|
data[i + 1] = noise; // G
|
||||||
|
data[i + 2] = noise; // B
|
||||||
|
data[i + 3] = 255; // A
|
||||||
|
}
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
return new THREE.CanvasTexture(canvas);
|
||||||
|
};
|
||||||
|
|
||||||
|
const texture = createNoiseTexture();
|
||||||
|
texture.wrapS = THREE.RepeatWrapping;
|
||||||
|
texture.wrapT = THREE.RepeatWrapping;
|
||||||
|
|
||||||
|
const baseMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
opacity: 0.3,
|
||||||
|
color: 0x88aaff, // Give the light a cool blueish tint
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Create multiple thin light shafts ---
|
||||||
|
const numShafts = 12;
|
||||||
|
for (let i = 0; i < numShafts; i++) {
|
||||||
|
const material = baseMaterial.clone(); // Each shaft needs its own material for individual opacity
|
||||||
|
|
||||||
|
const startAngle = Math.random() * Math.PI * 2;
|
||||||
|
const startRadius = Math.random() * roseWindowRadius;
|
||||||
|
const startPoint = new THREE.Vector3(
|
||||||
|
roseWindowCenter.x + Math.cos(startAngle) * startRadius,
|
||||||
|
roseWindowCenter.y + Math.sin(startAngle) * startRadius,
|
||||||
|
roseWindowCenter.z
|
||||||
|
);
|
||||||
|
|
||||||
|
// Define a linear path on the floor for the beam to travel
|
||||||
|
const floorStartPoint = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * stageWidth * 1.0,
|
||||||
|
0,
|
||||||
|
-length / 2 + Math.random() * 10 + 0
|
||||||
|
);
|
||||||
|
const floorEndPoint = new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * stageWidth * 1.0,
|
||||||
|
0,
|
||||||
|
-length / 2 + Math.random() * 10 + 3
|
||||||
|
);
|
||||||
|
|
||||||
|
const distance = startPoint.distanceTo(floorStartPoint);
|
||||||
|
const geometry = new THREE.CylinderGeometry(0.1, 0.5 + Math.random() * 0.5, distance, 16, 1, true);
|
||||||
|
const lightShaft = new THREE.Mesh(geometry, material);
|
||||||
|
|
||||||
|
state.scene.add(lightShaft);
|
||||||
|
this.shafts.push({
|
||||||
|
mesh: lightShaft,
|
||||||
|
startPoint: startPoint, // The stationary point in the window
|
||||||
|
endPoint: floorStartPoint.clone(), // The current position of the beam on the floor
|
||||||
|
floorStartPoint: floorStartPoint, // The start of the sweep path
|
||||||
|
floorEndPoint: floorEndPoint, // The end of the sweep path
|
||||||
|
moveSpeed: 0.5 + Math.random() * 1.5, // Each shaft has a different speed
|
||||||
|
// No 'state' needed anymore
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
|
const baseOpacity = 0.1;
|
||||||
|
|
||||||
|
this.shafts.forEach(shaft => {
|
||||||
|
const { mesh, startPoint, endPoint, floorStartPoint, floorEndPoint, moveSpeed } = shaft;
|
||||||
|
|
||||||
|
// Animate texture for dust motes
|
||||||
|
mesh.material.map.offset.y -= deltaTime * 0.001;
|
||||||
|
|
||||||
|
// --- Movement Logic ---
|
||||||
|
const pathDirection = floorEndPoint.clone().sub(floorStartPoint).normalize();
|
||||||
|
const pathLength = floorStartPoint.distanceTo(floorEndPoint);
|
||||||
|
|
||||||
|
// Move the endpoint along its path
|
||||||
|
endPoint.add(pathDirection.clone().multiplyScalar(moveSpeed * deltaTime));
|
||||||
|
|
||||||
|
const currentDistance = floorStartPoint.distanceTo(endPoint);
|
||||||
|
|
||||||
|
if (currentDistance >= pathLength) {
|
||||||
|
// Reached the end, reset to the start
|
||||||
|
endPoint.copy(floorStartPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Opacity based on Progress ---
|
||||||
|
const progress = Math.min(currentDistance / pathLength, 1.0);
|
||||||
|
// Use a sine curve to fade in at the start and out at the end
|
||||||
|
const fadeOpacity = Math.sin(progress * Math.PI) * baseOpacity;
|
||||||
|
|
||||||
|
// --- Update Mesh Position and Orientation ---
|
||||||
|
const distance = startPoint.distanceTo(endPoint);
|
||||||
|
mesh.scale.y = distance;
|
||||||
|
mesh.position.lerpVectors(startPoint, endPoint, 0.5);
|
||||||
|
|
||||||
|
const quaternion = new THREE.Quaternion();
|
||||||
|
const cylinderUp = new THREE.Vector3(0, 1, 0);
|
||||||
|
const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize();
|
||||||
|
quaternion.setFromUnitVectors(cylinderUp, direction);
|
||||||
|
mesh.quaternion.copy(quaternion);
|
||||||
|
|
||||||
|
// --- Music Visualization ---
|
||||||
|
const beatPulse = state.music ? state.music.beatIntensity * 0.05 : 0;
|
||||||
|
mesh.material.opacity = fadeOpacity + beatPulse;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new RoseWindowLightshafts();
|
||||||
@ -4,6 +4,8 @@ import { SceneFeature } from './SceneFeature.js';
|
|||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
import sparkTextureUrl from '/textures/spark.png';
|
import sparkTextureUrl from '/textures/spark.png';
|
||||||
|
|
||||||
|
const lightPositionBaseY = 1.2;
|
||||||
|
|
||||||
export class StageTorches extends SceneFeature {
|
export class StageTorches extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -48,7 +50,7 @@ export class StageTorches extends SceneFeature {
|
|||||||
|
|
||||||
// --- Point Light ---
|
// --- Point Light ---
|
||||||
const pointLight = new THREE.PointLight(0xffaa44, 2.5, 8);
|
const pointLight = new THREE.PointLight(0xffaa44, 2.5, 8);
|
||||||
pointLight.position.y = 1.2;
|
pointLight.position.y = lightPositionBaseY;
|
||||||
pointLight.castShadow = true;
|
pointLight.castShadow = true;
|
||||||
pointLight.shadow.mapSize.width = 128;
|
pointLight.shadow.mapSize.width = 128;
|
||||||
pointLight.shadow.mapSize.height = 128;
|
pointLight.shadow.mapSize.height = 128;
|
||||||
@ -86,30 +88,46 @@ export class StageTorches extends SceneFeature {
|
|||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
this.torches.forEach(torch => {
|
this.torches.forEach(torch => {
|
||||||
|
let measurePulse = 0;
|
||||||
|
if (state.music) {
|
||||||
|
measurePulse = state.music.measurePulse * 4.0; // Make flames jump higher
|
||||||
|
}
|
||||||
|
|
||||||
// --- Animate Particles ---
|
// --- Animate Particles ---
|
||||||
const positions = torch.particles.geometry.attributes.position.array;
|
const positions = torch.particles.geometry.attributes.position.array;
|
||||||
|
let averageY = 0;
|
||||||
for (let i = 0; i < torch.particleData.length; i++) {
|
for (let i = 0; i < torch.particleData.length; i++) {
|
||||||
const data = torch.particleData[i];
|
const data = torch.particleData[i];
|
||||||
data.life -= deltaTime;
|
data.life -= deltaTime;
|
||||||
|
const yVelocity = data.velocity.y;
|
||||||
if (data.life <= 0) {
|
if (data.life <= 0) {
|
||||||
// Reset particle
|
// Reset particle
|
||||||
positions[i * 3] = 0;
|
positions[i * 3] = 0;
|
||||||
positions[i * 3 + 1] = 1;
|
positions[i * 3 + 1] = 1;
|
||||||
positions[i * 3 + 2] = 0;
|
positions[i * 3 + 2] = 0;
|
||||||
data.life = Math.random() * 1.0;
|
data.life = Math.random() * 1.0;
|
||||||
|
data.velocity.y = Math.random() * 1.5 + measurePulse;
|
||||||
} else {
|
} else {
|
||||||
// Update position
|
// Update position
|
||||||
positions[i * 3] += data.velocity.x * deltaTime;
|
positions[i * 3] += data.velocity.x * deltaTime;
|
||||||
positions[i * 3 + 1] += data.velocity.y * deltaTime;
|
positions[i * 3 + 1] += yVelocity * deltaTime;
|
||||||
positions[i * 3 + 2] += data.velocity.z * deltaTime;
|
positions[i * 3 + 2] += data.velocity.z * deltaTime;
|
||||||
}
|
}
|
||||||
|
averageY += positions[i * 3 + 1];
|
||||||
}
|
}
|
||||||
|
averageY = averageY / positions.length;
|
||||||
torch.particles.geometry.attributes.position.needsUpdate = true;
|
torch.particles.geometry.attributes.position.needsUpdate = true;
|
||||||
|
|
||||||
// --- Flicker Light ---
|
// --- Flicker Light ---
|
||||||
const flicker = Math.random() * 0.5;
|
const baseIntensity = 2.0;
|
||||||
torch.light.intensity = 2.0 + flicker;
|
const flicker = Math.random() * 0.6;
|
||||||
|
let beatPulse = 0;
|
||||||
|
if (state.music) {
|
||||||
|
beatPulse = state.music.beatIntensity * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
torch.light.intensity = baseIntensity + flicker + beatPulse;
|
||||||
|
torch.light.position.y = lightPositionBaseY + averageY;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -235,10 +235,13 @@ export class StainedGlass extends SceneFeature {
|
|||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
// Add a subtle pulsing glow to the windows
|
// Add a subtle pulsing glow to the windows
|
||||||
const pulseSpeed = 0.5;
|
let intensity = 0.15; // Base intensity
|
||||||
const minIntensity = 0.1; // Increased intensity for a stronger glow
|
|
||||||
const maxIntensity = 0.2;
|
// --- Music Visualization ---
|
||||||
const intensity = minIntensity + (maxIntensity - minIntensity) * (0.5 * (1 + Math.sin(state.clock.getElapsedTime() * pulseSpeed)));
|
if (state.music) {
|
||||||
|
const beatPulse = state.music.beatIntensity * 0.3;
|
||||||
|
intensity += beatPulse;
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
// and modulate its intensity. The final glow color will be vertexColor * emissive * emissiveIntensity.
|
// and modulate its intensity. The final glow color will be vertexColor * emissive * emissiveIntensity.
|
||||||
|
|||||||
BIN
party-cathedral/textures/dancer1.png
Normal file
BIN
party-cathedral/textures/dancer1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Loading…
Reference in New Issue
Block a user