music-video-gen/party-cathedral/src/scene/medieval-musicians.js

257 lines
12 KiB
JavaScript

import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
const musicianTextureUrls = [
'/textures/musician1.png',
'/textures/musician2.png',
'/textures/musician3.png',
'/textures/musician4.png',
];
// --- Stage dimensions for positioning ---
const stageHeight = 1.5;
const stageDepth = 5;
const length = 40;
// --- Billboard Properties ---
const musicianHeight = 2.5;
const musicianWidth = 2.5;
export class MedievalMusicians extends SceneFeature {
constructor() {
super();
this.musicians = [];
sceneFeatureManager.register(this);
}
async init() {
const processTexture = (texture) => {
// 1. Draw texture to canvas to process it
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);
// 2. Get the key color from the top-left pixel
const keyPixelData = context.getImageData(0, 0, 1, 1).data;
const keyColor = { r: keyPixelData[0], g: keyPixelData[1], b: keyPixelData[2] };
// 3. Process the entire canvas to make background transparent
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const threshold = 20; // Adjust this for more/less color tolerance
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const 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; // Set alpha to 0 (transparent)
}
}
context.putImageData(imageData, 0, 0);
return new THREE.CanvasTexture(canvas);
};
// 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);
const musicianPositions = [
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, 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);
// Store musician object with state for animation
this.musicians.push({
mesh: musician,
// --- State for complex movement ---
currentPlane: 'stage', // 'stage' or 'floor'
state: 'WAITING',
targetPosition: pos.clone(),
waitStartTime: 0,
waitTime: 1 + Math.random() * 2, // Wait 1-3 seconds
jumpStartPos: null,
jumpEndPos: null,
jumpProgress: 0,
isMirrored: false,
canChangePose: true,
// --- State for jumping in place ---
isJumping: false,
jumpStartTime: 0,
});
});
};
createMusicians();
}
update(deltaTime) {
// Billboard effect: make each musician face the camera
if (this.musicians.length > 0) {
const cameraPosition = new THREE.Vector3();
state.camera.getWorldPosition(cameraPosition);
const time = state.clock.getElapsedTime();
const moveSpeed = 2.0;
const stageArea = { x: 10, z: 4, y: stageHeight, centerZ: -length / 2 + stageDepth / 2 };
const floorArea = { x: 10, z: 4, y: 0, centerZ: -length / 2 + stageDepth + 2 };
const planeEdgeZ = -length / 2 + stageDepth;
const planeJumpChance = 0.1;
const jumpChance = 0.005;
const jumpDuration = 0.5;
const jumpHeight = 1.0;
const jumpVariance = 1.0;
const jumpPlaneVariance = 2.0;
this.musicians.forEach(musicianObj => {
const { mesh } = musicianObj;
// We only want to rotate on the Y axis to keep them upright
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 ---
const area = musicianObj.currentPlane === 'stage' ? stageArea : floorArea;
const otherArea = musicianObj.currentPlane === 'stage' ? floorArea : stageArea;
if (musicianObj.state === 'WAITING') {
if (time > musicianObj.waitStartTime + musicianObj.waitTime) {
if (Math.random() < planeJumpChance) {
// --- 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, area.y + musicianHeight/2, planeEdgeZ);
} else {
// --- Decide to move to a new spot on the current plane ---
const newTarget = new THREE.Vector3(
(Math.random() - 0.5) * area.x,
area.y + musicianHeight/2,
area.centerZ + (Math.random() - 0.5) * area.z
);
musicianObj.targetPosition = newTarget;
musicianObj.state = 'MOVING';
}
}
} else if (musicianObj.state === 'MOVING') {
const distance = mesh.position.distanceTo(musicianObj.targetPosition);
if (distance > 0.1) {
const direction = musicianObj.targetPosition.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
} else {
musicianObj.state = 'WAITING';
musicianObj.waitStartTime = time;
musicianObj.waitTime = 1 + Math.random() * 2;
}
} else if (musicianObj.state === 'PREPARING_JUMP') {
const distance = mesh.position.distanceTo(musicianObj.targetPosition);
if (distance > 0.1) {
const direction = musicianObj.targetPosition.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
} 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;
musicianObj.jumpEndPos = new THREE.Vector3(
mesh.position.x,
targetArea.y + musicianHeight/2,
planeEdgeZ + (targetPlane === 'stage' ? -1 : 1)
);
musicianObj.targetPosition = musicianObj.jumpEndPos.clone();
musicianObj.currentPlane = targetPlane;
musicianObj.jumpProgress = 0;
}
} else if (musicianObj.state === 'JUMPING_PLANE') {
musicianObj.jumpProgress += deltaTime / jumpDuration;
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) * musicianObj.jumpHeight;
// Interpolate horizontal position
const horizontalProgress = musicianObj.jumpProgress;
mesh.position.x = THREE.MathUtils.lerp(musicianObj.jumpStartPos.x, musicianObj.jumpEndPos.x, horizontalProgress);
mesh.position.z = THREE.MathUtils.lerp(musicianObj.jumpStartPos.z, musicianObj.jumpEndPos.z, horizontalProgress);
// Apply vertical arc
mesh.position.y = baseHeight + arcHeight;
} else {
// Landed
mesh.position.copy(musicianObj.jumpEndPos);
musicianObj.state = 'WAITING';
musicianObj.waitStartTime = time;
musicianObj.waitTime = 1 + Math.random() * 2;
}
}
// --- Jumping in place (can happen in any state except during a plane jump) ---
if (musicianObj.isJumping) {
const jumpProgress = (time - musicianObj.jumpStartTime) / jumpDuration;
if (jumpProgress < 1) {
const baseHeight = area.y + musicianHeight/2;
mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * musicianObj.jumpHeight;
} else {
musicianObj.isJumping = false;
mesh.position.y = area.y + musicianHeight / 2;
}
} else {
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.jumpHeight = jumpHeight + Math.random() * jumpVariance;
musicianObj.jumpStartTime = time;
}
}
});
}
}
}
new MedievalMusicians();