Compare commits
No commits in common. "40952b348abeac17b44d1b2e09c4979c3273745b" and "e23b4109f8f54cb4089de73b47bb26cc0ce342aa" have entirely different histories.
40952b348a
...
e23b4109f8
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Magical Mirror</title>
|
||||
<title>Magical Crystal Ball</title>
|
||||
|
||||
<style>
|
||||
/* Cheerful medieval aesthetic */
|
||||
|
||||
@ -3,7 +3,6 @@ import { updateDoor } from '../scene/door.js';
|
||||
import { updateVcrDisplay } from '../scene/vcr-display.js';
|
||||
import { state } from '../state.js';
|
||||
import { updateScreenEffect } from '../scene/magic-mirror.js'
|
||||
import { updateCauldron } from '../scene/cauldron.js';
|
||||
import { updateFire } from '../scene/fireplace.js';
|
||||
|
||||
function updateCamera() {
|
||||
@ -175,7 +174,6 @@ export function animate() {
|
||||
// updatePictureFrame();
|
||||
updateScreenEffect();
|
||||
updateFire();
|
||||
updateCauldron();
|
||||
|
||||
// RENDER!
|
||||
state.renderer.render(state.scene, state.camera);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { turnTvScreenOff, turnTvScreenOn } from '../scene/magic-mirror.js';
|
||||
import { turnTvScreenOff, turnTvScreenOn, setScreenEffect } from '../scene/tv-set.js';
|
||||
|
||||
// --- Play video by index ---
|
||||
export function playVideoByIndex(index) {
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
|
||||
let cauldronParticles;
|
||||
let cauldronLight;
|
||||
const particleCount = 8;
|
||||
const particleVelocities = [];
|
||||
|
||||
export function createCauldron(x, y, z) {
|
||||
const cauldronGroup = new THREE.Group();
|
||||
|
||||
const cauldronRadius = 0.2;
|
||||
const cauldronHeight = 0.25;
|
||||
|
||||
// 1. Cauldron Body
|
||||
const cauldronMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 80 });
|
||||
// Use a sphere geometry cut in half for the bowl shape
|
||||
const cauldronGeo = new THREE.SphereGeometry(cauldronRadius, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2);
|
||||
const cauldronMesh = new THREE.Mesh(cauldronGeo, cauldronMaterial);
|
||||
cauldronMesh.castShadow = true;
|
||||
cauldronMesh.rotation.z = Math.PI;
|
||||
cauldronGroup.add(cauldronMesh);
|
||||
|
||||
// 1.5. Cauldron Handles
|
||||
const handleRadius = 0.08;
|
||||
const handleTube = 0.01;
|
||||
const handleMaterial = new THREE.MeshPhongMaterial({ color: 0x333333, shininess: 50 });
|
||||
|
||||
const handleGeo = new THREE.TorusGeometry(handleRadius, handleTube, 8, 32);
|
||||
|
||||
const handleShiftX = 0.07;
|
||||
const handleShiftY = 0.12;
|
||||
|
||||
const leftHandle = new THREE.Mesh(handleGeo, handleMaterial);
|
||||
leftHandle.rotation.y = Math.PI / 2;
|
||||
leftHandle.position.set(-cauldronRadius - handleRadius * 0.5 + handleShiftX, cauldronHeight * 0.5 - handleShiftY, 0);
|
||||
leftHandle.castShadow = true;
|
||||
cauldronGroup.add(leftHandle);
|
||||
|
||||
const rightHandle = new THREE.Mesh(handleGeo, handleMaterial);
|
||||
rightHandle.rotation.y = -Math.PI / 2;
|
||||
rightHandle.position.set(cauldronRadius + handleRadius * 0.5 - handleShiftX, cauldronHeight * 0.5 - handleShiftY, 0);
|
||||
rightHandle.castShadow = true;
|
||||
cauldronGroup.add(rightHandle);
|
||||
|
||||
// 2. Glowing Liquid Surface
|
||||
const liquidColor = 0x00ff00; // Bright green
|
||||
const liquidMaterial = new THREE.MeshPhongMaterial({
|
||||
color: liquidColor,
|
||||
emissive: liquidColor,
|
||||
emissiveIntensity: 0.6,
|
||||
shininess: 100
|
||||
});
|
||||
const liquidGeo = new THREE.CircleGeometry(cauldronRadius * 0.95, 32);
|
||||
const liquidSurface = new THREE.Mesh(liquidGeo, liquidMaterial);
|
||||
liquidSurface.rotation.x = -Math.PI / 2;
|
||||
liquidSurface.position.y = -0.01; // Slightly below the rim
|
||||
cauldronGroup.add(liquidSurface);
|
||||
|
||||
// 3. Bubbling Particles
|
||||
const particleGeo = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(particleCount * 3);
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
positions[i * 3] = (Math.random() - 0.5) * cauldronRadius * 1.5;
|
||||
positions[i * 3 + 1] = (Math.random() - 0.5) * 0.05; // Start near the surface
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * cauldronRadius * 1.5;
|
||||
particleVelocities.push((0.05 + Math.random() * 0.1) / 60); // Random upward velocity
|
||||
}
|
||||
particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
|
||||
const particleMaterial = new THREE.PointsMaterial({
|
||||
color: liquidColor,
|
||||
size: 0.015,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
opacity: 0.7
|
||||
});
|
||||
|
||||
cauldronParticles = new THREE.Points(particleGeo, particleMaterial);
|
||||
cauldronGroup.add(cauldronParticles);
|
||||
|
||||
// 4. Light from the cauldron
|
||||
cauldronLight = new THREE.PointLight(liquidColor, 0.8, 3);
|
||||
cauldronLight.position.y = 0.2;
|
||||
cauldronLight.castShadow = true;
|
||||
cauldronGroup.add(cauldronLight);
|
||||
|
||||
// Position and add to scene
|
||||
cauldronGroup.position.set(x, y, z);
|
||||
state.scene.add(cauldronGroup);
|
||||
}
|
||||
|
||||
export function updateCauldron() {
|
||||
if (!cauldronParticles || !cauldronLight) return;
|
||||
|
||||
// Animate Bubbles
|
||||
const positions = cauldronParticles.geometry.attributes.position.array;
|
||||
const bubbleMaxHeight = 0.1;
|
||||
const overfly = Math.random() * bubbleMaxHeight;
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
positions[i * 3 + 1] += particleVelocities[i]; // Move bubble up
|
||||
|
||||
// Reset bubble if it goes too high
|
||||
if (positions[i * 3 + 1] > bubbleMaxHeight + overfly) {
|
||||
positions[i * 3 + 1] = (Math.random() - 0.5) * 0.05;
|
||||
// Give it a new random X/Z position
|
||||
positions[i * 3] = (Math.random() - 0.5) * 0.2 * 1.5;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.2 * 1.5;
|
||||
}
|
||||
}
|
||||
cauldronParticles.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// Flicker Light
|
||||
const flicker = Math.random() * 0.02;
|
||||
cauldronLight.intensity = 0.1 + flicker;
|
||||
}
|
||||
167
magic-mirror/src/scene/crystal-ball.js
Normal file
167
magic-mirror/src/scene/crystal-ball.js
Normal file
@ -0,0 +1,167 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { screenVertexShader, screenFragmentShader } from '../shaders/screen-shaders.js';
|
||||
|
||||
export function createCrystalBall(x, z, rotY) {
|
||||
// --- Materials ---
|
||||
const woodMaterial = new THREE.MeshPhongMaterial({ color: 0x5c4033, shininess: 30 });
|
||||
const standMaterial = new THREE.MeshPhongMaterial({ color: 0x3d2d1d, shininess: 50, specular: 0x444444 });
|
||||
|
||||
const ballGroup = new THREE.Group();
|
||||
|
||||
// --- 1. Small Pedestal Table ---
|
||||
const tableHeight = 0.7;
|
||||
const tableWidth = 1.0;
|
||||
const tableDepth = 1.0;
|
||||
const legThickness = 0.08;
|
||||
|
||||
// Table Top
|
||||
const topGeometry = new THREE.BoxGeometry(tableWidth, 0.05, tableDepth);
|
||||
const tableTop = new THREE.Mesh(topGeometry, woodMaterial);
|
||||
tableTop.position.y = tableHeight;
|
||||
tableTop.castShadow = true;
|
||||
tableTop.receiveShadow = true;
|
||||
ballGroup.add(tableTop);
|
||||
|
||||
// Legs
|
||||
const legHeight = tableHeight;
|
||||
const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness);
|
||||
const legOffset = (tableWidth / 2) - (legThickness * 1.5);
|
||||
const depthOffset = (tableDepth / 2) - (legThickness * 1.5);
|
||||
|
||||
const createLeg = (lx, lz) => {
|
||||
const leg = new THREE.Mesh(legGeometry, woodMaterial);
|
||||
leg.position.set(lx, legHeight / 2, lz);
|
||||
leg.castShadow = true;
|
||||
leg.receiveShadow = true;
|
||||
return leg;
|
||||
};
|
||||
|
||||
ballGroup.add(createLeg(-legOffset, depthOffset));
|
||||
ballGroup.add(createLeg(legOffset, depthOffset));
|
||||
ballGroup.add(createLeg(-legOffset, -depthOffset));
|
||||
ballGroup.add(createLeg(legOffset, -depthOffset));
|
||||
|
||||
// --- 2. Crystal Ball Stand ---
|
||||
const standBaseGeo = new THREE.CylinderGeometry(0.25, 0.35, 0.1, 16);
|
||||
const standBase = new THREE.Mesh(standBaseGeo, standMaterial);
|
||||
standBase.position.y = tableHeight + 0.05;
|
||||
standBase.castShadow = true;
|
||||
standBase.receiveShadow = true;
|
||||
ballGroup.add(standBase);
|
||||
|
||||
const standNeckGeo = new THREE.CylinderGeometry(0.15, 0.20, 0.2, 12);
|
||||
const standNeck = new THREE.Mesh(standNeckGeo, standMaterial);
|
||||
standNeck.position.y = standBase.position.y + 0.15;
|
||||
standNeck.castShadow = true;
|
||||
standNeck.receiveShadow = true;
|
||||
ballGroup.add(standNeck);
|
||||
|
||||
// --- 3. The Crystal Ball ---
|
||||
const ballRadius = 0.35;
|
||||
const ballGeometry = new THREE.SphereGeometry(ballRadius, 64, 32);
|
||||
|
||||
// The 'tvScreen' from state will now be our crystal ball
|
||||
state.tvScreen = new THREE.Mesh(ballGeometry, new THREE.MeshBasicMaterial({ color: 0x000000 }));
|
||||
state.tvScreen.position.y = standNeck.position.y + 0.15 + ballRadius * 0.5;
|
||||
setCrystalBallOffMaterial(); // Set its initial "off" state
|
||||
ballGroup.add(state.tvScreen);
|
||||
|
||||
// --- 4. Light from the Crystal Ball ---
|
||||
state.screenLight = new THREE.PointLight(0xffffff, 0, 10);
|
||||
state.screenLight.position.copy(state.tvScreen.position);
|
||||
state.screenLight.position.y += 0.1; // Position light slightly above the center
|
||||
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;
|
||||
ballGroup.add(state.screenLight);
|
||||
|
||||
// Position and rotate the entire group
|
||||
ballGroup.position.set(x, 0, z);
|
||||
ballGroup.rotation.y = rotY;
|
||||
|
||||
state.scene.add(ballGroup);
|
||||
}
|
||||
|
||||
function setCrystalBallOffMaterial() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// A slightly reflective, dark, magical-looking material for when it's off
|
||||
state.tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
color: 0x100510,
|
||||
shininess: 100,
|
||||
specular: 0xeeeeff,
|
||||
transparent: true,
|
||||
opacity: 0.85
|
||||
});
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function turnTvScreenOff() {
|
||||
if (state.tvScreenPowered) {
|
||||
state.tvScreenPowered = false;
|
||||
setScreenEffect(2, () => {
|
||||
setCrystalBallOffMaterial();
|
||||
state.screenLight.intensity = 0.0;
|
||||
}); // Trigger power down effect
|
||||
}
|
||||
}
|
||||
|
||||
export function turnTvScreenOn() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// Use the same shader material as the TV for video playback
|
||||
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 power on effect
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (state.screenEffect.onComplete) {
|
||||
state.screenEffect.onComplete();
|
||||
}
|
||||
material.uniforms.u_effect_type.value = 0.0;
|
||||
}
|
||||
}
|
||||
@ -41,27 +41,12 @@ export function createMagicMirror(x, z, rotY) {
|
||||
const mirrorRadius = 0.8; // Adjusted radius for scaling
|
||||
const mirrorGeo = new THREE.CircleGeometry(mirrorRadius, 64);
|
||||
|
||||
// --- 3a. The permanent reflective mirror surface ---
|
||||
const mirrorBackMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0x051020, // Dark blue tint
|
||||
shininess: 100,
|
||||
specular: 0xcccccc,
|
||||
envMap: state.scene.background, // Reflect the room
|
||||
reflectivity: 0.9 // Increased reflectivity
|
||||
});
|
||||
const mirrorBack = new THREE.Mesh(mirrorGeo, mirrorBackMaterial);
|
||||
mirrorBack.position.y = 1.4; // Center height
|
||||
mirrorBack.position.z = 0.1; // Slightly forward in the frame
|
||||
mirrorBack.scale.set(1, 1.5, 1); // Scale Y to make it a tall ellipse
|
||||
mirrorGroup.add(mirrorBack);
|
||||
|
||||
// --- 3b. The video surface that appears when playing ---
|
||||
// This is what state.tvScreen will now refer to
|
||||
state.tvScreen = new THREE.Mesh(mirrorGeo, new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }));
|
||||
state.tvScreen.position.copy(mirrorBack.position);
|
||||
state.tvScreen.position.z += 0.01; // Place it just in front of the reflective surface
|
||||
state.tvScreen.scale.copy(mirrorBack.scale);
|
||||
state.tvScreen.visible = false; // Start invisible
|
||||
// The 'tvScreen' from state will now be our mirror surface
|
||||
state.tvScreen = new THREE.Mesh(mirrorGeo, new THREE.MeshBasicMaterial({ color: 0x000000 }));
|
||||
state.tvScreen.position.y = 1.4; // Center height
|
||||
state.tvScreen.position.z = 0.1; // Slightly forward in the frame
|
||||
state.tvScreen.scale.set(1, 1.5, 1); // Scale Y to make it a tall ellipse
|
||||
setMirrorOffMaterial(); // Set its initial "off" state
|
||||
mirrorGroup.add(state.tvScreen);
|
||||
|
||||
// --- 4. Ornate Elliptical Mirror Frame (Torus) ---
|
||||
@ -83,7 +68,7 @@ export function createMagicMirror(x, z, rotY) {
|
||||
state.screenLight.shadow.mapSize.height = 1024;
|
||||
state.screenLight.shadow.camera.near = 0.2;
|
||||
state.screenLight.shadow.camera.far = 5;
|
||||
//mirrorGroup.add(state.screenLight);
|
||||
mirrorGroup.add(state.screenLight);
|
||||
|
||||
// Position and rotate the entire group
|
||||
mirrorGroup.position.set(x, 0, z);
|
||||
@ -92,11 +77,26 @@ export function createMagicMirror(x, z, rotY) {
|
||||
state.scene.add(mirrorGroup);
|
||||
}
|
||||
|
||||
function setMirrorOffMaterial() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// A reflective, dark material for when it's off
|
||||
state.tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
color: 0x051020, // Dark blue tint
|
||||
shininess: 100,
|
||||
specular: 0xcccccc,
|
||||
envMap: state.scene.background, // Reflect the room
|
||||
reflectivity: 0.9 // Increased reflectivity
|
||||
});
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function turnTvScreenOff() {
|
||||
if (state.tvScreenPowered) {
|
||||
state.tvScreenPowered = false;
|
||||
setScreenEffect(2, () => {
|
||||
state.tvScreen.visible = false; // Hide the video surface on completion
|
||||
setMirrorOffMaterial();
|
||||
state.screenLight.intensity = 0.0;
|
||||
}); // Trigger power down effect
|
||||
}
|
||||
@ -106,9 +106,6 @@ export function turnTvScreenOn() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
|
||||
state.tvScreen.visible = true; // Make the video surface visible
|
||||
|
||||
// Use the shader material for video playback
|
||||
state.tvScreen.material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
|
||||
@ -5,7 +5,6 @@ import { createBookshelf } from './bookshelf.js';
|
||||
import { createMagicMirror } from './magic-mirror.js';
|
||||
import { createFireplace } from './fireplace.js';
|
||||
import { createTable } from './table.js';
|
||||
import { createCauldron } from './cauldron.js';
|
||||
import { PictureFrame } from './PictureFrame.js';
|
||||
import painting1 from '/textures/painting1.jpg';
|
||||
import painting2 from '/textures/painting2.jpg';
|
||||
@ -79,16 +78,13 @@ export function createSceneObjects() {
|
||||
|
||||
createTable(-1.8, 0, -0.8, Math.PI / 2.3);
|
||||
|
||||
// Add cauldron on top of the table (Y = table height + cauldron radius)
|
||||
createCauldron(-1.8, 0.5 + 0.2, -0.8);
|
||||
|
||||
// --- 8. Timber Frames ---
|
||||
const beamThickness = 0.15;
|
||||
const beamDepth = 0.2;
|
||||
|
||||
// Ceiling Beams
|
||||
const ceilingBeamGeoX = new THREE.BoxGeometry(state.roomSize, beamDepth * 1.3, beamThickness);
|
||||
const ceilingBeamGeoZ = new THREE.BoxGeometry(beamThickness, beamDepth * 0.8, state.roomSize);
|
||||
const ceilingBeamGeoX = new THREE.BoxGeometry(state.roomSize, beamDepth, beamThickness);
|
||||
const ceilingBeamGeoZ = new THREE.BoxGeometry(beamThickness, beamDepth, state.roomSize);
|
||||
|
||||
const createBeam = (geo, pos, rotY = 0) => {
|
||||
const beam = new THREE.Mesh(geo, woodMaterial);
|
||||
@ -125,8 +121,8 @@ export function createSceneObjects() {
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(state.roomSize / 2 - beamDepth / 2, state.roomHeight / 2, 1.5), -Math.PI / 2);
|
||||
|
||||
// Wall Beams (Horizontal)
|
||||
const wallBeamGeoX = new THREE.BoxGeometry(state.roomSize, beamThickness, beamDepth * 1.3);
|
||||
const wallBeamGeoZ = new THREE.BoxGeometry(beamDepth * 1.3, beamThickness, state.roomSize);
|
||||
const wallBeamGeoX = new THREE.BoxGeometry(state.roomSize, beamThickness, beamDepth);
|
||||
const wallBeamGeoZ = new THREE.BoxGeometry(beamDepth, beamThickness, state.roomSize);
|
||||
|
||||
// Back Wall
|
||||
createBeam(wallBeamGeoX, new THREE.Vector3(0, state.roomHeight - 0.5, -state.roomSize / 2 + beamDepth / 2));
|
||||
|
||||
250
magic-mirror/src/scene/tv-set.js
Normal file
250
magic-mirror/src/scene/tv-set.js
Normal file
@ -0,0 +1,250 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -40,53 +40,35 @@ float noise (vec2 st) {
|
||||
void main() {
|
||||
vec4 finalColor;
|
||||
|
||||
// Shimmering edge effect - ALWAYS ON
|
||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||
|
||||
// Shimmering edge effect
|
||||
float dist = distance(vUv, vec2(0.5));
|
||||
float shimmer = noise(vUv * 20.0 + vec2(u_time * 2.0, 0.0));
|
||||
float edgeFactor = smoothstep(0.3, 0.5, dist);
|
||||
|
||||
vec3 shimmerColor = vec3(0.7, 0.8, 1.0) * shimmer * edgeFactor * 0.5;
|
||||
|
||||
vec4 baseColor = vec4(videoColor.rgb + shimmerColor, videoColor.a);
|
||||
|
||||
if (u_effect_type < 0.9) {
|
||||
// normal video
|
||||
finalColor = baseColor;
|
||||
} else if (u_effect_type < 1.9) { // "Summon Vision" (Warm-up) effect
|
||||
// This is now a multi-stage effect controlled by u_effect_strength (0.0 -> 1.0)
|
||||
float noiseVal = noise(vUv * 10.0);
|
||||
vec3 mistColor = vec3(0.8, 0.7, 1.0) * noiseVal;
|
||||
if (u_effect_type < 0.5) { // No effect
|
||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||
|
||||
// Stage 1: Fade in the mist (u_effect_strength: 0.0 -> 0.5)
|
||||
// The overall opacity of the surface fades from 0 to 1.
|
||||
float fadeInOpacity = smoothstep(0.0, 0.5, u_effect_strength);
|
||||
// Shimmering edge effect
|
||||
float dist = distance(vUv, vec2(0.5));
|
||||
float shimmer = noise(vUv * 20.0 + vec2(u_time * 2.0, 0.0));
|
||||
float edgeFactor = smoothstep(0.3, 0.5, dist);
|
||||
|
||||
// Stage 2: Fade out the mist to reveal the video (u_effect_strength: 0.5 -> 1.0)
|
||||
// The mix factor between mist and video goes from 0 (all mist) to 1 (all video).
|
||||
float revealMix = smoothstep(0.5, 1.0, u_effect_strength);
|
||||
vec3 shimmerColor = vec3(0.7, 0.8, 1.0) * shimmer * edgeFactor * 0.5;
|
||||
|
||||
vec3 mixedColor = mix(mistColor, baseColor.rgb, revealMix);
|
||||
finalColor = vec4(mixedColor, fadeInOpacity);
|
||||
finalColor = vec4(videoColor.rgb + shimmerColor, videoColor.a);
|
||||
|
||||
} else if (u_effect_type < 1.5) { // "Summon Vision" (Warm-up) effect
|
||||
// Swirling mist clears to reveal the video
|
||||
float noiseVal = noise(vUv * 10.0);
|
||||
float revealFactor = smoothstep(0.0, 0.7, u_effect_strength);
|
||||
float mist = smoothstep(revealFactor - 0.2, revealFactor, noiseVal);
|
||||
|
||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||
finalColor = mix(vec4(0.8, 0.7, 1.0, 1.0) * noiseVal, videoColor, mist);
|
||||
|
||||
} else { // "Vision Fades" (Power-down) effect
|
||||
// Multi-stage effect: Last frame -> fade to mist -> fade to transparent
|
||||
|
||||
// Video dissolves into a magical fog
|
||||
float noiseVal = noise(vUv * 10.0);
|
||||
vec3 mistColor = vec3(0.8, 0.7, 1.0) * noiseVal;
|
||||
float dissolveFactor = smoothstep(0.3, 1.0, u_effect_strength);
|
||||
float mist = smoothstep(dissolveFactor - 0.2, dissolveFactor, noiseVal);
|
||||
|
||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||
|
||||
// Stage 1: Fade in the mist over the last frame (u_effect_strength: 0.0 -> 0.5)
|
||||
float mistMix = smoothstep(0.0, 0.5, u_effect_strength);
|
||||
vec3 mixedColor = mix(baseColor.rgb, mistColor, mistMix);
|
||||
|
||||
// Stage 2: Fade out the entire surface to transparent (u_effect_strength: 0.5 -> 1.0)
|
||||
float fadeOutOpacity = smoothstep(1.0, 0.5, u_effect_strength);
|
||||
|
||||
finalColor = vec4(mixedColor, fadeOutOpacity);
|
||||
finalColor = mix(videoColor, vec4(0.8, 0.7, 1.0, 1.0) * noiseVal, mist);
|
||||
}
|
||||
|
||||
gl_FragColor = finalColor;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user