188 lines
6.3 KiB
JavaScript
188 lines
6.3 KiB
JavaScript
#!/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`);
|
|
}
|