import * as THREE from 'three'; import { state } from '../state.js'; import { createVcr } from './vcr.js'; import { screenVertexShader, screenFragmentShader } from '../shaders/screen-shaders.js'; export function createTvSet(x, z, rotY) { // --- Materials (MeshPhongMaterial) --- const tvPlastic = new THREE.MeshPhongMaterial({ color: 0x4d4d4d, shininess: 30 }); const tvGroup = new THREE.Group(); // --- TV Table Dimensions & Material --- const woodColor = 0x5a3e36; // Dark brown wood const tableHeight = 0.7; // Height from floor to top surface const tableWidth = 2.0; const tableDepth = 1.0; const legThickness = 0.05; const shelfThickness = 0.03; // Use standard material for realistic shadowing const material = new THREE.MeshStandardMaterial({ color: woodColor, roughness: 0.8, metalness: 0.1 }); // VCR gap dimensions calculation const shelfGap = 0.2; // Height of the VCR opening const shelfY = tableHeight - shelfGap - (shelfThickness / 2); // Y position of the bottom shelf // 2. Table Top const topGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth); const tableTop = new THREE.Mesh(topGeometry, material); tableTop.position.set(0, tableHeight, 0); tableTop.castShadow = true; tableTop.receiveShadow = true; tvGroup.add(tableTop); // 3. VCR Shelf (Middle Shelf) const shelfGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth); const vcrShelf = new THREE.Mesh(shelfGeometry, material); vcrShelf.position.set(0, shelfY, 0); vcrShelf.castShadow = true; vcrShelf.receiveShadow = true; tvGroup.add(vcrShelf); // 4. Side Walls for VCR Compartment (NEW CODE) const wallHeight = shelfGap; // Height is the gap itself const wallThickness = shelfThickness; // Reuse the shelf thickness for the wall width/depth const wallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, tableDepth); // Calculate the Y center position for the wall const wallYCenter = tableHeight - (shelfThickness / 2) - (wallHeight / 2); // Calculate the X position to be flush with the table sides const wallXPosition = (tableWidth / 2) - (wallThickness / 2); // Left Wall const sideWallLeft = new THREE.Mesh(wallGeometry, material); sideWallLeft.position.set(-wallXPosition, wallYCenter, 0); sideWallLeft.castShadow = true; sideWallLeft.receiveShadow = true; tvGroup.add(sideWallLeft); // Right Wall const sideWallRight = new THREE.Mesh(wallGeometry, material); sideWallRight.position.set(wallXPosition, wallYCenter, 0); sideWallRight.castShadow = true; sideWallRight.receiveShadow = true; tvGroup.add(sideWallRight); // 5. Legs const legHeight = shelfY; // Legs go from the floor (y=0) to the shelf (y=shelfY) const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness); // Utility function to create and position a leg const createLeg = (x, z) => { const leg = new THREE.Mesh(legGeometry, material); // Position the leg so the center is at half its height leg.position.set(x, legHeight / 2, z); leg.castShadow = true; leg.receiveShadow = true; return leg; }; // Calculate offsets for positioning the legs near the corners const offset = (tableWidth / 2) - (legThickness * 2); const depthOffset = (tableDepth / 2) - (legThickness * 2); // Front Left tvGroup.add(createLeg(-offset, depthOffset)); // Front Right tvGroup.add(createLeg(offset, depthOffset)); // Back Left tvGroup.add(createLeg(-offset, -depthOffset)); // Back Right tvGroup.add(createLeg(offset, -depthOffset)); // --- 2. The TV box --- const cabinetGeometry = new THREE.BoxGeometry(1.9, 1.5, 1.0); const cabinet = new THREE.Mesh(cabinetGeometry, tvPlastic); cabinet.position.y = 1.51; cabinet.castShadow = true; cabinet.receiveShadow = true; tvGroup.add(cabinet); // --- 3. Screen Frame --- const frameGeometry = new THREE.BoxGeometry(1.7, 1.3, 0.1); const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 20 }); const frame = new THREE.Mesh(frameGeometry, frameMaterial); frame.position.set(0, 1.5, 0.68); frame.castShadow = true; frame.receiveShadow = true; tvGroup.add(frame); // --- 4. Curved Screen (CRT Effect) --- const screenRadius = 3.0; // Radius for the subtle curve const screenWidth = 1.6; const screenHeight = 1.2; const thetaLength = screenWidth / screenRadius; // Calculate angle needed for the arc // Use CylinderGeometry as a segment const screenGeometry = new THREE.CylinderGeometry( screenRadius, screenRadius, screenHeight, // Cylinder height is the vertical dimension of the screen 32, 1, true, (Math.PI / 2) - (thetaLength / 2), // Start angle to center the arc thetaLength // Arc length (width) ); // Rotate the cylinder segment: // 1. Rotate around X-axis by 90 degrees to lay the height (Y) along Z (depth). //screenGeometry.rotateX(Math.PI / 2); // 2. Rotate around Y-axis by 90 degrees to align the segment's arc across the X-axis (width). screenGeometry.rotateY(-Math.PI/2); const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 }); state.tvScreen = new THREE.Mesh(screenGeometry, screenMaterial); // Position the curved screen state.tvScreen.position.set(0.0, 1.5, -2.1); setTvScreenOffMaterial(); tvGroup.add(state.tvScreen); tvGroup.position.set(x, 0, z); tvGroup.rotation.y = rotY; // Light from the screen (initially low intensity, will increase when video loads) state.screenLight = new THREE.PointLight(0xffffff, 0, 10); state.screenLight.position.set(0, 1.5, 1.0); // Screen light casts shadows state.screenLight.castShadow = true; state.screenLight.shadow.mapSize.width = 1024; state.screenLight.shadow.mapSize.height = 1024; state.screenLight.shadow.camera.near = 0.2; state.screenLight.shadow.camera.far = 5; tvGroup.add(state.screenLight); // -- VCR -- const vcr = createVcr(); vcr.position.set(-0.3, 0.6, 0.05); tvGroup.add(vcr); state.scene.add(tvGroup); } function setTvScreenOffMaterial() { if (state.tvScreen.material) { state.tvScreen.material.dispose(); } state.tvScreen.material = new THREE.MeshPhongMaterial({ color: 0x203530, shininess: 45, specular: 0x111111, }); state.tvScreen.material.needsUpdate = true; } export function turnTvScreenOff() { if (state.tvScreenPowered) { state.tvScreenPowered = false; setScreenEffect(2, () => { setTvScreenOffMaterial(); state.screenLight.intensity = 0.0; }); // Trigger power down } } export function turnTvScreenOn() { if (state.tvScreen.material) { state.tvScreen.material.dispose(); } state.tvScreen.material = new THREE.ShaderMaterial({ uniforms: { videoTexture: { value: state.videoTexture }, u_effect_type: { value: 0.0 }, u_effect_strength: { value: 0.0 }, }, vertexShader: screenVertexShader, fragmentShader: screenFragmentShader, transparent: true, }); state.tvScreen.material.needsUpdate = true; if (!state.tvScreenPowered) { state.tvScreenPowered = true; setScreenEffect(1); // Trigger warm-up } } /** * Controls the warm-up and power-down effects on the TV screen. * @param {number} effectType - 0 normal, 1 for warm-up, 2 for power-down. * @param {function} onComplete - Optional callback when the animation finishes. */ export function setScreenEffect(effectType, onComplete) { const material = state.tvScreen.material; if (!material.uniforms) return; state.screenEffect.active = true; state.screenEffect.type = effectType; state.screenEffect.startTime = state.clock.getElapsedTime() * 1000; state.screenEffect.onComplete = onComplete; } /** * Updates the screen effect animation. Should be called in the main render loop. */ export function updateScreenEffect() { if (!state.screenEffect.active) return; const material = state.tvScreen.material; if (!material.uniforms) return; const elapsedTime = (state.clock.getElapsedTime() * 1000) - state.screenEffect.startTime; const progress = Math.min(elapsedTime / state.screenEffect.duration, 1.0); const easedProgress = state.screenEffect.easing(progress); material.uniforms.u_effect_type.value = state.screenEffect.type; material.uniforms.u_effect_strength.value = easedProgress; if (progress >= 1.0) { state.screenEffect.active = false; material.uniforms.u_effect_strength.value = (state.screenEffect.type === 2) ? 1.0 : 0.0; // Final state if (state.screenEffect.onComplete) { state.screenEffect.onComplete(); } material.uniforms.u_effect_type.value = 0.0; // Reset effect type } }