diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index 2839e6e..15c0dab 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -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; } }