Feature: Improved mirror fade effect
This commit is contained in:
parent
6a2498ef04
commit
40952b348a
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Magical Crystal Ball</title>
|
<title>Magical Mirror</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Cheerful medieval aesthetic */
|
/* Cheerful medieval aesthetic */
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { turnTvScreenOff, turnTvScreenOn, setScreenEffect } from '../scene/tv-set.js';
|
import { turnTvScreenOff, turnTvScreenOn } from '../scene/magic-mirror.js';
|
||||||
|
|
||||||
// --- Play video by index ---
|
// --- Play video by index ---
|
||||||
export function playVideoByIndex(index) {
|
export function playVideoByIndex(index) {
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
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,12 +41,27 @@ export function createMagicMirror(x, z, rotY) {
|
|||||||
const mirrorRadius = 0.8; // Adjusted radius for scaling
|
const mirrorRadius = 0.8; // Adjusted radius for scaling
|
||||||
const mirrorGeo = new THREE.CircleGeometry(mirrorRadius, 64);
|
const mirrorGeo = new THREE.CircleGeometry(mirrorRadius, 64);
|
||||||
|
|
||||||
// The 'tvScreen' from state will now be our mirror surface
|
// --- 3a. The permanent reflective mirror surface ---
|
||||||
state.tvScreen = new THREE.Mesh(mirrorGeo, new THREE.MeshBasicMaterial({ color: 0x000000 }));
|
const mirrorBackMaterial = new THREE.MeshPhongMaterial({
|
||||||
state.tvScreen.position.y = 1.4; // Center height
|
color: 0x051020, // Dark blue tint
|
||||||
state.tvScreen.position.z = 0.1; // Slightly forward in the frame
|
shininess: 100,
|
||||||
state.tvScreen.scale.set(1, 1.5, 1); // Scale Y to make it a tall ellipse
|
specular: 0xcccccc,
|
||||||
setMirrorOffMaterial(); // Set its initial "off" state
|
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
|
||||||
mirrorGroup.add(state.tvScreen);
|
mirrorGroup.add(state.tvScreen);
|
||||||
|
|
||||||
// --- 4. Ornate Elliptical Mirror Frame (Torus) ---
|
// --- 4. Ornate Elliptical Mirror Frame (Torus) ---
|
||||||
@ -68,7 +83,7 @@ export function createMagicMirror(x, z, rotY) {
|
|||||||
state.screenLight.shadow.mapSize.height = 1024;
|
state.screenLight.shadow.mapSize.height = 1024;
|
||||||
state.screenLight.shadow.camera.near = 0.2;
|
state.screenLight.shadow.camera.near = 0.2;
|
||||||
state.screenLight.shadow.camera.far = 5;
|
state.screenLight.shadow.camera.far = 5;
|
||||||
mirrorGroup.add(state.screenLight);
|
//mirrorGroup.add(state.screenLight);
|
||||||
|
|
||||||
// Position and rotate the entire group
|
// Position and rotate the entire group
|
||||||
mirrorGroup.position.set(x, 0, z);
|
mirrorGroup.position.set(x, 0, z);
|
||||||
@ -77,26 +92,11 @@ export function createMagicMirror(x, z, rotY) {
|
|||||||
state.scene.add(mirrorGroup);
|
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() {
|
export function turnTvScreenOff() {
|
||||||
if (state.tvScreenPowered) {
|
if (state.tvScreenPowered) {
|
||||||
state.tvScreenPowered = false;
|
state.tvScreenPowered = false;
|
||||||
setScreenEffect(2, () => {
|
setScreenEffect(2, () => {
|
||||||
setMirrorOffMaterial();
|
state.tvScreen.visible = false; // Hide the video surface on completion
|
||||||
state.screenLight.intensity = 0.0;
|
state.screenLight.intensity = 0.0;
|
||||||
}); // Trigger power down effect
|
}); // Trigger power down effect
|
||||||
}
|
}
|
||||||
@ -106,6 +106,9 @@ export function turnTvScreenOn() {
|
|||||||
if (state.tvScreen.material) {
|
if (state.tvScreen.material) {
|
||||||
state.tvScreen.material.dispose();
|
state.tvScreen.material.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.tvScreen.visible = true; // Make the video surface visible
|
||||||
|
|
||||||
// Use the shader material for video playback
|
// Use the shader material for video playback
|
||||||
state.tvScreen.material = new THREE.ShaderMaterial({
|
state.tvScreen.material = new THREE.ShaderMaterial({
|
||||||
uniforms: {
|
uniforms: {
|
||||||
|
|||||||
@ -1,250 +0,0 @@
|
|||||||
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,7 +40,7 @@ float noise (vec2 st) {
|
|||||||
void main() {
|
void main() {
|
||||||
vec4 finalColor;
|
vec4 finalColor;
|
||||||
|
|
||||||
if (u_effect_type < 0.5) { // No effect
|
// Shimmering edge effect - ALWAYS ON
|
||||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||||
|
|
||||||
// Shimmering edge effect
|
// Shimmering edge effect
|
||||||
@ -50,25 +50,43 @@ void main() {
|
|||||||
|
|
||||||
vec3 shimmerColor = vec3(0.7, 0.8, 1.0) * shimmer * edgeFactor * 0.5;
|
vec3 shimmerColor = vec3(0.7, 0.8, 1.0) * shimmer * edgeFactor * 0.5;
|
||||||
|
|
||||||
finalColor = vec4(videoColor.rgb + shimmerColor, videoColor.a);
|
vec4 baseColor = vec4(videoColor.rgb + shimmerColor, videoColor.a);
|
||||||
|
|
||||||
} else if (u_effect_type < 1.5) { // "Summon Vision" (Warm-up) effect
|
if (u_effect_type < 0.9) {
|
||||||
// Swirling mist clears to reveal the video
|
// 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);
|
float noiseVal = noise(vUv * 10.0);
|
||||||
float revealFactor = smoothstep(0.0, 0.7, u_effect_strength);
|
vec3 mistColor = vec3(0.8, 0.7, 1.0) * noiseVal;
|
||||||
float mist = smoothstep(revealFactor - 0.2, revealFactor, noiseVal);
|
|
||||||
|
|
||||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||||
finalColor = mix(vec4(0.8, 0.7, 1.0, 1.0) * noiseVal, videoColor, mist);
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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 mixedColor = mix(mistColor, baseColor.rgb, revealMix);
|
||||||
|
finalColor = vec4(mixedColor, fadeInOpacity);
|
||||||
|
|
||||||
} else { // "Vision Fades" (Power-down) effect
|
} else { // "Vision Fades" (Power-down) effect
|
||||||
// Video dissolves into a magical fog
|
// Multi-stage effect: Last frame -> fade to mist -> fade to transparent
|
||||||
float noiseVal = noise(vUv * 10.0);
|
|
||||||
float dissolveFactor = smoothstep(0.3, 1.0, u_effect_strength);
|
|
||||||
float mist = smoothstep(dissolveFactor - 0.2, dissolveFactor, noiseVal);
|
|
||||||
|
|
||||||
|
float noiseVal = noise(vUv * 10.0);
|
||||||
|
vec3 mistColor = vec3(0.8, 0.7, 1.0) * noiseVal;
|
||||||
vec4 videoColor = texture2D(videoTexture, vUv);
|
vec4 videoColor = texture2D(videoTexture, vUv);
|
||||||
finalColor = mix(videoColor, vec4(0.8, 0.7, 1.0, 1.0) * noiseVal, mist);
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_FragColor = finalColor;
|
gl_FragColor = finalColor;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user