Feature: add fireplace, shelves with flasks

This commit is contained in:
Dejvino 2025-11-19 23:21:11 +01:00
parent da94aa0aa3
commit e23b4109f8
6 changed files with 287 additions and 51 deletions

View File

@ -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);

View File

@ -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
}
}

View 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;
}

View File

@ -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
});

View 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;
}

View 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);
}
`;