Finished with Phase 2.6

This commit is contained in:
2026-08-04 01:46:58 -05:00
parent 8c13c5e5ec
commit b7b21ab5e0
11 changed files with 1350 additions and 41 deletions
+222
View File
@@ -0,0 +1,222 @@
// Minimal GeckoLib/Blockbench geo.json viewer (Three.js) for the Models tab.
// Renders cubes with the standard "box UV" layout and the NoN `afw_bone_textures`
// map (bone → texture), textures served through the gated pack_asset endpoint.
(function () {
let renderer = null;
let scene = null;
let camera = null;
let meshRoot = null;
let rafId = null;
function assetUrl(baseUrl, member) {
return baseUrl.replace('ASSET', member);
}
function boneChildren(map, name, rootName) {
return Object.keys(map).filter(
b => map[b].parent === name || (name === rootName && !map[b].parent)
);
}
// Minecraft "box UV" face rects (u, v, w, h) in texture pixels.
function boxUVRects(uv, size) {
const u = uv[0], v = uv[1];
const 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],
};
}
function assignBoxUVs(geometry, uv, tw, th) {
// Three.js BoxGeometry groups: 0=px(east) 1=nx(west) 2=py(top) 3=ny(bottom) 4=pz(south) 5=nz(north)
const rects = boxUVRects(uv, [
geometry.parameters.width,
geometry.parameters.height,
geometry.parameters.depth,
]);
const order = ['east', 'west', 'top', 'bottom', 'south', 'north'];
const uvs = geometry.attributes.uv;
const positions = geometry.attributes.position;
for (let g = 0; g < geometry.groups.length; g++) {
const face = order[g];
if (!face) break;
const [ru, rv, rw, rh] = rects[face];
const u0 = ru / tw;
const v0 = 1 - (rv + rh) / th; // flip: image v=0 is the top
const u1 = (ru + rw) / tw;
const v1 = 1 - rv / th;
const start = geometry.groups[g].start;
const count = geometry.groups[g].count;
for (let i = start; i < start + count; i++) {
// Keep U/V in order (positions already give 0..1 corners per face).
const px = positions.getX(i), py = positions.getY(i), pz = positions.getZ(i);
let U, V;
if (face === 'east' || face === 'west') { U = (pz + 0.5) * (u1 - u0) + u0; V = (py + 0.5) * (v1 - v0) + v0; }
else if (face === 'top' || face === 'bottom') { U = (px + 0.5) * (u1 - u0) + u0; V = (pz + 0.5) * (v1 - v0) + v0; }
else { U = (px + 0.5) * (u1 - u0) + u0; V = (py + 0.5) * (v1 - v0) + v0; }
uvs.setXY(i, U, V);
}
}
uvs.needsUpdate = true;
}
function buildBone(map, name, textures, tw, th, parentGroup) {
const bone = map[name];
const group = new THREE.Group();
if (bone.pivot) group.position.set(bone.pivot[0], bone.pivot[1], bone.pivot[2]);
if (bone.rotation) {
const r = bone.rotation;
group.rotation.order = 'XYZ';
group.rotation.set(r[0] * Math.PI / 180, r[1] * Math.PI / 180, r[2] * Math.PI / 180);
}
const texMember = textures[name];
let material;
if (texMember) {
const loader = new THREE.TextureLoader();
const texture = loader.load(assetUrl(window.__PACK_ASSET_BASE__ || '', texMember));
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
material = new THREE.MeshStandardMaterial({ map: texture, roughness: 0.9, metalness: 0.0 });
} else {
material = new THREE.MeshStandardMaterial({ color: 0x9a9a9a, roughness: 0.9 });
}
for (const cube of bone.cubes || []) {
const size = new THREE.Vector3(cube.size[0], cube.size[1], cube.size[2]);
const geometry = new THREE.BoxGeometry(size.x, size.y, size.z);
assignBoxUVs(geometry, cube.uv || [0, 0], tw, th);
const mesh = new THREE.Mesh(geometry, material);
const cx = cube.origin[0] + size.x / 2;
const cy = cube.origin[1] + size.y / 2;
const cz = cube.origin[2] + size.z / 2;
mesh.position.set(cx, cy, cz);
if (cube.rotation) {
const r = cube.rotation;
mesh.rotation.set(r[0] * Math.PI / 180, r[1] * Math.PI / 180, r[2] * Math.PI / 180);
}
group.add(mesh);
}
for (const child of boneChildren(map, name, null)) {
buildBone(map, child, textures, tw, th, group);
}
parentGroup.add(group);
}
function disposeObject(obj) {
obj.traverse((node) => {
if (node.geometry) node.geometry.dispose();
if (node.material) {
if (node.material.map) node.material.map.dispose();
node.material.dispose();
}
});
}
function render(member, container, baseUrl) {
window.__PACK_ASSET_BASE__ = baseUrl;
if (renderer) dispose();
const width = container.clientWidth || 480;
const height = container.clientHeight || 480;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1e1e2e);
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 2000);
camera.position.set(0, 20, 60);
camera.lookAt(0, 12, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio || 1);
container.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const key = new THREE.DirectionalLight(0xffffff, 0.9);
key.position.set(30, 60, 30);
scene.add(key);
const fill = new THREE.DirectionalLight(0xffffff, 0.3);
fill.position.set(-30, 20, -30);
scene.add(fill);
meshRoot = new THREE.Group();
scene.add(meshRoot);
const url = assetUrl(baseUrl, member);
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(r => { if (!r.ok) throw new Error('http'); return r.json(); })
.then(geo => {
const geometry = (geo['minecraft:geometry'] || [])[0];
if (!geometry) throw new Error('no geometry');
const desc = geometry.description || {};
const tw = desc.texture_width || 64;
const th = desc.texture_height || 64;
const textures = geo['afw_bone_textures'] || {};
const map = {};
(geometry.bones || []).forEach(b => { map[b.name] = b; });
const roots = Object.keys(map).filter(b => !map[b].parent);
roots.forEach(root => buildBone(map, root, textures, tw, th, meshRoot));
// Fit camera to bounds.
const box = new THREE.Box3().setFromObject(meshRoot);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const radius = Math.max(size.x, size.y, size.z) / 2 || 10;
meshRoot.position.sub(center);
camera.position.set(radius * 2.2, radius * 1.6, radius * 2.6);
camera.near = radius / 10;
camera.far = radius * 40;
camera.updateProjectionMatrix();
camera.lookAt(0, 0, 0);
})
.catch(() => {
container.innerHTML = '<p class="empty-hint">Could not load this model.</p>';
});
let isDragging = false;
let lastX = 0, lastY = 0;
renderer.domElement.addEventListener('mousedown', (e) => {
isDragging = true; lastX = e.clientX; lastY = e.clientY;
});
window.addEventListener('mouseup', () => { isDragging = false; });
window.addEventListener('mousemove', (e) => {
if (!isDragging || !meshRoot) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
meshRoot.rotation.y += dx * 0.01;
meshRoot.rotation.x += dy * 0.01;
});
renderer.domElement.addEventListener('wheel', (e) => {
e.preventDefault();
camera.position.multiplyScalar(e.deltaY > 0 ? 1.06 : 0.94);
}, { passive: false });
function animate() {
rafId = requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
function dispose() {
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
if (renderer) {
if (meshRoot) disposeObject(meshRoot);
renderer.dispose();
if (renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
}
renderer = null;
scene = null;
camera = null;
meshRoot = null;
}
window.PacksModelViewer = { render: render, dispose: dispose };
})();