Feature: add fireplace, shelves with flasks
This commit is contained in:
parent
da94aa0aa3
commit
e23b4109f8
@ -3,6 +3,7 @@ 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 { updateFire } from '../scene/fireplace.js';
|
||||
|
||||
function updateCamera() {
|
||||
const globalTime = Date.now() * 0.00003;
|
||||
@ -172,6 +173,7 @@ export function animate() {
|
||||
// updateDoor();
|
||||
// updatePictureFrame();
|
||||
updateScreenEffect();
|
||||
updateFire();
|
||||
|
||||
// RENDER!
|
||||
state.renderer.render(state.scene, state.camera);
|
||||
|
||||
@ -80,43 +80,60 @@ export function createBookshelf(x, z, rotationY, uniqueSeed) {
|
||||
const shelfSurfaceY = currentShelfY + woodThickness / 2;
|
||||
|
||||
while (currentBookX < internalWidth / 2 - 0.05) {
|
||||
// sizes vary
|
||||
const bookWidth = 0.02 + seededRandom() * 0.05;
|
||||
const bookHeight = (shelfSpacing * 0.6) + seededRandom() * (shelfSpacing * 0.1);
|
||||
const bookDepth = 0.15 + seededRandom() * 0.03;
|
||||
// --- Procedural Flasks ---
|
||||
const flaskRadius = 0.03 + seededRandom() * 0.04;
|
||||
const flaskHeight = (shelfSpacing * 0.5) + seededRandom() * (shelfSpacing * 0.3);
|
||||
const neckHeight = flaskHeight * (0.4 + seededRandom() * 0.2);
|
||||
const neckRadius = flaskRadius * (0.4 + seededRandom() * 0.2);
|
||||
|
||||
if (currentBookX + bookWidth > internalWidth / 2) break;
|
||||
if (currentBookX + flaskRadius * 2 > internalWidth / 2) break;
|
||||
|
||||
const bookColor = getRandomColor();
|
||||
const bookMat = new THREE.MeshPhongMaterial({ color: bookColor, shininess: 60 });
|
||||
const bookGeo = new THREE.BoxGeometry(bookWidth, bookHeight, bookDepth);
|
||||
const book = new THREE.Mesh(bookGeo, bookMat);
|
||||
const flaskGroup = new THREE.Group();
|
||||
|
||||
// Position: Resting on shelf, pushed towards the back with slight random variation
|
||||
const depthVariation = seededRandom() * 0.05;
|
||||
book.position.set(
|
||||
currentBookX + bookWidth / 2,
|
||||
shelfSurfaceY + bookHeight / 2,
|
||||
-shelfDepth / 2 + woodThickness + bookDepth / 2 + depthVariation
|
||||
// 1. Glass Material
|
||||
const glassMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0xdddddd,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
shininess: 100,
|
||||
specular: 0xeeeeff
|
||||
});
|
||||
|
||||
// 2. Flask Geometry (Sphere base + Cylinder neck)
|
||||
const sphereGeo = new THREE.SphereGeometry(flaskRadius, 16, 12);
|
||||
const sphereMesh = new THREE.Mesh(sphereGeo, glassMaterial);
|
||||
sphereMesh.position.y = flaskRadius;
|
||||
|
||||
const neckGeo = new THREE.CylinderGeometry(neckRadius, neckRadius, neckHeight, 12);
|
||||
const neckMesh = new THREE.Mesh(neckGeo, glassMaterial);
|
||||
neckMesh.position.y = flaskRadius * 2 + neckHeight / 2;
|
||||
|
||||
flaskGroup.add(sphereMesh, neckMesh);
|
||||
|
||||
// 3. Liquid inside
|
||||
const liquidFill = 0.5 + seededRandom() * 0.4; // 50% to 90% full
|
||||
const liquidColor = getRandomColor();
|
||||
const liquidMaterial = new THREE.MeshPhongMaterial({ color: liquidColor, emissive: liquidColor, emissiveIntensity: 0.3, shininess: 80 });
|
||||
const liquidGeo = new THREE.SphereGeometry(flaskRadius * 0.95, 16, 12, 0, Math.PI * 2, 0, Math.PI * liquidFill);
|
||||
const liquidMesh = new THREE.Mesh(liquidGeo, liquidMaterial);
|
||||
liquidMesh.position.y = flaskRadius;
|
||||
liquidMesh.rotation.z = Math.PI;
|
||||
flaskGroup.add(liquidMesh);
|
||||
|
||||
// Position flask on shelf
|
||||
flaskGroup.position.set(
|
||||
currentBookX + flaskRadius,
|
||||
shelfSurfaceY,
|
||||
-shelfDepth / 2 + woodThickness + flaskRadius + (seededRandom() * 0.05)
|
||||
);
|
||||
|
||||
book.castShadow = true;
|
||||
book.receiveShadow = true;
|
||||
// Store original Y position and animation data
|
||||
book.userData.originalY = book.position.y;
|
||||
book.userData.levitateOffset = 0;
|
||||
book.userData.oscillationTime = Math.random() * Math.PI * 2; // Start at random phase
|
||||
shelfGroup.add(book);
|
||||
flaskGroup.userData.originalY = flaskGroup.position.y;
|
||||
flaskGroup.userData.levitateOffset = 0;
|
||||
flaskGroup.userData.oscillationTime = Math.random() * Math.PI * 2;
|
||||
shelfGroup.add(flaskGroup);
|
||||
state.books.push(flaskGroup); // Add all flasks to levitation candidates
|
||||
|
||||
if (Math.random() > 0.8) {
|
||||
state.books.push(book);
|
||||
}
|
||||
|
||||
currentBookX += bookWidth + 0.002; // Tiny gap between books
|
||||
|
||||
if (seededRandom() > 0.92) {
|
||||
currentBookX += bookWidth * 3; // random bigger gaps
|
||||
}
|
||||
currentBookX += flaskRadius * 2 + 0.02; // Gap between flasks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
114
magic-mirror/src/scene/fireplace.js
Normal file
114
magic-mirror/src/scene/fireplace.js
Normal file
@ -0,0 +1,114 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { fireVertexShader, fireFragmentShader } from '../shaders/fire-shaders.js';
|
||||
import stoneTextureUrl from '/textures/stone_floor.png';
|
||||
|
||||
let fireMaterial;
|
||||
let fireLight;
|
||||
|
||||
export function createFireplace(x, z, rotY) {
|
||||
const fireplaceGroup = new THREE.Group();
|
||||
|
||||
// --- Materials ---
|
||||
const stoneTexture = state.loader.load(stoneTextureUrl);
|
||||
stoneTexture.wrapS = THREE.RepeatWrapping;
|
||||
stoneTexture.wrapT = THREE.RepeatWrapping;
|
||||
stoneTexture.repeat.set(2, 2);
|
||||
const stoneMaterial = new THREE.MeshPhongMaterial({ map: stoneTexture, color: 0x999999, shininess: 10 });
|
||||
|
||||
const hearthWidth = 2.5;
|
||||
const hearthHeight = 0.2;
|
||||
const hearthDepth = 1.5;
|
||||
|
||||
// 1. Hearth (base)
|
||||
const hearthGeo = new THREE.BoxGeometry(hearthWidth, hearthHeight, hearthDepth);
|
||||
const hearth = new THREE.Mesh(hearthGeo, stoneMaterial);
|
||||
hearth.position.y = hearthHeight / 2;
|
||||
hearth.receiveShadow = true;
|
||||
fireplaceGroup.add(hearth);
|
||||
|
||||
// 2. Fireplace Body
|
||||
const bodyWidth = 2.2;
|
||||
const bodyHeight = 2.0;
|
||||
const bodyDepth = 0.8;
|
||||
const bodyGeo = new THREE.BoxGeometry(bodyWidth, bodyHeight, bodyDepth);
|
||||
const body = new THREE.Mesh(bodyGeo, stoneMaterial);
|
||||
body.position.y = hearthHeight + bodyHeight / 2;
|
||||
body.castShadow = true;
|
||||
body.receiveShadow = true;
|
||||
fireplaceGroup.add(body);
|
||||
|
||||
// 3. Fireplace Opening (carved out look)
|
||||
const openingWidth = 1.2;
|
||||
const openingHeight = 1.0;
|
||||
const openingGeo = new THREE.BoxGeometry(openingWidth, openingHeight, bodyDepth);
|
||||
const openingMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a }); // Dark interior
|
||||
const opening = new THREE.Mesh(openingGeo, openingMaterial);
|
||||
opening.position.set(0, hearthHeight + openingHeight / 2, 0.01);
|
||||
fireplaceGroup.add(opening);
|
||||
|
||||
// 3.5. Thick Stone Frame around Opening
|
||||
const frameThickness = 0.2;
|
||||
const frameDepth = bodyDepth * 0.25; // Make it stick out a bit
|
||||
|
||||
// Lintel (Top Frame)
|
||||
const lintelWidth = openingWidth + 2 * frameThickness;
|
||||
const lintelGeo = new THREE.BoxGeometry(lintelWidth, frameThickness, frameDepth);
|
||||
const lintel = new THREE.Mesh(lintelGeo, stoneMaterial);
|
||||
lintel.position.set(0, hearthHeight + openingHeight + frameThickness / 2, bodyDepth / 2 + frameDepth / 2);
|
||||
lintel.castShadow = true;
|
||||
lintel.receiveShadow = true;
|
||||
fireplaceGroup.add(lintel);
|
||||
|
||||
// Jambs (Side Frames)
|
||||
const jambHeight = openingHeight + frameThickness; // Go up to the lintel
|
||||
const jambGeo = new THREE.BoxGeometry(frameThickness, jambHeight, frameDepth);
|
||||
|
||||
const leftJamb = new THREE.Mesh(jambGeo, stoneMaterial);
|
||||
leftJamb.position.set(-openingWidth / 2 - frameThickness / 2, hearthHeight + jambHeight / 2, bodyDepth / 2 + frameDepth / 2);
|
||||
fireplaceGroup.add(leftJamb);
|
||||
|
||||
const rightJamb = new THREE.Mesh(jambGeo, stoneMaterial);
|
||||
rightJamb.position.set(openingWidth / 2 + frameThickness / 2, hearthHeight + jambHeight / 2, bodyDepth / 2 + frameDepth / 2);
|
||||
fireplaceGroup.add(rightJamb);
|
||||
|
||||
// 4. Animated Fire
|
||||
fireMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
u_time: { value: 0.0 },
|
||||
},
|
||||
vertexShader: fireVertexShader,
|
||||
fragmentShader: fireFragmentShader,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
const fireGeo = new THREE.PlaneGeometry(openingWidth * 1.2, openingHeight * 1.2);
|
||||
const firePlane = new THREE.Mesh(fireGeo, fireMaterial);
|
||||
firePlane.position.set(0, hearthHeight + openingHeight / 2, 0.42);
|
||||
fireplaceGroup.add(firePlane);
|
||||
|
||||
// 5. Fire Light
|
||||
fireLight = new THREE.PointLight(0xffaa33, 1.5, 8);
|
||||
fireLight.position.set(0, hearthHeight + openingHeight / 2, 0.3);
|
||||
fireLight.castShadow = true;
|
||||
fireLight.shadow.mapSize.width = 512;
|
||||
fireLight.shadow.mapSize.height = 512;
|
||||
fireplaceGroup.add(fireLight);
|
||||
|
||||
// Position and add to scene
|
||||
fireplaceGroup.position.set(x, 0, z);
|
||||
fireplaceGroup.rotation.y = rotY;
|
||||
state.scene.add(fireplaceGroup);
|
||||
}
|
||||
|
||||
export function updateFire() {
|
||||
if (!fireMaterial || !fireLight) return;
|
||||
|
||||
// Animate shader time
|
||||
fireMaterial.uniforms.u_time.value = state.clock.getElapsedTime();
|
||||
|
||||
// Flicker light
|
||||
const flicker = Math.random() * 0.4;
|
||||
fireLight.intensity = 1.2 + flicker;
|
||||
fireLight.position.y = 0.2 + 1.0 / 2 + (Math.random() - 0.5) * 0.1;
|
||||
}
|
||||
@ -3,6 +3,8 @@ import { state } from '../state.js';
|
||||
import { createRoomWalls } from './room-walls.js';
|
||||
import { createBookshelf } from './bookshelf.js';
|
||||
import { createMagicMirror } from './magic-mirror.js';
|
||||
import { createFireplace } from './fireplace.js';
|
||||
import { createTable } from './table.js';
|
||||
import { PictureFrame } from './PictureFrame.js';
|
||||
import painting1 from '/textures/painting1.jpg';
|
||||
import painting2 from '/textures/painting2.jpg';
|
||||
@ -50,7 +52,7 @@ export function createSceneObjects() {
|
||||
state.scene.add(roomLight);
|
||||
|
||||
|
||||
createMagicMirror(0, -state.roomSize/2 + 1.0, 0);
|
||||
createMagicMirror(-0.1, -state.roomSize/2 + 0.3, 0.2);
|
||||
|
||||
// --- 5. Candle ---
|
||||
const candleStickGeo = new THREE.CylinderGeometry(0.05, 0.06, 0.3, 12);
|
||||
@ -70,22 +72,11 @@ export function createSceneObjects() {
|
||||
|
||||
const candleGroup = new THREE.Group();
|
||||
candleGroup.add(candleStick, state.candleLight);
|
||||
candleGroup.position.set(0.8, 0.15, -state.roomSize/2+0.5);
|
||||
candleGroup.position.set(0.8, 0.15, -state.roomSize/2+0.6);
|
||||
|
||||
state.scene.add(candleGroup);
|
||||
|
||||
// --- 7. Table ---
|
||||
const tableTopGeo = new THREE.BoxGeometry(1.5, 0.1, 0.8);
|
||||
const tableTop = new THREE.Mesh(tableTopGeo, woodMaterial);
|
||||
tableTop.position.y = 0.5;
|
||||
tableTop.castShadow = true;
|
||||
tableTop.receiveShadow = true;
|
||||
|
||||
const table = new THREE.Group();
|
||||
table.add(tableTop); // You could add legs here for more detail
|
||||
table.position.set(-1.7, 0, -0.8);
|
||||
table.rotation.y = Math.PI / 5;
|
||||
state.scene.add(table);
|
||||
createTable(-1.8, 0, -0.8, Math.PI / 2.3);
|
||||
|
||||
// --- 8. Timber Frames ---
|
||||
const beamThickness = 0.15;
|
||||
@ -142,14 +133,16 @@ export function createSceneObjects() {
|
||||
// Right Wall
|
||||
createBeam(wallBeamGeoZ, new THREE.Vector3(state.roomSize / 2 - beamDepth / 2, state.roomHeight - 0.5, 0));
|
||||
|
||||
// --- 9. Fireplace ---
|
||||
createFireplace(state.roomSize / 2 - 0.5, -1, -Math.PI / 2);
|
||||
|
||||
createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.2, Math.PI/2, 0);
|
||||
createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.7, Math.PI/2, 0);
|
||||
createBookshelf(state.roomSize/2 * 0.7, -state.roomSize/2+0.3, 0, 1);
|
||||
createBookshelf(-state.roomSize/2 * 0.7, -state.roomSize/2+0.3, 0, 1);
|
||||
|
||||
const pictureFrame = new PictureFrame(state.scene, {
|
||||
position: new THREE.Vector3(-state.roomSize/2, 2.0, -state.roomSize/2 + 1.5),
|
||||
width: 1.5,
|
||||
position: new THREE.Vector3(-state.roomSize/2, 1.7, -state.roomSize/2 + 0.6),
|
||||
width: 0.5,
|
||||
height: 1,
|
||||
imageUrls: [painting1, painting2],
|
||||
rotationY: Math.PI / 2
|
||||
@ -157,9 +150,9 @@ export function createSceneObjects() {
|
||||
state.pictureFrames.push(pictureFrame);
|
||||
|
||||
const pictureFrame2 = new PictureFrame(state.scene, {
|
||||
position: new THREE.Vector3(state.roomSize/2, 2.0, 0.3),
|
||||
width: 1.5,
|
||||
height: 1,
|
||||
position: new THREE.Vector3(state.roomSize/2 - 0.9, 1.7, -1.1),
|
||||
width: 0.8,
|
||||
height: 0.5,
|
||||
imageUrls: [painting2, painting1],
|
||||
rotationY: -Math.PI / 2
|
||||
});
|
||||
|
||||
47
magic-mirror/src/scene/table.js
Normal file
47
magic-mirror/src/scene/table.js
Normal file
@ -0,0 +1,47 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import tableTextureUrl from '/textures/wood.png';
|
||||
|
||||
export function createTable(x, y, z, rotY) {
|
||||
const woodMaterial = new THREE.MeshPhongMaterial({
|
||||
map: state.loader.load(tableTextureUrl),
|
||||
shininess: 10,
|
||||
specular: 0x222222
|
||||
});
|
||||
|
||||
const tableTopGeo = new THREE.BoxGeometry(1.5, 0.1, 0.8);
|
||||
const tableTop = new THREE.Mesh(tableTopGeo, woodMaterial);
|
||||
tableTop.position.y = 0.5;
|
||||
tableTop.castShadow = true;
|
||||
tableTop.receiveShadow = true;
|
||||
|
||||
// Table Legs
|
||||
const legThickness = 0.1;
|
||||
const legHeight = 0.5; // Same height as tableTop.position.y
|
||||
const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness);
|
||||
|
||||
const legOffset = (1.5 / 2) - (legThickness * 1.5); // Half table width - some margin
|
||||
const depthOffset = (0.8 / 2) - (legThickness * 1.5); // Half table depth - some margin
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const table = new THREE.Group();
|
||||
table.add(tableTop);
|
||||
// Add the four legs
|
||||
table.add(createLeg(-legOffset, depthOffset));
|
||||
table.add(createLeg(legOffset, depthOffset));
|
||||
table.add(createLeg(-legOffset, -depthOffset));
|
||||
table.add(createLeg(legOffset, -depthOffset));
|
||||
|
||||
table.position.set(x, y, z);
|
||||
table.rotation.y = rotY;
|
||||
state.scene.add(table);
|
||||
|
||||
return table;
|
||||
}
|
||||
63
magic-mirror/src/shaders/fire-shaders.js
Normal file
63
magic-mirror/src/shaders/fire-shaders.js
Normal file
@ -0,0 +1,63 @@
|
||||
export const fireVertexShader = `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const fireFragmentShader = `
|
||||
varying vec2 vUv;
|
||||
uniform float u_time;
|
||||
|
||||
// 2D Random function
|
||||
float random (vec2 st) {
|
||||
return fract(sin(dot(st.xy,
|
||||
vec2(12.9898,78.233)))*
|
||||
43758.5453123);
|
||||
}
|
||||
|
||||
// 2D Noise function
|
||||
float noise (in vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
|
||||
vec2 u = f*f*(3.0-2.0*f);
|
||||
return mix(a, b, u.x) +
|
||||
(c - a)* u.y * (1.0 - u.x) +
|
||||
(d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
// Fractional Brownian Motion to create more complex noise
|
||||
float fbm(in vec2 st) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 0.0;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
value += amplitude * noise(st);
|
||||
st *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv;
|
||||
float q = fbm(uv * 2.0 - vec2(0.0, u_time * 0.2));
|
||||
float r = fbm(uv * 2.0 + q + vec2(1.7, 9.2) + vec2(0.0, u_time * -0.3));
|
||||
|
||||
float fireAmount = fbm(uv * 2.0 + r + vec2(0.0, u_time * 0.15));
|
||||
|
||||
// Shape the fire to rise from the bottom
|
||||
fireAmount *= (1.0 - uv.y);
|
||||
|
||||
vec3 fireColor = mix(vec3(0.9, 0.5, 0.1), vec3(1.0, 0.9, 0.3), fireAmount);
|
||||
gl_FragColor = vec4(fireColor, fireAmount * 2.0);
|
||||
}
|
||||
`;
|
||||
Loading…
Reference in New Issue
Block a user