Feature: projection side-screens

This commit is contained in:
Dejvino 2026-01-03 20:30:48 +00:00
parent 1586df7e51
commit 0e9deaa161

View File

@ -12,15 +12,13 @@ void main() {
}
`;
const ledCountX = 256;
const ledCountY = ledCountX * (9 / 16);
const screenFragmentShader = `
uniform sampler2D videoTexture;
uniform float u_effect_type;
uniform float u_effect_strength;
uniform float u_time;
uniform float u_opacity;
uniform vec2 u_resolution;
varying vec2 vUv;
float random(vec2 st) {
@ -29,8 +27,8 @@ float random(vec2 st) {
void main() {
// LED Grid Setup
float ledCountX = ${ledCountX}.0;
float ledCountY = ${ledCountY}.0;
float ledCountX = u_resolution.x;
float ledCountY = u_resolution.y;
vec2 gridUV = vec2(vUv.x * ledCountX, vUv.y * ledCountY);
@ -68,6 +66,7 @@ const visualizerFragmentShader = `
uniform float u_time;
uniform float u_beat;
uniform float u_opacity;
uniform vec2 u_resolution;
varying vec2 vUv;
vec3 hsv2rgb(vec3 c) {
@ -77,8 +76,8 @@ vec3 hsv2rgb(vec3 c) {
}
void main() {
float ledCountX = 128.0;
float ledCountY = 72.0;
float ledCountX = u_resolution.x;
float ledCountY = u_resolution.y;
vec2 gridUV = vec2(vUv.x * ledCountX, vUv.y * ledCountY);
@ -115,7 +114,7 @@ export class ProjectionScreen extends SceneFeature {
super();
projectionScreenInstance = this;
this.isVisualizerActive = false;
this.originalPositions = null;
this.screens = [];
sceneFeatureManager.register(this);
}
@ -149,8 +148,6 @@ export class ProjectionScreen extends SceneFeature {
const geometry = new THREE.PlaneGeometry(width, height, 32, 32);
// Initial black material
this.originalPositions = geometry.attributes.position.clone();
const material = new THREE.MeshBasicMaterial({ color: 0x000000 });
this.mesh = new THREE.Mesh(geometry, material);
@ -158,8 +155,31 @@ export class ProjectionScreen extends SceneFeature {
this.mesh.position.set(0, 5.5, -20.5);
state.scene.add(this.mesh);
this.screens.push({
mesh: this.mesh,
originalPositions: geometry.attributes.position.clone(),
resolution: new THREE.Vector2(200, 200 * (9 / 16))
});
// --- Create Side Screens ---
const sideWidth = 4.5;
const sideHeight = sideWidth * (9 / 16);
const sideGeometry = new THREE.PlaneGeometry(sideWidth, sideHeight, 16, 16);
const createSideScreen = (x, y, z, rotY, resX) => {
const resY = resX * (9 / 16);
const mesh = new THREE.Mesh(sideGeometry, material); // Share initial material
mesh.position.set(x, y, z);
mesh.rotation.y = rotY;
state.scene.add(mesh);
this.screens.push({ mesh, originalPositions: sideGeometry.attributes.position.clone(), resolution: new THREE.Vector2(resX, resY) });
};
createSideScreen(-9.5, 3.5, -16.6, 0.4, 100); // Left (Lower resolution)
createSideScreen(9.5, 3.5, -16.5, -0.4, 100); // Right (Lower resolution)
state.tvScreen = this.mesh;
state.tvScreen.visible = false;
this.setAllVisible(false);
// --- Screen Light ---
// A light that projects the screen's color/ambiance into the room
@ -171,39 +191,57 @@ export class ProjectionScreen extends SceneFeature {
state.scene.add(state.screenLight);
}
setAllVisible(visible) {
this.screens.forEach(s => s.mesh.visible = visible);
}
applyMaterialToAll(baseMaterial) {
this.screens.forEach(s => {
if (baseMaterial instanceof THREE.ShaderMaterial && baseMaterial.uniforms.u_resolution) {
const newMat = baseMaterial.clone();
newMat.uniforms = THREE.UniformsUtils.clone(baseMaterial.uniforms);
newMat.uniforms.u_resolution.value.copy(s.resolution);
s.mesh.material = newMat;
} else {
s.mesh.material = baseMaterial;
}
});
}
update(deltaTime) {
updateScreenEffect();
// Wobble Logic
if (this.mesh && this.originalPositions) {
const time = state.clock.getElapsedTime();
const waveSpeed = 0.5;
const waveFrequency = 1.2;
const waveAmplitude = 0.3;
// same as stage-curtain ^^^
const positions = this.mesh.geometry.attributes.position;
const time = state.clock.getElapsedTime();
const waveSpeed = 0.5;
const waveFrequency = 1.2;
const waveAmplitude = 0.3;
this.screens.forEach(screenObj => {
const positions = screenObj.mesh.geometry.attributes.position;
const originalPositions = screenObj.originalPositions;
for (let i = 0; i < positions.count; i++) {
const originalX = this.originalPositions.getX(i);
const originalZ = this.originalPositions.getZ(i);
const originalX = originalPositions.getX(i);
const originalZ = originalPositions.getZ(i);
const zOffset = Math.sin(originalX * waveFrequency + time * waveSpeed) * waveAmplitude;
positions.setZ(i, originalZ + zOffset);
}
positions.needsUpdate = true;
this.mesh.geometry.computeVertexNormals();
}
screenObj.mesh.geometry.computeVertexNormals();
});
if (this.isVisualizerActive && state.tvScreen.material.uniforms) {
state.tvScreen.material.uniforms.u_time.value = state.clock.getElapsedTime();
if (this.isVisualizerActive) {
const beat = (state.music && state.music.beatIntensity) ? state.music.beatIntensity : 0.0;
state.tvScreen.material.uniforms.u_beat.value = beat;
// Sync light to beat
state.screenLight.intensity = state.originalScreenIntensity * (0.5 + beat * 0.5);
}
this.screens.forEach(s => {
if (s.mesh.material && s.mesh.material.uniforms && s.mesh.material.uniforms.u_time) {
s.mesh.material.uniforms.u_time.value = state.clock.getElapsedTime();
s.mesh.material.uniforms.u_beat.value = beat;
}
});
}
}
onPartyStart() {
@ -227,13 +265,13 @@ export class ProjectionScreen extends SceneFeature {
activateVisualizer() {
this.isVisualizerActive = true;
state.tvScreen.visible = true;
state.tvScreenPowered = true;
state.tvScreen.material = new THREE.ShaderMaterial({
const material = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0.0 },
u_beat: { value: 0.0 },
u_opacity: { value: state.screenOpacity }
u_opacity: { value: state.screenOpacity },
u_resolution: { value: new THREE.Vector2(1, 1) } // Placeholder, set in applyMaterialToAll
},
vertexShader: screenVertexShader,
fragmentShader: visualizerFragmentShader,
@ -241,6 +279,9 @@ export class ProjectionScreen extends SceneFeature {
transparent: true,
derivatives: true
});
this.applyMaterialToAll(material);
this.setAllVisible(true);
state.screenLight.intensity = state.originalScreenIntensity;
}
@ -296,14 +337,12 @@ export function showStandbyScreen() {
const texture = new THREE.CanvasTexture(canvas);
if (state.tvScreen.material) state.tvScreen.material.dispose();
state.tvScreen.material = new THREE.MeshBasicMaterial({
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide
});
state.tvScreen.visible = true;
state.tvScreenPowered = true;
if (projectionScreenInstance) projectionScreenInstance.applyMaterialToAll(material);
if (projectionScreenInstance) projectionScreenInstance.setAllVisible(true);
state.screenLight.intensity = 0.5;
}
@ -311,20 +350,15 @@ export function showStandbyScreen() {
export function turnTvScreenOn() {
if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false;
if (state.tvScreen.material) {
state.tvScreen.material.dispose();
}
state.tvScreen.visible = true;
// Switch to ShaderMaterial for video playback
state.tvScreen.material = new THREE.ShaderMaterial({
const material = new THREE.ShaderMaterial({
uniforms: {
videoTexture: { value: state.videoTexture },
u_effect_type: { value: 0.0 },
u_effect_strength: { value: 0.0 },
u_time: { value: 0.0 },
u_opacity: { value: state.screenOpacity !== undefined ? state.screenOpacity : 0.7 }
u_opacity: { value: state.screenOpacity !== undefined ? state.screenOpacity : 0.7 },
u_resolution: { value: new THREE.Vector2(1, 1) } // Placeholder
},
vertexShader: screenVertexShader,
fragmentShader: screenFragmentShader,
@ -333,7 +367,8 @@ export function turnTvScreenOn() {
derivatives: true
});
state.tvScreen.material.needsUpdate = true;
if (projectionScreenInstance) projectionScreenInstance.applyMaterialToAll(material);
if (projectionScreenInstance) projectionScreenInstance.setAllVisible(true);
if (!state.tvScreenPowered) {
state.tvScreenPowered = true;
@ -346,7 +381,8 @@ export function turnTvScreenOff() {
state.tvScreenPowered = false;
setScreenEffect(2, () => {
// Revert to black material or hide
state.tvScreen.material = new THREE.MeshBasicMaterial({ color: 0x000000 });
const blackMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
if (projectionScreenInstance) projectionScreenInstance.applyMaterialToAll(blackMat);
state.screenLight.intensity = 0.0;
});
}
@ -365,34 +401,35 @@ export function setScreenEffect(effectType, onComplete) {
export function updateScreenEffect() {
if (!state.screenEffect || !state.screenEffect.active) return;
const material = state.tvScreen.material;
if (!material || !material.uniforms) return;
// Update time uniform for noise
material.uniforms.u_time.value = state.clock.getElapsedTime();
const elapsedTime = (state.clock.getElapsedTime() * 1000) - state.screenEffect.startTime;
const progress = Math.min(elapsedTime / state.screenEffect.duration, 1.0);
// Simple linear fade for effect strength
// Type 1 (On): 1.0 -> 0.0
// Type 2 (Off): 0.0 -> 1.0
let strength = progress;
if (state.screenEffect.type === 1) {
strength = 1.0 - progress;
}
material.uniforms.u_effect_type.value = state.screenEffect.type;
material.uniforms.u_effect_strength.value = strength;
if (projectionScreenInstance) {
projectionScreenInstance.screens.forEach(s => {
const material = s.mesh.material;
if (!material || !material.uniforms) return;
material.uniforms.u_time.value = state.clock.getElapsedTime();
material.uniforms.u_effect_type.value = state.screenEffect.type;
material.uniforms.u_effect_strength.value = strength;
if (progress >= 1.0) {
material.uniforms.u_effect_type.value = 0.0;
material.uniforms.u_effect_strength.value = 0.0;
}
});
}
if (progress >= 1.0) {
state.screenEffect.active = false;
if (state.screenEffect.onComplete) {
state.screenEffect.onComplete();
}
// Reset effect uniforms
material.uniforms.u_effect_type.value = 0.0;
material.uniforms.u_effect_strength.value = 0.0;
}
}