working Player-like geometry renderer
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"format_version": "1.12.0",
|
||||
"afw_bone_textures": {
|
||||
"body": "needsofnature:textures/entity/zombie/zombie.png",
|
||||
"leftleg": "needsofnature:textures/entity/zombie/zombie.png",
|
||||
"rightleg": "needsofnature:textures/entity/zombie/zombie.png",
|
||||
"d": "needsofnature:textures/entity/zombie/zombie.png"
|
||||
},
|
||||
"minecraft:geometry": [
|
||||
{
|
||||
"description": {
|
||||
"identifier": "geometry.unknown",
|
||||
"texture_width": 64,
|
||||
"texture_height": 64,
|
||||
"visible_bounds_width": 2,
|
||||
"visible_bounds_height": 3.5,
|
||||
"visible_bounds_offset": [0, 1.25, 0]
|
||||
},
|
||||
"bones": [
|
||||
{
|
||||
"name": "root",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "zombie",
|
||||
"parent": "root",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "waist",
|
||||
"parent": "zombie",
|
||||
"pivot": [0, 12, 0]
|
||||
},
|
||||
{
|
||||
"name": "head",
|
||||
"parent": "waist",
|
||||
"pivot": [0, 24, 0],
|
||||
"cubes": [
|
||||
{"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "headwear",
|
||||
"parent": "waist",
|
||||
"pivot": [0, 24, 0],
|
||||
"cubes": [
|
||||
{"origin": [-4, 24, -4], "size": [8, 8, 8], "inflate": 0.5, "uv": [32, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"parent": "waist",
|
||||
"pivot": [0, 24, 0],
|
||||
"cubes": [
|
||||
{"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "d",
|
||||
"parent": "body",
|
||||
"pivot": [0, 12, 0],
|
||||
"cubes": [
|
||||
{"origin": [-1, 10.75, -6], "size": [2, 2, 5], "inflate": -0.3, "uv": [17, 8]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "leftarm",
|
||||
"parent": "waist",
|
||||
"pivot": [5, 22, 0],
|
||||
"mirror": true,
|
||||
"cubes": [
|
||||
{"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 16]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "propleft",
|
||||
"parent": "leftarm",
|
||||
"pivot": [6, 12, 0]
|
||||
},
|
||||
{
|
||||
"name": "rightarm",
|
||||
"parent": "waist",
|
||||
"pivot": [-5, 22, 0],
|
||||
"cubes": [
|
||||
{"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "propright",
|
||||
"parent": "rightarm",
|
||||
"pivot": [-6, 12, 0]
|
||||
},
|
||||
{
|
||||
"name": "leftleg",
|
||||
"parent": "zombie",
|
||||
"pivot": [1.9, 12, 0],
|
||||
"mirror": true,
|
||||
"cubes": [
|
||||
{"origin": [0, 0, -2], "size": [4, 12, 4], "uv": [0, 16]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rightleg",
|
||||
"parent": "zombie",
|
||||
"pivot": [-1.9, 12, 0],
|
||||
"cubes": [
|
||||
{"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [0, 16]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Unit tests for geo_builder.js — run with: node static/js/tests/geo_builder.test.js
|
||||
// No framework: asserts, prints a summary, exits non-zero on failure.
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const GeoBuilder = require(path.join(__dirname, '..', 'geo_builder.js'));
|
||||
|
||||
const zombie = require(path.join(__dirname, 'fixtures', 'zombie.geo.json'));
|
||||
|
||||
let failures = 0;
|
||||
let checks = 0;
|
||||
|
||||
function approx(a, b, tol = 1e-6) {
|
||||
return Math.abs(a - b) <= tol;
|
||||
}
|
||||
|
||||
function assertVec(actual, expected, label, tol = 1e-6) {
|
||||
checks++;
|
||||
const ok = approx(actual[0], expected[0], tol) && approx(actual[1], expected[1], tol) && approx(actual[2], expected[2], tol);
|
||||
if (!ok) {
|
||||
failures++;
|
||||
console.error(`FAIL ${label}: got [${actual.map(v => v.toFixed(4)).join(', ')}] expected [${expected.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(cond, label) {
|
||||
checks++;
|
||||
if (!cond) { failures++; console.error(`FAIL ${label}`); }
|
||||
}
|
||||
|
||||
function worldCenter(matrix) {
|
||||
return GeoBuilder.transformPoint(matrix, [0, 0, 0]);
|
||||
}
|
||||
|
||||
// ---------- 1. Real zombie model (nested hierarchy, identity rotations) ----------
|
||||
const built = GeoBuilder.build(zombie);
|
||||
const tw = built.texture_width, th = built.texture_height;
|
||||
|
||||
assert(built.cubes.length === 8, `zombie cube count (got ${built.cubes.length})`);
|
||||
|
||||
const centers = built.cubes.map(c => ({ c, center: worldCenter(c.matrix) }));
|
||||
|
||||
function cubeNear(center) {
|
||||
return centers.filter(({ center: c }) =>
|
||||
approx(c[0], center[0], 1e-6) && approx(c[1], center[1], 1e-6) && approx(c[2], center[2], 1e-6)
|
||||
).map(x => x.c);
|
||||
}
|
||||
|
||||
assert(cubeNear([0, 28, 0]).length >= 2, 'head + headwear at (0,28,0)'); // head & headwear
|
||||
assert(cubeNear([0, 18, 0]).length === 1, 'body at (0,18,0)');
|
||||
assert(cubeNear([6, 18, 0]).length === 1, 'leftarm at (6,18,0)');
|
||||
assert(cubeNear([-6, 18, 0]).length === 1, 'rightarm at (-6,18,0)');
|
||||
assert(cubeNear([2, 6, 0]).length === 1, 'leftleg at (2,6,0)');
|
||||
assert(cubeNear([-2, 6, 0]).length === 1, 'rightleg at (-2,6,0)');
|
||||
assert(cubeNear([0, 11.75, -3.5]).length === 1, 'd at (0,11.75,-3.5)');
|
||||
|
||||
// Head = the (0,28,0) cube with uv [0,0]; headwear has uv [32,0].
|
||||
const headCube = cubeNear([0, 28, 0]).find(c => c.uv[0] === 0 && c.uv[1] === 0);
|
||||
assert(!!headCube, 'head cube identified');
|
||||
if (headCube) {
|
||||
// Head north face (last face) must sample the vanilla (8..16, 8..16) region.
|
||||
const geo = GeoBuilder.cubeGeometry(headCube.size, headCube.uv, tw, th);
|
||||
const northStart = 5 * 8; // face order: east,west,top,bottom,south,north
|
||||
let minU = 2, maxU = -1, minV = 2, maxV = -1;
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
minU = Math.min(minU, geo.uvs[northStart + i]);
|
||||
maxU = Math.max(maxU, geo.uvs[northStart + i]);
|
||||
minV = Math.min(minV, geo.uvs[northStart + i + 1]);
|
||||
maxV = Math.max(maxV, geo.uvs[northStart + i + 1]);
|
||||
}
|
||||
assert(approx(minU, 8 / 64) && approx(maxU, 16 / 64), `head north U in (8..16)/64 (got ${minU.toFixed(3)}..${maxU.toFixed(3)})`);
|
||||
assert(approx(minV, 1 - 16 / 64) && approx(maxV, 1 - 8 / 64), `head north V in [0.75,0.875] (got ${minV.toFixed(3)}..${maxV.toFixed(3)})`);
|
||||
}
|
||||
|
||||
// All UVs within [0,1].
|
||||
let allInRange = true;
|
||||
for (const { c } of centers) {
|
||||
const g = GeoBuilder.cubeGeometry(c.size, c.uv, tw, th);
|
||||
for (const u of g.uvs) if (u < 0 || u > 1) { allInRange = false; break; }
|
||||
}
|
||||
assert(allInRange, 'all UVs within [0,1]');
|
||||
|
||||
// Unmapped bones default to the pack's main texture.
|
||||
assert(headCube.texture === 'needsofnature:textures/entity/zombie/zombie.png', 'head uses default texture');
|
||||
|
||||
// ---------- 2. Synthetic rotated bone: Rz(90°) around pivot (0,10,0) ----------
|
||||
const rotated = {
|
||||
'minecraft:geometry': [{
|
||||
description: { identifier: 'geometry.t', texture_width: 64, texture_height: 64 },
|
||||
bones: [{
|
||||
name: 'waist', pivot: [0, 10, 0], rotation: [0, 0, 90],
|
||||
cubes: [{ origin: [0, 10, 0], size: [2, 2, 2], uv: [0, 0] }],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
const rotBuilt = GeoBuilder.build(rotated);
|
||||
assert(rotBuilt.cubes.length === 1, 'rotated model has one cube');
|
||||
// cube center = origin + size/2 = (1,11,1); Rz90 around pivot (0,10,0):
|
||||
// rel (1,1,1) -> (-1,1,1) -> +pivot = (-1,11,1)
|
||||
const rotCenter = worldCenter(rotBuilt.cubes[0].matrix);
|
||||
assertVec(rotCenter, [-1, 11, 1], 'rotated cube center around pivot (expect (-1,11,1))');
|
||||
|
||||
// ---------- 3. Nested pivot with rotation ----------
|
||||
const nested = {
|
||||
'minecraft:geometry': [{
|
||||
description: { identifier: 'geometry.n', texture_width: 64, texture_height: 64 },
|
||||
bones: [
|
||||
{ name: 'root', pivot: [0, 0, 0] },
|
||||
{ name: 'waist', pivot: [0, 12, 0], parent: 'root', rotation: [0, 0, 0] },
|
||||
{ name: 'head', pivot: [0, 24, 0], parent: 'waist',
|
||||
cubes: [{ origin: [-4, 24, -4], size: [8, 8, 8], uv: [0, 0] }] },
|
||||
],
|
||||
}],
|
||||
};
|
||||
const nestedBuilt = GeoBuilder.build(nested);
|
||||
assertVec(worldCenter(nestedBuilt.cubes[0].matrix), [0, 28, 0], 'nested pivot head center (expect (0,28,0))');
|
||||
|
||||
// ---------- summary ----------
|
||||
if (failures === 0) {
|
||||
console.log(`geo_builder tests OK (${checks} checks)`);
|
||||
} else {
|
||||
console.error(`${failures}/${checks} checks FAILED`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user