From e23b4109f8f54cb4089de73b47bb26cc0ce342aa Mon Sep 17 00:00:00 2001 From: Dejvino Date: Wed, 19 Nov 2025 23:21:11 +0100 Subject: [PATCH] Feature: add fireplace, shelves with flasks --- magic-mirror/src/core/animate.js | 2 + magic-mirror/src/scene/bookshelf.js | 79 ++++++++++------ magic-mirror/src/scene/fireplace.js | 114 +++++++++++++++++++++++ magic-mirror/src/scene/root.js | 33 +++---- magic-mirror/src/scene/table.js | 47 ++++++++++ magic-mirror/src/shaders/fire-shaders.js | 63 +++++++++++++ 6 files changed, 287 insertions(+), 51 deletions(-) create mode 100644 magic-mirror/src/scene/fireplace.js create mode 100644 magic-mirror/src/scene/table.js create mode 100644 magic-mirror/src/shaders/fire-shaders.js diff --git a/magic-mirror/src/core/animate.js b/magic-mirror/src/core/animate.js index bbec5f6..ac566d3 100644 --- a/magic-mirror/src/core/animate.js +++ b/magic-mirror/src/core/animate.js @@ -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); diff --git a/magic-mirror/src/scene/bookshelf.js b/magic-mirror/src/scene/bookshelf.js index 099057b..21ebed4 100644 --- a/magic-mirror/src/scene/bookshelf.js +++ b/magic-mirror/src/scene/bookshelf.js @@ -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); - if (Math.random() > 0.8) { - state.books.push(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 - 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 } } diff --git a/magic-mirror/src/scene/fireplace.js b/magic-mirror/src/scene/fireplace.js new file mode 100644 index 0000000..2cbd726 --- /dev/null +++ b/magic-mirror/src/scene/fireplace.js @@ -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; +} \ No newline at end of file diff --git a/magic-mirror/src/scene/root.js b/magic-mirror/src/scene/root.js index 7bafc10..f16e393 100644 --- a/magic-mirror/src/scene/root.js +++ b/magic-mirror/src/scene/root.js @@ -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 }); diff --git a/magic-mirror/src/scene/table.js b/magic-mirror/src/scene/table.js new file mode 100644 index 0000000..da697d6 --- /dev/null +++ b/magic-mirror/src/scene/table.js @@ -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; +} \ No newline at end of file diff --git a/magic-mirror/src/shaders/fire-shaders.js b/magic-mirror/src/shaders/fire-shaders.js new file mode 100644 index 0000000..0c6d800 --- /dev/null +++ b/magic-mirror/src/shaders/fire-shaders.js @@ -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); +} +`; \ No newline at end of file