working Player-like geometry renderer
This commit is contained in:
@@ -1,126 +1,55 @@
|
||||
// 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.
|
||||
// GeoJSON model viewer for the Models/Textures tab. Uses the shared GeoBuilder
|
||||
// geometry math (bone transforms + box-UV) and renders world-space cubes with
|
||||
// Three.js. Textures are served through the gated pack_asset endpoint.
|
||||
(function () {
|
||||
let renderer = null;
|
||||
let scene = null;
|
||||
let camera = null;
|
||||
let meshRoot = null;
|
||||
let rafId = null;
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const materialCache = {};
|
||||
|
||||
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)
|
||||
);
|
||||
function buildGeometry(size, uv, tw, th) {
|
||||
const g = GeoBuilder.cubeGeometry(size, uv, tw, th);
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(g.positions, 3));
|
||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(g.uvs, 2));
|
||||
geometry.setIndex(g.indices);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
// 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];
|
||||
function materialFor(member, baseUrl) {
|
||||
if (materialCache[member || '__gray__']) return materialCache[member || '__gray__'];
|
||||
let material;
|
||||
if (texMember) {
|
||||
const loader = new THREE.TextureLoader();
|
||||
const texture = loader.load(assetUrl(window.__PACK_ASSET_BASE__ || '', texMember));
|
||||
if (member) {
|
||||
const texture = textureLoader.load(assetUrl(baseUrl, member));
|
||||
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);
|
||||
materialCache[member || '__gray__'] = material;
|
||||
return material;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
for (const key in materialCache) {
|
||||
if (materialCache[key].map) materialCache[key].map.dispose();
|
||||
materialCache[key].dispose();
|
||||
}
|
||||
for (const key in materialCache) delete materialCache[key];
|
||||
}
|
||||
|
||||
function render(member, container, baseUrl) {
|
||||
window.__PACK_ASSET_BASE__ = baseUrl;
|
||||
if (renderer) dispose();
|
||||
const width = container.clientWidth || 480;
|
||||
const height = container.clientHeight || 480;
|
||||
@@ -147,22 +76,19 @@
|
||||
meshRoot = new THREE.Group();
|
||||
scene.add(meshRoot);
|
||||
|
||||
const url = assetUrl(baseUrl, member);
|
||||
fetch(url, { headers: { 'Accept': 'application/json' } })
|
||||
fetch(assetUrl(baseUrl, member), { 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));
|
||||
const built = GeoBuilder.build(geo);
|
||||
if (!built.cubes.length) throw new Error('no cubes');
|
||||
for (const cube of built.cubes) {
|
||||
const geometry = buildGeometry(cube.size, cube.uv, built.texture_width, built.texture_height);
|
||||
const mesh = new THREE.Mesh(geometry, materialFor(cube.texture, baseUrl));
|
||||
mesh.matrix = new THREE.Matrix4().fromArray(cube.matrix);
|
||||
mesh.matrixAutoUpdate = false;
|
||||
meshRoot.add(mesh);
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
Reference in New Issue
Block a user