Files
NoN-Site/nonpacks/static/js/geo_builder.js
T

257 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 hasRot = !!(c.rotation && (c.rotation[0] || c.rotation[1] || c.rotation[2]));
// Blockbench cubes rotate around their own pivot (fall back to origin).
var cPivot = hasRot ? (c.pivot || origin) : origin;
var center = [
origin[0] + size[0] / 2,
origin[1] + size[1] / 2,
origin[2] + size[2] / 2,
];
var cubeWorld = multiply(
world,
multiply(
translation(cPivot[0], cPivot[1], cPivot[2]),
multiply(
rotationXYZ(cRot[0], cRot[1], cRot[2]),
multiply(
translation(-cPivot[0], -cPivot[1], -cPivot[2]),
translation(center[0], center[1], center[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,
};
});