250 lines
9.0 KiB
JavaScript
250 lines
9.0 KiB
JavaScript
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
|
|
}
|
|
} |