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'; import woodTextureUrl from '/textures/wood.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 woodTexture = state.loader.load(woodTextureUrl); const logMaterial = new THREE.MeshPhongMaterial({ map: woodTexture, color: 0x5c4033 // A dark wood tint }); 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.2; 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); // Chimney const chimneyGeo = new THREE.BoxGeometry(bodyWidth*0.6, state.roomHeight, bodyDepth*0.6); const chimney = new THREE.Mesh(chimneyGeo, stoneMaterial); chimney.position.z = -bodyDepth*0.2; chimney.position.y = bodyHeight; chimney.castShadow = true; chimney.receiveShadow = true; fireplaceGroup.add(chimney); // 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+0.1); 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); // 3.8. Logs inside fireplace const logGeo = new THREE.CylinderGeometry(0.08, 0.1, openingWidth * 0.7, 8); const createLog = (pos, rot) => { const log = new THREE.Mesh(logGeo, logMaterial); log.position.copy(pos); log.rotation.copy(rot); log.castShadow = false; log.receiveShadow = true; fireplaceGroup.add(log); }; const logY = hearthHeight + 0.1; createLog(new THREE.Vector3(0, logY, 0.5), new THREE.Euler(0, 0, Math.PI / 2)); createLog(new THREE.Vector3(-0.1, logY + 0.1, 0.4), new THREE.Euler(0.2, 0, Math.PI / 2.2)); createLog(new THREE.Vector3(0.2, logY + 0.1, 0.5), new THREE.Euler(-0.2, 0, Math.PI / 1.8)); // 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; const t = state.clock.getElapsedTime(); fireLight.position.y = 0.2 + 1.0 / 2 + Math.sin(t * 3)*0.01 + Math.cos(t * 7)*0.01 + Math.cos(t * 11 ) * 0.01; }