Spaces:
Running
Running
File size: 7,630 Bytes
8fb1496 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | // [e29 coach] Model import for the config panel: parse .glb / .gltf (GLTFLoader) and .fbx (FBXLoader)
// with vendored three.js r180 (MIT), then hand plain triangle arrays to the raw-WebGL engine.
// three.js is only used as a parser here; the engine renders the result itself.
// Imported models are VISUAL ONLY: colliders and hitboxes stay the built-in ones.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
const MAX_VERTS = 1500000;
function extOf(name) { const m = /\.([a-z0-9]+)(?:[?#].*)?$/i.exec(name || ''); return m ? m[1].toLowerCase() : ''; }
function baseName(u) { try { u = decodeURIComponent(u); } catch (_) {} return u.split(/[\\/]/).pop().split('?')[0]; }
function parseBuffer(ext, buf, manager, basePath) {
return new Promise((resolve, reject) => {
if (ext === 'fbx') {
try { resolve(new FBXLoader(manager).parse(buf, basePath || '')); } catch (e) { reject(e); }
} else if (ext === 'glb' || ext === 'gltf') {
new GLTFLoader(manager).parse(buf, basePath || '', (g) => resolve(g.scene || (g.scenes && g.scenes[0])), reject);
} else reject(new Error('Unsupported file type: .' + ext + ' (use .glb, .gltf or .fbx)'));
});
}
function waitManager(manager) {
// textures load asynchronously after parse; wait (bounded) for the manager to go idle
return new Promise((resolve) => {
let done = false;
const finish = () => { if (!done) { done = true; resolve(); } };
manager.onLoad = finish;
setTimeout(finish, 6000);
setTimeout(() => { if (!manager._busy) finish(); }, 50);
});
}
function makeManager(fileMap) {
const manager = new THREE.LoadingManager();
manager._busy = 0;
manager.onStart = () => { manager._busy++; };
manager.onError = (u) => console.info('[import] could not load resource:', baseName(u), '(material colour used instead)');
if (fileMap) manager.setURLModifier((u) => fileMap[baseName(u).toLowerCase()] || u);
return manager;
}
/** files: FileList/array from <input type=file multiple>. Picks the first .glb/.gltf/.fbx as the main file;
* other selected files (.bin, textures) resolve relative references by file name. */
export async function loadFromFiles(files) {
files = Array.from(files || []);
const main = files.find((f) => /\.(glb|gltf|fbx)$/i.test(f.name));
if (!main) throw new Error('Pick a .glb, .gltf or .fbx file (plus its .bin/textures for .gltf/.fbx if separate)');
const fileMap = {}; const urls = [];
for (const f of files) { const u = URL.createObjectURL(f); urls.push(u); fileMap[f.name.toLowerCase()] = u; }
const manager = makeManager(fileMap);
const ext = extOf(main.name);
const buf = ext === 'gltf' ? await main.text() : await main.arrayBuffer();
const root = await parseBuffer(ext, buf, manager, '');
await waitManager(manager);
setTimeout(() => urls.forEach((u) => URL.revokeObjectURL(u)), 10000);
return { root, name: main.name, source: 'file' };
}
export async function loadFromUrl(url) {
const r = await fetch(url);
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url);
const ext = extOf(url);
const buf = ext === 'gltf' ? await r.text() : await r.arrayBuffer();
const manager = makeManager(null);
const basePath = url.slice(0, url.lastIndexOf('/') + 1);
const root = await parseBuffer(ext, buf, manager, basePath);
await waitManager(manager);
return { root, name: baseName(url), source: 'url' };
}
/** Flatten to world-space triangles, then normalise.
* mode 'scene': fit the XZ footprint to the 20x20 map (+-10), centre on X/Z, sit on y=0.
* mode 'character': fit into the collider's unit box (-0.5..0.5, feet at y=-0.5); the engine
* then scales it by the same matrix as the built-in character box (so fall/boom FX still apply). */
export function toEngineMeshes(root, mode) {
root.updateMatrixWorld(true);
const out = []; let verts = 0;
const tmpC = new THREE.Color();
root.traverse((o) => {
if (!o.isMesh || !o.geometry || !o.geometry.attributes.position) return;
let g = o.geometry.index ? o.geometry.toNonIndexed() : o.geometry.clone();
g.applyMatrix4(o.matrixWorld);
if (!g.attributes.normal) g.computeVertexNormals();
const mats = Array.isArray(o.material) ? o.material : [o.material];
const groups = (g.groups && g.groups.length && Array.isArray(o.material)) ? g.groups : [{ start: 0, count: g.attributes.position.count, materialIndex: 0 }];
const PA = g.attributes.position, NA = g.attributes.normal, UA = g.attributes.uv || null;
const take = (attr, start, count, dim) => {
const a = new Float32Array(count * dim);
for (let i = 0; i < count; i++) {
a[i * dim] = attr.getX(start + i); a[i * dim + 1] = attr.getY(start + i);
if (dim === 3) a[i * dim + 2] = attr.getZ(start + i);
}
return a;
};
for (const gr of groups) {
const count = Math.min(gr.count, PA.count - gr.start);
if (count <= 0) continue;
verts += count;
if (verts > MAX_VERTS) break;
const mat = mats[gr.materialIndex || 0] || mats[0] || {};
let color = [0.8, 0.8, 0.8];
if (mat.color) { mat.color.getRGB(tmpC, THREE.SRGBColorSpace); color = [tmpC.r, tmpC.g, tmpC.b]; }
const map = mat.map && mat.map.image ? mat.map : null;
out.push({
positions: take(PA, gr.start, count, 3),
normals: take(NA, gr.start, count, 3),
uvs: UA ? take(UA, gr.start, count, 2) : null,
color: map ? [1, 1, 1] : color,
image: map ? map.image : null,
flipY: map ? !!map.flipY : false
});
}
});
if (!out.length) throw new Error('No meshes found in that file');
if (verts > MAX_VERTS) throw new Error('Model too heavy for this WebGL1 engine (' + verts + ' verts > ' + MAX_VERTS + ')');
// bounds
let mn = [Infinity, Infinity, Infinity], mx = [-Infinity, -Infinity, -Infinity];
for (const m of out) for (let i = 0; i < m.positions.length; i += 3) for (let k = 0; k < 3; k++) {
const v = m.positions[i + k]; if (v < mn[k]) mn[k] = v; if (v > mx[k]) mx[k] = v;
}
const size = [mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]];
const cx = (mn[0] + mx[0]) / 2, cz = (mn[2] + mx[2]) / 2;
let s, oy;
if (mode === 'scene') {
const foot = Math.max(size[0], size[2]) || size[1] || 1;
s = 20 / foot; oy = -mn[1] * s;
} else {
const big = Math.max(size[1], size[0], size[2]) || 1;
s = 1 / big; oy = -0.5 - mn[1] * s;
}
for (const m of out) {
const p = m.positions;
for (let i = 0; i < p.length; i += 3) { p[i] = (p[i] - cx) * s; p[i + 1] = p[i + 1] * s + oy; p[i + 2] = (p[i + 2] - cz) * s; }
// uniform scale keeps normals valid (renormalise in case the source had scaled nodes)
const n = m.normals;
for (let i = 0; i < n.length; i += 3) { const l = Math.hypot(n[i], n[i + 1], n[i + 2]) || 1; n[i] /= l; n[i + 1] /= l; n[i + 2] /= l; }
}
return { meshes: out, verts, origSize: size, scale: s };
}
/** convenience: load + convert + hand to the engine */
export async function importInto(slot, { files, url }) {
const E = window.MuseEngine29;
if (!E) throw new Error('engine not ready');
const res = files ? await loadFromFiles(files) : await loadFromUrl(url);
const conv = toEngineMeshes(res.root, slot === 'scene' ? 'scene' : 'character');
const info = { name: res.name, source: res.source, url: url || null, meshes: conv.meshes.length, verts: conv.verts,
origSize: conv.origSize.map((v) => +v.toFixed(3)), scale: +conv.scale.toPrecision(4) };
E.setImportedModel(slot, conv.meshes, info);
return info;
}
|