// Pure geometry math for GeckoLib/Blockbench geo.json models. // No Three.js dependency — shared by model_viewer.js (browser) and unit tests (Node). // // Transforms follow the GeckoLib convention: each bone contributes // local = T(pivot) · R(bone.rotation) · T(−pivot) // and cubes are authored in model space (origin at feet, Y up). // `build()` flattens the bone tree into world-space cubes, avoiding the // nested-pivot double-counting that broke naive group-based renderers. (function (global, factory) { if (typeof module !== 'undefined' && module.exports) { module.exports = factory(); } else { global.GeoBuilder = factory(); } })(typeof self !== 'undefined' ? self : this, function () { 'use strict'; var DEG = Math.PI / 180; // ---------- minimal 4x4 matrix helpers (column-major, Three.js order) ---------- function identity() { return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; } function multiply(a, b) { var out = new Array(16); for (var c = 0; c < 4; c++) { for (var r = 0; r < 4; r++) { out[c * 4 + r] = a[0 * 4 + r] * b[c * 4 + 0] + a[1 * 4 + r] * b[c * 4 + 1] + a[2 * 4 + r] * b[c * 4 + 2] + a[3 * 4 + r] * b[c * 4 + 3]; } } return out; } function translation(x, y, z) { return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]; } function rotationXYZ(rx, ry, rz) { var cx = Math.cos(rx), sx = Math.sin(rx); var cy = Math.cos(ry), sy = Math.sin(ry); var cz = Math.cos(rz), sz = Math.sin(rz); // R = Rz * Ry * Rx (matches Three.js Euler 'XYZ' default order) var r = identity(); r[0] = cy * cz; r[1] = cy * sz; r[2] = -sy; r[4] = sx * sy * cz - cx * sz; r[5] = sx * sy * sz + cx * cz; r[6] = sx * cy; r[8] = cx * sy * cz + sx * sz; r[9] = cx * sy * sz - sx * cz; r[10] = cx * cy; return r; } function transformPoint(m, p) { var x = p[0], y = p[1], z = p[2]; var w = m[3] * x + m[7] * y + m[11] * z + m[15]; return [ (m[0] * x + m[4] * y + m[8] * z + m[12]) / w, (m[1] * x + m[5] * y + m[9] * z + m[13]) / w, (m[2] * x + m[6] * y + m[10] * z + m[14]) / w, ]; } // ---------- box UV ---------- // Minecraft "box UV" face rects (u, v, w, h) in texture pixels. function boxUVRects(uv, size) { var u = uv[0], v = uv[1]; var x = size[0], y = size[1], z = size[2]; return { top: [u + z, v, x, z], bottom: [u + z + x, v, x, z], north: [u + z, v + z, x, y], south: [u + z + x, v + z, x, y], east: [u, v + z, z, y], west: [u + z + x + z, v + z, z, y], }; } // Build one cube as a BufferGeometry-compatible mesh: // 24 positions (6 faces x 4 corners), 24 UVs, 36 indices, outward winding. function cubeGeometry(size, uv, tw, th) { var hx = size[0] / 2, hy = size[1] / 2, hz = size[2] / 2; var rects = boxUVRects(uv, size); var order = ['east', 'west', 'top', 'bottom', 'south', 'north']; var outward = { east: [1, 0, 0], west: [-1, 0, 0], top: [0, 1, 0], bottom: [0, -1, 0], south: [0, 0, 1], north: [0, 0, -1], }; var positions = []; var uvs = []; var indices = []; var v = 0; function pushFace(face, corners) { var a = corners[0], b = corners[1], c = corners[2]; var abx = b[0] - a[0], aby = b[1] - a[1], abz = b[2] - a[2]; var acx = c[0] - a[0], acy = c[1] - a[1], acz = c[2] - a[2]; var nx = aby * acz - abz * acy; var ny = abz * acx - abx * acz; var nz = abx * acy - aby * acx; var out = outward[face]; if (nx * out[0] + ny * out[1] + nz * out[2] < 0) { var tmp = corners[1]; corners[1] = corners[2]; corners[2] = tmp; } for (var i = 0; i < 4; i++) { var cor = corners[i]; positions.push(cor[0], cor[1], cor[2]); uvs.push(cor[3], cor[4]); } indices.push(v, v + 1, v + 2, v, v + 2, v + 3); v += 4; } for (var i = 0; i < order.length; i++) { var face = order[i]; var rect = rects[face]; var u0 = rect[0] / tw, u1 = (rect[0] + rect[2]) / tw; var vTop = 1 - (rect[1] + rect[3]) / th; var vBot = 1 - rect[1] / th; var cs; if (face === 'east') { cs = [ [hx, hy, -hz, u1, vTop], [hx, hy, hz, u0, vTop], [hx, -hy, hz, u0, vBot], [hx, -hy, -hz, u1, vBot], ]; } else if (face === 'west') { cs = [ [-hx, hy, hz, u1, vTop], [-hx, hy, -hz, u0, vTop], [-hx, -hy, -hz, u0, vBot], [-hx, -hy, hz, u1, vBot], ]; } else if (face === 'top') { cs = [ [-hx, hy, hz, u0, vTop], [hx, hy, hz, u1, vTop], [hx, hy, -hz, u1, vBot], [-hx, hy, -hz, u0, vBot], ]; } else if (face === 'bottom') { cs = [ [-hx, -hy, -hz, u0, vTop], [hx, -hy, -hz, u1, vTop], [hx, -hy, hz, u1, vBot], [-hx, -hy, hz, u0, vBot], ]; } else if (face === 'south') { cs = [ [hx, hy, hz, u0, vTop], [-hx, hy, hz, u1, vTop], [-hx, -hy, hz, u1, vBot], [hx, -hy, hz, u0, vBot], ]; } else { // north cs = [ [-hx, hy, -hz, u0, vTop], [hx, hy, -hz, u1, vTop], [hx, -hy, -hz, u1, vBot], [-hx, -hy, -hz, u0, vBot], ]; } pushFace(face, cs); } return { positions: positions, uvs: uvs, indices: indices }; } // ---------- model build ---------- // Returns { cubes: [{matrix, size, uv, texture}], texture_width, texture_height } function build(geo) { var geometry = ((geo['minecraft:geometry'] || [])[0]) || null; if (!geometry) return { cubes: [], texture_width: 64, texture_height: 64 }; var tw = (geometry.description && geometry.description.texture_width) || 64; var th = (geometry.description && geometry.description.texture_height) || 64; var textures = geo['afw_bone_textures'] || {}; var defaultTex = null; for (var k in textures) { defaultTex = textures[k]; break; } var bones = {}; (geometry.bones || []).forEach(function (b) { bones[b.name] = b; }); var cubes = []; function walk(name, parentMatrix) { var bone = bones[name]; if (!bone) return; var pivot = bone.pivot || [0, 0, 0]; // Bedrock/GeckoLib rotations are opposite the Three.js default // direction, so negate the Euler angles (identity models unaffected). var rot = (bone.rotation || [0, 0, 0]).map(function (d) { return -d * DEG; }); var local = multiply( translation(pivot[0], pivot[1], pivot[2]), multiply( rotationXYZ(rot[0], rot[1], rot[2]), translation(-pivot[0], -pivot[1], -pivot[2]) ) ); var world = multiply(parentMatrix, local); var tex = textures[name] || defaultTex; (bone.cubes || []).forEach(function (c) { var origin = c.origin || [0, 0, 0]; var size = c.size || [1, 1, 1]; var cRot = (c.rotation || [0, 0, 0]).map(function (d) { return -d * DEG; }); var cubeWorld = multiply( world, multiply( translation(origin[0], origin[1], origin[2]), multiply( rotationXYZ(cRot[0], cRot[1], cRot[2]), translation(size[0] / 2, size[1] / 2, size[2] / 2) ) ) ); cubes.push({ matrix: cubeWorld, size: size, uv: c.uv || [0, 0], texture: tex, }); }); for (var child in bones) { if (bones[child].parent === name) walk(child, world); } } for (var root in bones) { if (!bones[root].parent) walk(root, identity()); } return { cubes: cubes, texture_width: tw, texture_height: th }; } return { build: build, cubeGeometry: cubeGeometry, boxUVRects: boxUVRects, transformPoint: transformPoint, identity: identity, translation: translation, rotationXYZ: rotationXYZ, multiply: multiply, }; });