Spaces:
Running
Running
Muse Engine 32 WebGL2 release candidate
Browse files- game/muse-authored/phase32/harness/ref/fbx6-keys.ref.js +37 -0
- game/muse-authored/phase32/harness/ref/model-check.ref.js +74 -0
- game/muse-authored/phase32/harness/ref/root-motion.ref.js +47 -0
- game/muse-authored/phase32/harness/ref/setup-schema.ref.js +128 -0
- game/muse-authored/phase32/harness/ref/texture-budget.ref.js +27 -0
game/muse-authored/phase32/harness/ref/fbx6-keys.ref.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Coach reference (Grok Bot) for the Phase 32 part C spec: engine/import/fbx6-keys.js. Used only by the harness.
|
| 2 |
+
(function (root) {
|
| 3 |
+
var KTIME = 46186158000, IM = { C: 'step', L: 'linear', U: 'cubic', T: 'cubic' };
|
| 4 |
+
function isN(v) { return typeof v === 'number'; }
|
| 5 |
+
function decode(tokens, keyCount) {
|
| 6 |
+
var r = { ok: true, error: '', count: 0, times: [], values: [], interp: [] };
|
| 7 |
+
if (!Array.isArray(tokens)) { r.ok = false; r.error = 'tokens must be an array'; return r; }
|
| 8 |
+
var errs = [], i = 0, n = tokens.length, stop = false;
|
| 9 |
+
while (i < n && !stop) {
|
| 10 |
+
if (!(isN(tokens[i]) && isN(tokens[i + 1]))) { errs.push('bad key at token ' + i); break; }
|
| 11 |
+
var t = tokens[i], v = tokens[i + 1], ip, j;
|
| 12 |
+
if (i + 2 >= n) { ip = 'linear'; j = n; }
|
| 13 |
+
else {
|
| 14 |
+
var c = tokens[i + 2];
|
| 15 |
+
if (typeof c !== 'string' || !IM[c]) { errs.push('unknown interpolation "' + c + '" at token ' + (i + 2)); break; }
|
| 16 |
+
ip = IM[c]; j = i + 3;
|
| 17 |
+
while (j < n && !((isN(tokens[j]) && isN(tokens[j + 1]) && (IM[tokens[j + 2]] !== undefined && typeof tokens[j + 2] === 'string' || j + 2 === n)))) j++;
|
| 18 |
+
}
|
| 19 |
+
r.times.push(t / KTIME); r.values.push(v); r.interp.push(ip); r.count++;
|
| 20 |
+
i = j;
|
| 21 |
+
}
|
| 22 |
+
if (typeof keyCount === 'number' && r.count !== keyCount) errs.push('expected ' + keyCount + ' keys, decoded ' + r.count);
|
| 23 |
+
for (var k = 1; k < r.times.length; k++) if (r.times[k] < r.times[k - 1]) { errs.push('times not ascending at key ' + k); break; }
|
| 24 |
+
if (errs.length) { r.ok = false; r.error = errs[0]; }
|
| 25 |
+
return r;
|
| 26 |
+
}
|
| 27 |
+
function stepToLinear(times, values, interp, eps) {
|
| 28 |
+
var e = typeof eps === 'number' && eps > 0 ? eps : 1e-4, T = [], V = [];
|
| 29 |
+
for (var i = 0; i < times.length; i++) {
|
| 30 |
+
T.push(times[i]); V.push(values[i]);
|
| 31 |
+
if (interp[i] === 'step' && i + 1 < times.length && values[i + 1] !== values[i] && times[i + 1] - times[i] > 2 * e) { T.push(times[i + 1] - e); V.push(values[i]); }
|
| 32 |
+
}
|
| 33 |
+
return { times: T, values: V };
|
| 34 |
+
}
|
| 35 |
+
root.MuseFbx6Keys = { KTIME: KTIME, decode: decode, stepToLinear: stepToLinear };
|
| 36 |
+
if (typeof module !== 'undefined' && module.exports) module.exports = root.MuseFbx6Keys;
|
| 37 |
+
})(typeof window !== 'undefined' ? window : globalThis);
|
game/muse-authored/phase32/harness/ref/model-check.ref.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// [e32 coach] Reference implementation of the p32a model-check spec (harness oracle, Grok Bot). Not shipped.
|
| 2 |
+
(function (root) {
|
| 3 |
+
var LIMITS = { sceneTrisWarn: 300000, sceneTrisError: 1000000, charTrisWarn: 100000, charTrisError: 400000, partsWarn: 400,
|
| 4 |
+
maxBonesDefault: 60, rootMotionMinM: 0.10, rootMotionFrac: 0.05, charHeightMin: 0.5, charHeightMax: 3.0,
|
| 5 |
+
sceneExtentMin: 2, sceneExtentMax: 5000, fpsLow: 15 };
|
| 6 |
+
var S = [12, 15, 23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60, 100, 120];
|
| 7 |
+
function estimateFps(times) {
|
| 8 |
+
var t = Array.prototype.slice.call(times || []), d = [];
|
| 9 |
+
for (var i = 1; i < t.length; i++) { var x = t[i] - t[i - 1]; if (x >= 1e-6) d.push(x); }
|
| 10 |
+
if (!d.length) return { fps: 0, frames: t.length, standard: false };
|
| 11 |
+
d.sort(function (a, b) { return a - b; });
|
| 12 |
+
var dt = d[(d.length - 1) >> 1], raw = 1 / dt, best = S[0];
|
| 13 |
+
for (var k = 1; k < S.length; k++) if (Math.abs(raw - S[k]) < Math.abs(raw - best)) best = S[k];
|
| 14 |
+
var fps, std;
|
| 15 |
+
if (Math.abs(raw - best) <= 0.02 * best) { fps = best; std = true; } else { fps = Math.round(raw * 100) / 100; std = false; }
|
| 16 |
+
return { fps: fps, frames: Math.round((t[t.length - 1] - t[0]) * fps) + 1, standard: std };
|
| 17 |
+
}
|
| 18 |
+
function num(v) { return typeof v === 'number' && isFinite(v) ? v : 0; }
|
| 19 |
+
function arr(v) { return Array.isArray(v) ? v : []; }
|
| 20 |
+
function obj(v) { return v && typeof v === 'object' && !Array.isArray(v) ? v : {}; }
|
| 21 |
+
function analyze(facts) {
|
| 22 |
+
var f = obj(facts), W = [];
|
| 23 |
+
function add(code, level, msg, fix) { W.push({ code: code, level: level, msg: msg, fix: fix }); }
|
| 24 |
+
var rig = obj(f.rig), units = obj(f.units), up = obj(f.up), tex = obj(f.textures), sc = obj(f.scene);
|
| 25 |
+
var clips = arr(rig.clips);
|
| 26 |
+
if (f.kind === 'character') {
|
| 27 |
+
if (rig.skinned !== true) add('NO_SKIN', 'warn', 'No skinned mesh / skeleton: drawn as a static model scaled into the enemy box (no animation).', 'Export with Skin/Skinning and Deformations enabled, bind the mesh to the skeleton.');
|
| 28 |
+
else {
|
| 29 |
+
var maxB = num(rig.maxBones) || LIMITS.maxBonesDefault, merged = arr(rig.merged), bones = num(rig.bones);
|
| 30 |
+
if (bones > maxB) add('BONES_OVER_LIMIT', 'warn', bones + ' bones > ' + maxB + ' GPU slots: ' + merged.length + ' minor leaf bones merged into their parents' + (merged.length ? ' (' + merged.slice(0, 8).join(', ') + (merged.length > 8 ? ', …' : '') + ')' : '') + '.', 'Remove or skip fingers/twist/helper bones on export, or accept the merge.');
|
| 31 |
+
if (clips.length === 0) add('NO_CLIPS', 'warn', 'Skinned but no animation takes: the character stands in its bind pose.', 'Bake animation on export (Maya: Bake Animation; Max: Bake Animation) and include the takes.');
|
| 32 |
+
var th = Math.max(LIMITS.rootMotionMinM, LIMITS.rootMotionFrac * (num(rig.heightM) || 1.8));
|
| 33 |
+
var moving = clips.filter(function (c) { return num(c.rootMotionM) > th; });
|
| 34 |
+
if (moving.length) {
|
| 35 |
+
var lv = moving.every(function (c) { return c.stripped; }) ? 'info' : 'warn';
|
| 36 |
+
add('ROOT_MOTION', lv, 'Root motion in ' + moving.length + ' clip(s): ' + moving.map(function (c) { return c.name + ' ' + num(c.rootMotionM).toFixed(2) + ' m'; }).join(', ') + (lv === 'info' ? ' (stripped: played in place).' : ' (not stripped: the character slides away from its capsule).'), 'Keep the toggle "Strip root motion" on, or export in-place animations.');
|
| 37 |
+
}
|
| 38 |
+
if (clips.length > 0) {
|
| 39 |
+
var roles = obj(rig.roles);
|
| 40 |
+
var miss = ['idle', 'walk', 'run', 'shoot', 'death'].filter(function (r) { return !(typeof roles[r] === 'number' && roles[r] >= 0); });
|
| 41 |
+
if (miss.length) add('UNMAPPED_ROLES', 'info', 'No clip found for: ' + miss.join(', ') + ' (pick one in the clip picker, or name takes e.g. Idle, Walk, Run, Shoot, Death).', 'Rename the takes or pick clips in the panel.');
|
| 42 |
+
}
|
| 43 |
+
var low = [], odd = [];
|
| 44 |
+
clips.forEach(function (c) { var e = estimateFps(c.times); if (e.fps > 0 && e.fps < LIMITS.fpsLow) low.push([c, e]); if (e.fps > 0 && !e.standard) odd.push([c, e]); });
|
| 45 |
+
function lst(a) { return a.map(function (x) { return x[0].name + ' ' + x[1].fps + ' fps'; }).join(', '); }
|
| 46 |
+
if (low.length) add('LOW_FPS', 'warn', 'Low key rate in: ' + lst(low) + '.', 'Bake at 30 fps (every frame).');
|
| 47 |
+
else if (odd.length) add('ODD_FPS', 'info', 'Unusual key rate in: ' + lst(odd) + '.', 'Bake every frame at the scene frame rate (24/25/30/60).');
|
| 48 |
+
}
|
| 49 |
+
var h = num(rig.heightM);
|
| 50 |
+
if (h > 0 && (h < LIMITS.charHeightMin || h > LIMITS.charHeightMax)) add('ODD_SIZE', 'warn', 'Character is ' + h.toFixed(2) + ' m tall after unit conversion: check the units / scale.', 'Set the units in the panel (Units) or export in centimetres with unit metadata.');
|
| 51 |
+
}
|
| 52 |
+
if (f.kind === 'scene') {
|
| 53 |
+
var ext = Math.max.apply(null, arr(f.sizeM).map(num).concat([0]));
|
| 54 |
+
if (ext > 0 && (ext < LIMITS.sceneExtentMin || ext > LIMITS.sceneExtentMax)) add('ODD_SIZE', 'warn', 'Scene is ' + ext.toFixed(1) + ' m across after unit conversion: check the units / scale.', 'Set the units in the panel (Units) or export in centimetres with unit metadata.');
|
| 55 |
+
}
|
| 56 |
+
if (units.source === 'guess') add('NO_UNITS_INFO', 'warn', 'No unit information in the file: guessed ' + units.value + ' from the size.', 'FBX: keep the file units (Maya: Preferences > Settings > Working units; export with Automatic units). glTF is always metres.');
|
| 57 |
+
if (up.source === 'guess') add('NO_UP_INFO', 'warn', 'No up-axis information in the file: assumed ' + String(up.value).toUpperCase() + '-up.', 'FBX: export with Up Axis set (Y or Z); the engine converts Z-up to Y-up.');
|
| 58 |
+
var miss2 = arr(tex.missing);
|
| 59 |
+
if (miss2.length) add('MISSING_TEXTURES', 'warn', miss2.length + ' texture(s) not found: ' + miss2.slice(0, 6).join(', ') + (miss2.length > 6 ? ', …' : '') + '. Material colours used instead.', 'Embed media in the FBX (Embed Media) or select the texture files together with the model; for .gltf select the .bin and images too.');
|
| 60 |
+
var tris = num(f.triangles), isS = f.kind === 'scene';
|
| 61 |
+
var wAt = isS ? LIMITS.sceneTrisWarn : LIMITS.charTrisWarn, eAt = isS ? LIMITS.sceneTrisError : LIMITS.charTrisError;
|
| 62 |
+
if (tris >= eAt || tris >= wAt) { var er = tris >= eAt; add('HEAVY_MESH', er ? 'error' : 'warn', tris.toLocaleString('en-US') + ' triangles: ' + (er ? 'very heavy for WebGL1, expect low frame rates.' : 'heavy, frame rate may drop on laptops.'), 'Reduce polygons (Maya: Mesh > Reduce; Max: ProOptimizer) or remove hidden detail.'); }
|
| 63 |
+
if (num(f.parts) > LIMITS.partsWarn) add('MANY_PARTS', 'info', num(f.parts) + ' draw parts (meshes x materials): the engine batches parts that share a material.', 'Combine meshes that share a material before export.');
|
| 64 |
+
if (isS && sc.mode === 'level') {
|
| 65 |
+
if (num(sc.walkableTris) === 0) add('NO_FLOOR', 'error', 'No walkable floor found (no upward-facing collision triangles): you will fall.', 'Check the up axis / units, and that floors are not named _nocol.');
|
| 66 |
+
if (sc.spawn !== 'node') add('NO_SPAWN', 'info', 'No PlayerStart node: spawning on the nearest open floor.', 'Add an empty/dummy named PlayerStart where the player should start.');
|
| 67 |
+
}
|
| 68 |
+
var rank = { error: 0, warn: 1, info: 2 };
|
| 69 |
+
var sorted = W.map(function (w, i) { return [w, i]; }).sort(function (a, b) { return (rank[a[0].level] - rank[b[0].level]) || (a[1] - b[1]); }).map(function (x) { return x[0]; });
|
| 70 |
+
var e = sorted.filter(function (w) { return w.level === 'error'; }).length, wn = sorted.filter(function (w) { return w.level === 'warn'; }).length;
|
| 71 |
+
return { level: e ? 'error' : wn ? 'warn' : 'ok', warnings: sorted, summary: { errors: e, warns: wn, infos: sorted.length - e - wn, codes: sorted.map(function (w) { return w.code; }) } };
|
| 72 |
+
}
|
| 73 |
+
root.MuseModelCheck = { analyze: analyze, estimateFps: estimateFps, LIMITS: LIMITS };
|
| 74 |
+
})(typeof window !== 'undefined' ? window : globalThis);
|
game/muse-authored/phase32/harness/ref/root-motion.ref.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// [e32 coach] Reference implementation of the p32a root-motion spec (harness oracle, Grok Bot). Not shipped.
|
| 2 |
+
(function (root) {
|
| 3 |
+
function trk(clip, node) { var t = clip && clip.tracks || []; for (var i = 0; i < t.length; i++) if (t[i].node === node && t[i].path === 'translation') return t[i]; return null; }
|
| 4 |
+
function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
|
| 5 |
+
function h(v, up) { var d = dot(v, up); return [v[0] - d * up[0], v[1] - d * up[1], v[2] - d * up[2]]; }
|
| 6 |
+
function len(v) { return Math.hypot(v[0], v[1], v[2]); }
|
| 7 |
+
function key(t, i) { return [t.values[3 * i], t.values[3 * i + 1], t.values[3 * i + 2]]; }
|
| 8 |
+
function measure(clip, node, up) {
|
| 9 |
+
var t = trk(clip, node); if (!t) return null;
|
| 10 |
+
var n = t.times.length, first = key(t, 0), last = key(t, n - 1);
|
| 11 |
+
var net = [last[0] - first[0], last[1] - first[1], last[2] - first[2]], mh = 0;
|
| 12 |
+
for (var i = 0; i < n; i++) { var k = key(t, i); mh = Math.max(mh, len(h([k[0] - first[0], k[1] - first[1], k[2] - first[2]], up))); }
|
| 13 |
+
var hz = len(h(net, up));
|
| 14 |
+
return { keys: n, first: first, last: last, net: net, horiz: hz, vert: Math.abs(dot(net, up)), maxHoriz: mh, speed: clip.duration > 0 ? hz / clip.duration : 0 };
|
| 15 |
+
}
|
| 16 |
+
function strip(clip, node, up, opts) {
|
| 17 |
+
var mode = opts && opts.mode === 'lock' ? 'lock' : 'drift';
|
| 18 |
+
var t = trk(clip, node); if (!t) return { clip: clip, removed: [0, 0, 0], mode: mode };
|
| 19 |
+
var n = t.times.length, first = key(t, 0), last = key(t, n - 1);
|
| 20 |
+
var hn = h([last[0] - first[0], last[1] - first[1], last[2] - first[2]], up);
|
| 21 |
+
var v = new Float32Array(t.values.length), t0 = t.times[0], t1 = t.times[n - 1];
|
| 22 |
+
for (var i = 0; i < n; i++) {
|
| 23 |
+
var k = key(t, i), o;
|
| 24 |
+
if (mode === 'drift') { var u = t1 === t0 ? 0 : (t.times[i] - t0) / (t1 - t0); o = [u * hn[0], u * hn[1], u * hn[2]]; }
|
| 25 |
+
else o = h([k[0] - first[0], k[1] - first[1], k[2] - first[2]], up);
|
| 26 |
+
v[3 * i] = k[0] - o[0]; v[3 * i + 1] = k[1] - o[1]; v[3 * i + 2] = k[2] - o[2];
|
| 27 |
+
}
|
| 28 |
+
var nt = Object.assign({}, t, { values: v });
|
| 29 |
+
return { clip: { name: clip.name, duration: clip.duration, tracks: clip.tracks.map(function (x) { return x === t ? nt : x; }) }, removed: hn, mode: mode };
|
| 30 |
+
}
|
| 31 |
+
function moves(clips, node) {
|
| 32 |
+
for (var c = 0; c < clips.length; c++) { var t = trk(clips[c], node); if (!t) continue; var k0 = key(t, 0);
|
| 33 |
+
for (var i = 1; i < t.times.length; i++) { var k = key(t, i); if (len([k[0] - k0[0], k[1] - k0[1], k[2] - k0[2]]) > 1e-6) return true; } }
|
| 34 |
+
return false;
|
| 35 |
+
}
|
| 36 |
+
function pickRoot(skel, clips, hips) {
|
| 37 |
+
var nodes = skel && skel.nodes || [], cand = {};
|
| 38 |
+
(clips || []).forEach(function (c) { (c.tracks || []).forEach(function (t) { if (t.path === 'translation') cand[t.node] = true; }); });
|
| 39 |
+
if (hips >= 0) for (var n = hips, guard = 0; n >= 0 && guard < 10000; n = nodes[n] ? nodes[n].parent : -1, guard++) if (cand[n] && moves(clips, n)) return n;
|
| 40 |
+
var best = -1, bd = Infinity;
|
| 41 |
+
Object.keys(cand).map(Number).sort(function (a, b) { return a - b; }).forEach(function (i) {
|
| 42 |
+
if (!moves(clips, i)) return; var d = 0; for (var p = nodes[i] ? nodes[i].parent : -1; p >= 0 && d < 10000; p = nodes[p] ? nodes[p].parent : -1) d++;
|
| 43 |
+
if (d < bd) { bd = d; best = i; } });
|
| 44 |
+
return best;
|
| 45 |
+
}
|
| 46 |
+
root.MuseRootMotion = { measure: measure, strip: strip, pickRoot: pickRoot };
|
| 47 |
+
})(typeof window !== 'undefined' ? window : globalThis);
|
game/muse-authored/phase32/harness/ref/setup-schema.ref.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// [e32 coach] Reference implementation of the p32b setup-schema spec (harness oracle, Grok Bot). Not shipped.
|
| 2 |
+
// Revised after the Muse review: the 'loaded from a file' warning now names a non-string name as String(name || '') like make() (was '').
|
| 3 |
+
(function (root) {
|
| 4 |
+
var KIND = 'muse-engine-setup', VERSION = 1;
|
| 5 |
+
var UNITS = ['auto', 'm', 'cm', 'mm', 'in', 'ft'], UP = ['auto', 'y', 'z'], FACING = ['+z', '-z', '+x', '-x'], ROLES = ['idle', 'walk', 'run', 'shoot', 'death'], SOURCES = ['url', 'file'];
|
| 6 |
+
function isObj(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
|
| 7 |
+
function slot(s, which) {
|
| 8 |
+
if (s === null || s === undefined) return null;
|
| 9 |
+
var source = s.source === 'file' ? 'file' : 'url';
|
| 10 |
+
var o = { name: String(s.name || ''), source: source, url: source === 'url' ? String(s.url || '') : null };
|
| 11 |
+
if (which === 'scene') o.asLevel = s.asLevel !== false;
|
| 12 |
+
o.units = UNITS.indexOf(s.units) >= 0 ? s.units : 'auto';
|
| 13 |
+
o.up = UP.indexOf(s.up) >= 0 ? s.up : 'auto';
|
| 14 |
+
if (which === 'character') {
|
| 15 |
+
o.facing = FACING.indexOf(s.facing) >= 0 ? s.facing : '+z';
|
| 16 |
+
var cp = {}, src = isObj(s.clipPicks) ? s.clipPicks : {};
|
| 17 |
+
ROLES.forEach(function (r) { if (Object.prototype.hasOwnProperty.call(src, r) && (typeof src[r] === 'string' || src[r] === null)) cp[r] = src[r]; });
|
| 18 |
+
o.clipPicks = cp;
|
| 19 |
+
}
|
| 20 |
+
return o;
|
| 21 |
+
}
|
| 22 |
+
function make(st) {
|
| 23 |
+
st = st || {};
|
| 24 |
+
var cfg = {}, c = isObj(st.config) ? st.config : {};
|
| 25 |
+
Object.keys(c).sort().forEach(function (k) { if (typeof c[k] === 'boolean') cfg[k] = c[k]; });
|
| 26 |
+
return { kind: KIND, version: VERSION, engine: String(st.engine || ''), savedAt: String(st.savedAt || ''), look: String(st.look || 'A'), config: cfg,
|
| 27 |
+
scene: slot(st.scene, 'scene'), character: slot(st.character, 'character') };
|
| 28 |
+
}
|
| 29 |
+
function urlOk(u) {
|
| 30 |
+
var t = u.trim(); if (t.length > 2048) return false;
|
| 31 |
+
if (/^https?:\/\//.test(t)) return true;
|
| 32 |
+
var slash = t.indexOf('/'), colon = t.indexOf(':');
|
| 33 |
+
return colon < 0 || (slash >= 0 && colon > slash);
|
| 34 |
+
}
|
| 35 |
+
var TOP = ['kind', 'version', 'engine', 'savedAt', 'look', 'config', 'scene', 'character'];
|
| 36 |
+
function validate(o, opts) {
|
| 37 |
+
opts = opts || {};
|
| 38 |
+
var E = [], W = [];
|
| 39 |
+
if (!isObj(o)) return { ok: false, errors: ['not a JSON object'], warnings: [], value: null };
|
| 40 |
+
if (o.kind !== KIND) E.push('kind must be "muse-engine-setup"');
|
| 41 |
+
var v = o.version;
|
| 42 |
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 1) E.push('version must be a positive integer');
|
| 43 |
+
else if (v > VERSION) E.push('version ' + v + ' is newer than this engine (' + VERSION + ')');
|
| 44 |
+
Object.keys(o).forEach(function (k) { if (TOP.indexOf(k) < 0) W.push('unknown key "' + k + '" ignored'); });
|
| 45 |
+
var cfg = {};
|
| 46 |
+
if (o.config !== undefined) {
|
| 47 |
+
if (!isObj(o.config)) W.push('config ignored (not an object)');
|
| 48 |
+
else Object.keys(o.config).forEach(function (k) {
|
| 49 |
+
if (typeof o.config[k] !== 'boolean') W.push('config.' + k + ' ignored (not true/false)');
|
| 50 |
+
else if (opts.configKeys && opts.configKeys.indexOf(k) < 0) W.push('config.' + k + ' ignored (unknown toggle)');
|
| 51 |
+
else cfg[k] = o.config[k];
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
var look = o.look === undefined ? 'A' : String(o.look);
|
| 55 |
+
if (o.look !== undefined && opts.looks && opts.looks.indexOf(look) < 0) { W.push('look "' + look + '" unknown, using ' + opts.looks[0]); look = opts.looks[0]; }
|
| 56 |
+
var slots = {};
|
| 57 |
+
['scene', 'character'].forEach(function (sn) {
|
| 58 |
+
var s = o[sn];
|
| 59 |
+
if (s === undefined || s === null) { slots[sn] = null; return; }
|
| 60 |
+
if (!isObj(s)) { E.push(sn + ' must be an object or null'); return; }
|
| 61 |
+
if (s.source !== undefined && SOURCES.indexOf(s.source) < 0) E.push(sn + '.source must be url or file');
|
| 62 |
+
if ((s.source === 'url' || s.source === undefined) && !(typeof s.url === 'string' && s.url.length > 0)) E.push(sn + '.url is required for source url');
|
| 63 |
+
if (typeof s.url === 'string' && !urlOk(s.url)) E.push(sn + '.url is not allowed: ' + s.url);
|
| 64 |
+
if (s.name !== undefined && typeof s.name !== 'string') E.push(sn + '.name must be a string');
|
| 65 |
+
var fields = sn === 'scene' ? ['units', 'up'] : ['units', 'up', 'facing'];
|
| 66 |
+
fields.forEach(function (fd) {
|
| 67 |
+
var en = fd === 'units' ? UNITS : fd === 'up' ? UP : FACING;
|
| 68 |
+
if (s[fd] !== undefined && en.indexOf(s[fd]) < 0) W.push(sn + '.' + fd + ' "' + s[fd] + '" invalid, using ' + (fd === 'facing' ? '+z' : 'auto'));
|
| 69 |
+
});
|
| 70 |
+
if (sn === 'character' && isObj(s.clipPicks)) Object.keys(s.clipPicks).forEach(function (k) {
|
| 71 |
+
if (ROLES.indexOf(k) < 0) W.push(sn + '.clipPicks.' + k + ' ignored');
|
| 72 |
+
else if (!(typeof s.clipPicks[k] === 'string' || s.clipPicks[k] === null)) W.push(sn + '.clipPicks.' + k + ' ignored (not a clip name)');
|
| 73 |
+
});
|
| 74 |
+
var c2 = Object.assign({}, s);
|
| 75 |
+
if (typeof c2.name === 'string' && c2.name.length > 260) { c2.name = c2.name.slice(0, 260); W.push(sn + '.name truncated'); }
|
| 76 |
+
if (s.source === 'file') W.push(sn + ' "' + String(c2.name || '') + '" was loaded from a file: drop that file on the game view to load it');
|
| 77 |
+
slots[sn] = c2;
|
| 78 |
+
});
|
| 79 |
+
if (E.length) return { ok: false, errors: E, warnings: W, value: null };
|
| 80 |
+
return { ok: true, errors: [], warnings: W, value: make({ engine: o.engine, savedAt: o.savedAt, look: look, config: cfg, scene: slots.scene, character: slots.character }) };
|
| 81 |
+
}
|
| 82 |
+
function parse(text, opts) {
|
| 83 |
+
if (typeof text === 'string' && text.length > 262144) return { ok: false, errors: ['file too large (max 256 KB)'], warnings: [], value: null };
|
| 84 |
+
var o; try { o = JSON.parse(text); } catch (e) { return { ok: false, errors: ['not valid JSON: ' + e.message], warnings: [], value: null }; }
|
| 85 |
+
return validate(o, opts);
|
| 86 |
+
}
|
| 87 |
+
var MODEL = ['glb', 'gltf', 'fbx'], TAKES = ['gltf', 'fbx'], UNSUP = ['obj', 'blend', 'max', 'mb', 'ma', '3ds', 'dae', 'usd', 'usdz', 'stl', 'c4d', 'skp'], EXTRA = ['bin', 'png', 'jpg', 'jpeg', 'webp', 'tga', 'ktx2'];
|
| 88 |
+
function ext(n) { var i = String(n).lastIndexOf('.'); return i < 0 ? '' : String(n).slice(i + 1).toLowerCase(); }
|
| 89 |
+
function planDrop(files, setup, target) {
|
| 90 |
+
files = files || [];
|
| 91 |
+
var r = { setup: -1, scene: { main: -1, extras: [] }, character: { main: -1, extras: [] }, ignored: [], errors: [] };
|
| 92 |
+
var models = [], extras = [];
|
| 93 |
+
files.forEach(function (f, i) {
|
| 94 |
+
var e = ext(f.name);
|
| 95 |
+
if (e === 'json') { if (r.setup < 0) r.setup = i; else r.ignored.push(i); }
|
| 96 |
+
else if (MODEL.indexOf(e) >= 0) models.push(i);
|
| 97 |
+
else if (UNSUP.indexOf(e) >= 0) { r.ignored.push(i); r.errors.push(f.name + ': .' + e + ' is not supported, export FBX or glTF (.glb) instead'); }
|
| 98 |
+
else if (EXTRA.indexOf(e) >= 0) extras.push(i);
|
| 99 |
+
else { r.ignored.push(i); r.errors.push(f.name + ': unknown file type'); }
|
| 100 |
+
});
|
| 101 |
+
var rest = [];
|
| 102 |
+
models.forEach(function (i) {
|
| 103 |
+
var n = String(files[i].name).toLowerCase();
|
| 104 |
+
if (setup && setup.scene && r.scene.main < 0 && n === String(setup.scene.name).toLowerCase()) r.scene.main = i;
|
| 105 |
+
else if (setup && setup.character && r.character.main < 0 && n === String(setup.character.name).toLowerCase()) r.character.main = i;
|
| 106 |
+
else rest.push(i);
|
| 107 |
+
});
|
| 108 |
+
var unassigned = [];
|
| 109 |
+
rest.forEach(function (i) {
|
| 110 |
+
if ((target === 'scene' || target === 'character') && r[target].main < 0) r[target].main = i;
|
| 111 |
+
else if (r.setup < 0 && target == null && r.scene.main < 0) r.scene.main = i;
|
| 112 |
+
else if (r.setup < 0 && target == null && r.character.main < 0) r.character.main = i;
|
| 113 |
+
else unassigned.push(i);
|
| 114 |
+
});
|
| 115 |
+
// error messages for unassigned models are emitted in file order, after the type errors collected above
|
| 116 |
+
unassigned.forEach(function (i) { r.ignored.push(i); r.errors.push(files[i].name + ': dropped with no free slot (drop one model on each half of the game view)'); });
|
| 117 |
+
var takes = ['scene', 'character'].filter(function (s) { var m = r[s].main; return m >= 0 && TAKES.indexOf(ext(files[m].name)) >= 0; }); // [scope change P32-9] registered extensions may take extras
|
| 118 |
+
extras.forEach(function (i) { if (takes.length) takes.forEach(function (s) { r[s].extras.push(i); }); else r.ignored.push(i); });
|
| 119 |
+
r.ignored.sort(function (a, b) { return a - b; });
|
| 120 |
+
return r;
|
| 121 |
+
}
|
| 122 |
+
function registerModelExt(e, o) { // [scope change P32-9]
|
| 123 |
+
e = String(e == null ? '' : e).toLowerCase().replace(/^\./, '');
|
| 124 |
+
if (!/^[a-z0-9]+$/.test(e) || e === 'json' || EXTRA.indexOf(e) >= 0 || MODEL.indexOf(e) >= 0) return false;
|
| 125 |
+
MODEL.push(e); if (UNSUP.indexOf(e) >= 0) UNSUP.splice(UNSUP.indexOf(e), 1); if (o && o.extras) TAKES.push(e); return true;
|
| 126 |
+
}
|
| 127 |
+
root.MuseSetup = { KIND: KIND, VERSION: VERSION, make: make, validate: validate, parse: parse, planDrop: planDrop, registerModelExt: registerModelExt };
|
| 128 |
+
})(typeof window !== 'undefined' ? window : globalThis);
|
game/muse-authored/phase32/harness/ref/texture-budget.ref.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Coach reference (Grok Bot) for the Phase 32 part C spec: engine/check/texture-budget.js. Used only by the harness.
|
| 2 |
+
(function (root) {
|
| 3 |
+
var LIMITS = { uploadMax: 1024, largeWarn: 2048, memWarnMB: 256 };
|
| 4 |
+
function pot(x) { return Math.pow(2, Math.round(Math.log2(Math.max(1, x)))); }
|
| 5 |
+
function isPot(x) { return x > 0 && (x & (x - 1)) === 0; }
|
| 6 |
+
function num(v) { return typeof v === 'number' && v > 0; }
|
| 7 |
+
function analyze(textures, opts) {
|
| 8 |
+
var o = {}; for (var k in LIMITS) o[k] = opts && num(opts[k]) ? opts[k] : LIMITS[k];
|
| 9 |
+
var list = (Array.isArray(textures) ? textures : []).filter(function (t) { return t && typeof t === 'object' && num(t.width) && num(t.height); });
|
| 10 |
+
var dec = 0, up = 0, largest = null, over = [], npot = [], L = null, firstNpot = null;
|
| 11 |
+
list.forEach(function (t) {
|
| 12 |
+
dec += t.width * t.height * 4 / 1048576;
|
| 13 |
+
up += Math.min(o.uploadMax, pot(t.width)) * Math.min(o.uploadMax, pot(t.height)) * 4 * 4 / 3 / 1048576;
|
| 14 |
+
if (!largest || t.width * t.height > largest.width * largest.height) largest = t;
|
| 15 |
+
if (Math.max(t.width, t.height) > o.largeWarn) { over.push(t.name); if (!L || t.width * t.height > L.width * L.height) L = t; }
|
| 16 |
+
if (!isPot(t.width) || !isPot(t.height)) { npot.push(t.name); if (!firstNpot) firstNpot = t; }
|
| 17 |
+
});
|
| 18 |
+
var w = [];
|
| 19 |
+
if (over.length) w.push({ code: 'LARGE_TEXTURES', level: 'warn', msg: over.length + ' texture(s) larger than ' + o.largeWarn + ' px (largest: ' + L.name + ' ' + L.width + 'x' + L.height + '): the engine shrinks every texture to ' + o.uploadMax + ' px, so the extra size only costs memory and loading time.', fix: 'Export 1024 or 2048 px maps (3ds Max: Render to Texture / resize the bitmaps; Maya: resize the file textures) before exporting.' });
|
| 20 |
+
if (dec > o.memWarnMB) w.push({ code: 'TEXTURE_MEMORY', level: 'warn', msg: 'Textures need about ' + Math.round(dec) + ' MB of memory while loading (' + list.length + ' images).', fix: 'Use fewer or smaller textures, or share one atlas between parts.' });
|
| 21 |
+
if (npot.length) w.push({ code: 'NPOT_TEXTURES', level: 'info', msg: npot.length + ' texture(s) are not a power of two (e.g. ' + firstNpot.name + ' ' + firstNpot.width + 'x' + firstNpot.height + '): they are resized on upload and may blur slightly.', fix: 'Use power-of-two sizes such as 512, 1024 or 2048.' });
|
| 22 |
+
return { count: list.length, totalDecodedMB: Math.round(dec * 10) / 10, totalUploadMB: Math.round(up * 10) / 10,
|
| 23 |
+
largest: largest ? { name: largest.name, width: largest.width, height: largest.height } : null, over: over, npot: npot, warnings: w };
|
| 24 |
+
}
|
| 25 |
+
root.MuseTextureBudget = { analyze: analyze, LIMITS: LIMITS };
|
| 26 |
+
if (typeof module !== 'undefined' && module.exports) module.exports = root.MuseTextureBudget;
|
| 27 |
+
})(typeof window !== 'undefined' ? window : globalThis);
|