feat(falukant): generate low-poly house models
All checks were successful
Deploy to production / deploy (push) Successful in 3m15s

This commit is contained in:
Torsten Schulz (local)
2026-09-29 07:00:15 +02:00
parent e8c30cb218
commit 4531133a37

View File

@@ -0,0 +1,187 @@
#!/usr/bin/env node
/**
* Creates the nine small, dependency-free Falukant house source models.
* The generated GLBs are intentionally low-poly; deployment optimizes them
* further with the regular Draco pipeline.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as THREE from 'three';
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
// GLTFExporter uses the browser FileReader API for binary output. Node has
// Blob but not FileReader, so provide the tiny compatible subset we need.
if (typeof globalThis.FileReader === 'undefined') {
globalThis.FileReader = class FileReader {
result = null;
onloadend = null;
readAsArrayBuffer(blob) {
blob.arrayBuffer().then((result) => {
this.result = result;
this.onloadend?.();
});
}
};
}
const here = path.dirname(fileURLToPath(import.meta.url));
const outputDir = path.resolve(process.argv[2] || path.join(here, '..', 'models-src', '3d', 'falukant', 'houses'));
fs.mkdirSync(outputDir, { recursive: true });
const colors = {
plaster: 0xe9d7b5,
wood: 0x8a593b,
darkWood: 0x533526,
roof: 0x9b4635,
straw: 0xcba75b,
stone: 0x9a958b,
window: 0x75a8b7,
door: 0x4a3024,
grass: 0x78965a,
};
function material(color) {
return new THREE.MeshStandardMaterial({ color, roughness: 0.82, metalness: 0 });
}
function box(group, width, height, depth, color, x = 0, y = height / 2, z = 0) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth), material(color));
mesh.position.set(x, y, z);
group.add(mesh);
return mesh;
}
function cylinder(group, radiusTop, radiusBottom, height, color, x = 0, y = height / 2, z = 0, segments = 6) {
const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radiusTop, radiusBottom, height, segments), material(color));
mesh.position.set(x, y, z);
group.add(mesh);
return mesh;
}
function roof(group, width, depth, baseY, color, x = 0, z = 0) {
const mesh = new THREE.Mesh(new THREE.ConeGeometry(Math.max(width, depth) * 0.78, 1.15, 4), material(color));
mesh.rotation.y = Math.PI / 4;
mesh.scale.set(width / Math.max(width, depth), 1, depth / Math.max(width, depth));
mesh.position.set(x, baseY + 0.575, z);
group.add(mesh);
}
function doorAndWindows(group, { width, height, z = 0.511, offset = 0 }) {
box(group, 0.38, height * 0.48, 0.035, colors.door, offset, height * 0.24, z);
for (const side of [-1, 1]) box(group, 0.28, 0.3, 0.035, colors.window, offset + side * width * 0.29, height * 0.62, z);
}
function base(name) {
const group = new THREE.Group();
group.name = name;
box(group, 4.8, 0.16, 4.4, colors.grass, 0, 0.08, 0);
return group;
}
function underBridge() {
const g = base('under_bridge');
box(g, 4.4, 0.24, 1.15, colors.stone, 0, 2.5, 0);
for (const x of [-1.65, 1.65]) cylinder(g, 0.28, 0.38, 2.4, colors.stone, x, 1.28, 0, 6);
box(g, 1.45, 1.0, 1.65, colors.wood, 0, 0.58, 0.45);
roof(g, 1.8, 2.0, 1.1, colors.straw, 0, 0.45);
return g;
}
function strawHut() {
const g = base('straw_hut');
cylinder(g, 1.34, 1.48, 1.18, colors.wood, 0, 0.67, 0, 8);
const roofMesh = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1.2, 8), material(colors.straw));
roofMesh.position.y = 1.78;
g.add(roofMesh);
box(g, 0.42, 0.62, 0.04, colors.door, 0, 0.38, 1.48);
return g;
}
function woodenHouse() {
const g = base('wooden_house');
box(g, 2.55, 1.48, 2.12, colors.wood, 0, 0.82, 0);
roof(g, 2.9, 2.45, 1.55, colors.darkWood);
doorAndWindows(g, { width: 2.55, height: 1.48, z: 1.078 });
return g;
}
function backyardRoom() {
const g = base('backyard_room');
box(g, 1.65, 1.12, 1.5, colors.plaster, 0, 0.64, 0);
box(g, 1.86, 0.16, 1.7, colors.darkWood, 0, 1.28, 0);
doorAndWindows(g, { width: 1.65, height: 1.12, z: 0.77 });
return g;
}
function familyHouse() {
const g = base('family_house');
box(g, 3.15, 2.05, 2.5, colors.plaster, 0, 1.1, 0);
roof(g, 3.55, 2.9, 2.1, colors.roof);
doorAndWindows(g, { width: 3.15, height: 2.05, z: 1.27 });
return g;
}
function townhouse() {
const g = base('townhouse');
box(g, 2.0, 3.05, 1.8, colors.plaster, 0, 1.6, 0);
box(g, 2.18, 0.16, 1.96, colors.roof, 0, 3.17, 0);
doorAndWindows(g, { width: 2.0, height: 1.45, z: 0.92, offset: 0 });
for (const y of [1.86, 2.53]) {
for (const x of [-0.5, 0.5]) box(g, 0.3, 0.32, 0.04, colors.window, x, y, 0.92);
}
return g;
}
function villa() {
const g = base('villa');
box(g, 3.7, 1.75, 2.5, colors.plaster, 0, 0.95, 0);
box(g, 1.3, 1.3, 1.9, colors.plaster, -2.0, 0.72, 0.1);
roof(g, 4.05, 2.9, 1.8, colors.roof);
roof(g, 1.55, 2.2, 1.35, colors.roof, -2.0, 0.1);
doorAndWindows(g, { width: 3.7, height: 1.75, z: 1.27 });
return g;
}
function mansion() {
const g = base('mansion');
box(g, 4.1, 2.75, 2.75, colors.plaster, 0, 1.45, 0);
roof(g, 4.55, 3.15, 2.8, colors.roof);
for (const x of [-1.55, 1.55]) cylinder(g, 0.48, 0.52, 3.2, colors.stone, x, 1.7, 0, 6);
for (const x of [-1.55, 1.55]) {
const cap = new THREE.Mesh(new THREE.ConeGeometry(0.67, 0.75, 6), material(colors.roof));
cap.position.set(x, 3.68, 0);
g.add(cap);
}
doorAndWindows(g, { width: 4.1, height: 2.1, z: 1.395 });
return g;
}
function castle() {
const g = base('castle');
box(g, 3.2, 3.0, 2.6, colors.stone, 0, 1.58, 0);
for (const x of [-1.75, 1.75]) {
for (const z of [-1.35, 1.35]) cylinder(g, 0.48, 0.55, 3.75, colors.stone, x, 1.96, z, 6);
}
for (const x of [-1.75, 1.75]) {
for (const z of [-1.35, 1.35]) {
const cap = new THREE.Mesh(new THREE.ConeGeometry(0.72, 0.85, 6), material(colors.roof));
cap.position.set(x, 4.27, z);
g.add(cap);
}
}
box(g, 0.64, 1.2, 0.04, colors.door, 0, 0.68, 1.32);
return g;
}
const models = { under_bridge: underBridge, straw_hut: strawHut, wooden_house: woodenHouse, backyard_room: backyardRoom, family_house: familyHouse, townhouse, villa, mansion, castle };
const exporter = new GLTFExporter();
for (const [name, make] of Object.entries(models)) {
const scene = new THREE.Scene();
scene.add(make());
const binary = await new Promise((resolve, reject) => exporter.parse(scene, resolve, reject, { binary: true, onlyVisible: true }));
fs.writeFileSync(path.join(outputDir, `${name}.glb`), Buffer.from(binary));
console.log(`Created ${name}.glb`);
}