Feature: dancers and mirroring of people textures
This commit is contained in:
parent
390189e116
commit
8abdb94182
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();
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
@ -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(
|
||||||
|
|||||||
@ -10,6 +10,7 @@ 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 { MusicVisualizer } from './music-visualizer.js';
|
||||||
// Scene Features ^^^
|
// Scene Features ^^^
|
||||||
|
|
||||||
|
|||||||
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