You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
129 lines
6.2 KiB
JavaScript
129 lines
6.2 KiB
JavaScript
// Generates src/3d/massing.glb — a conceptual massing model of a Kerala
|
|
// residence (two gabled volumes on a plinth + ember "section cut" fin),
|
|
// echoing the homepage hero elevation drawing. Pure Node, no deps.
|
|
// Usage: node tools/make-glb.js (rerun only if you change the geometry)
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const positions = [], normals = [], groups = { walls: [], roof: [], ember: [] };
|
|
|
|
function tri(mat, a, b, c) {
|
|
// flat normal
|
|
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
const v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
let n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
|
|
const l = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
n = n.map(x => x / l);
|
|
for (const p of [a, b, c]) {
|
|
groups[mat].push(positions.length / 3);
|
|
positions.push(...p);
|
|
normals.push(...n);
|
|
}
|
|
}
|
|
function quad(mat, a, b, c, d) { tri(mat, a, b, c); tri(mat, a, c, d); }
|
|
|
|
// axis-aligned box
|
|
function box(mat, [x0, y0, z0], [x1, y1, z1]) {
|
|
quad(mat, [x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]); // front +z
|
|
quad(mat, [x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0]); // back
|
|
quad(mat, [x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]); // left
|
|
quad(mat, [x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1]); // right
|
|
quad(mat, [x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]); // top
|
|
quad(mat, [x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]); // bottom
|
|
}
|
|
// gabled volume: walls to eave height, triangular gable ends, roof planes with overhang
|
|
function gabled(matW, matR, [x0, z0], [x1, z1], yBase, yEave, yRidge, over) {
|
|
box(matW, [x0, yBase, z0], [x1, yEave, z1]);
|
|
const zm = (z0 + z1) / 2;
|
|
// gable end triangles (on x0 and x1 faces)
|
|
tri(matW, [x0, yEave, z1], [x0, yRidge, zm], [x0, yEave, z0]);
|
|
tri(matW, [x1, yEave, z0], [x1, yRidge, zm], [x1, yEave, z1]);
|
|
// roof planes (slightly overhanging slabs)
|
|
const t = 0.12; // roof slab thickness
|
|
const ox0 = x0 - over, ox1 = x1 + over, oz0 = z0 - over, oz1 = z1 + over;
|
|
// south plane (z1 side): from eave edge to ridge
|
|
quad(matR, [ox0, yEave, oz1], [ox1, yEave, oz1], [ox1, yRidge, zm], [ox0, yRidge, zm]);
|
|
quad(matR, [ox0, yEave + t, oz1], [ox0, yRidge + t, zm], [ox1, yRidge + t, zm], [ox1, yEave + t, oz1]);
|
|
// north plane
|
|
quad(matR, [ox1, yEave, oz0], [ox0, yEave, oz0], [ox0, yRidge, zm], [ox1, yRidge, zm]);
|
|
quad(matR, [ox1, yEave + t, oz0], [ox1, yRidge + t, zm], [ox0, yRidge + t, zm], [ox0, yEave + t, oz0]);
|
|
}
|
|
|
|
// ---- compose the massing (meters; y up; x along ridge) ----
|
|
box('walls', [-7.2, 0, -4.2], [7.2, 0.3, 4.2]); // plinth
|
|
gabled('walls', 'roof', [-6.4, -3.4], [0.6, 3.4], 0.3, 3.4, 5.1, 0.5); // main volume
|
|
gabled('walls', 'roof', [1.2, -2.6], [6.4, 2.6], 0.3, 2.9, 4.2, 0.4); // secondary volume
|
|
box('walls', [0.6, 0.3, -1.0], [1.2, 2.9, 1.0]); // link block
|
|
box('walls', [-6.9, 0.3, 3.6], [-2.9, 0.45, 4.9]); // veranda deck
|
|
box('roof', [-6.9, 3.05, 3.6], [-2.9, 3.2, 5.0]); // veranda canopy
|
|
for (const px of [-6.7, -4.9, -3.1]) box('walls', [px, 0.45, 4.55], [px + 0.14, 3.05, 4.69]); // posts
|
|
box('ember', [-2.95, 0.05, -4.6], [-2.83, 5.6, 4.6]); // section-cut fin
|
|
|
|
// ---- write GLB ----
|
|
function toBuf(arr, Ctor) { return Buffer.from(new Ctor(arr).buffer); }
|
|
const posBuf = toBuf(positions, Float32Array);
|
|
const nrmBuf = toBuf(normals, Float32Array);
|
|
const idxBufs = ['walls', 'roof', 'ember'].map(m => toBuf(groups[m], Uint32Array));
|
|
|
|
const bin = Buffer.concat([posBuf, nrmBuf, ...idxBufs]);
|
|
const pad4 = n => (4 - (n % 4)) % 4;
|
|
|
|
let mins = [Infinity, Infinity, Infinity], maxs = [-Infinity, -Infinity, -Infinity];
|
|
for (let i = 0; i < positions.length; i += 3)
|
|
for (let k = 0; k < 3; k++) { mins[k] = Math.min(mins[k], positions[i + k]); maxs[k] = Math.max(maxs[k], positions[i + k]); }
|
|
|
|
const bufferViews = [
|
|
{ buffer: 0, byteOffset: 0, byteLength: posBuf.length, target: 34962 },
|
|
{ buffer: 0, byteOffset: posBuf.length, byteLength: nrmBuf.length, target: 34962 },
|
|
];
|
|
let off = posBuf.length + nrmBuf.length;
|
|
for (const b of idxBufs) { bufferViews.push({ buffer: 0, byteOffset: off, byteLength: b.length, target: 34963 }); off += b.length; }
|
|
|
|
const accessors = [
|
|
{ bufferView: 0, componentType: 5126, count: positions.length / 3, type: 'VEC3', min: mins, max: maxs },
|
|
{ bufferView: 1, componentType: 5126, count: normals.length / 3, type: 'VEC3' },
|
|
];
|
|
['walls', 'roof', 'ember'].forEach((m, i) => accessors.push({ bufferView: 2 + i, componentType: 5125, count: groups[m].length, type: 'SCALAR' }));
|
|
|
|
const materials = [
|
|
{ name: 'walls', pbrMetallicRoughness: { baseColorFactor: [0.87, 0.845, 0.79, 1], metallicFactor: 0, roughnessFactor: 0.92 } },
|
|
{ name: 'roof', pbrMetallicRoughness: { baseColorFactor: [0.16, 0.15, 0.14, 1], metallicFactor: 0, roughnessFactor: 0.85 } },
|
|
{ name: 'ember', pbrMetallicRoughness: { baseColorFactor: [0.34, 0.16, 0.09, 1], metallicFactor: 0, roughnessFactor: 0.7 }, emissiveFactor: [0.05, 0.02, 0.01] },
|
|
];
|
|
|
|
const json = {
|
|
asset: { version: '2.0', generator: 'artn-massing' },
|
|
scene: 0,
|
|
scenes: [{ nodes: [0] }],
|
|
nodes: [{ mesh: 0, name: 'massing' }],
|
|
meshes: [{
|
|
primitives: ['walls', 'roof', 'ember'].map((m, i) => ({
|
|
attributes: { POSITION: 0, NORMAL: 1 }, indices: 2 + i, material: i
|
|
}))
|
|
}],
|
|
materials, accessors, bufferViews,
|
|
buffers: [{ byteLength: bin.length }],
|
|
};
|
|
|
|
let jsonStr = JSON.stringify(json);
|
|
jsonStr += ' '.repeat(pad4(jsonStr.length));
|
|
const jsonChunk = Buffer.from(jsonStr);
|
|
const binPadded = Buffer.concat([bin, Buffer.alloc(pad4(bin.length))]);
|
|
const total = 12 + 8 + jsonChunk.length + 8 + binPadded.length;
|
|
|
|
const glb = Buffer.alloc(total);
|
|
glb.writeUInt32LE(0x46546C67, 0); // magic 'glTF'
|
|
glb.writeUInt32LE(2, 4);
|
|
glb.writeUInt32LE(total, 8);
|
|
glb.writeUInt32LE(jsonChunk.length, 12);
|
|
glb.writeUInt32LE(0x4E4F534A, 16); // 'JSON'
|
|
jsonChunk.copy(glb, 20);
|
|
glb.writeUInt32LE(binPadded.length, 20 + jsonChunk.length);
|
|
glb.writeUInt32LE(0x004E4942, 24 + jsonChunk.length); // 'BIN'
|
|
binPadded.copy(glb, 28 + jsonChunk.length);
|
|
|
|
const dest = path.join(__dirname, '..', 'src', '3d', 'massing.glb');
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
fs.writeFileSync(dest, glb);
|
|
console.log(`massing.glb written: ${(glb.length / 1024).toFixed(1)} KB, ${positions.length / 3} verts`);
|