lecodes-assets 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +449 -326
  2. package/dist/cli.js +2161 -198
  3. package/dist/index.js +2268 -198
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -960,15 +960,18 @@ var init_toDocument = __esm(() => {
960
960
  var exports_rig = {};
961
961
  __export(exports_rig, {
962
962
  worldOf: () => worldOf,
963
+ sameSkeleton: () => sameSkeleton,
963
964
  rotateRig: () => rotateRig,
964
965
  rigFromFbx: () => rigFromFbx,
965
966
  rigFromDocument: () => rigFromDocument,
967
+ rebindClips: () => rebindClips,
966
968
  qslerp: () => qslerp,
967
969
  qrot: () => qrot,
968
970
  qnorm: () => qnorm,
969
971
  qmul: () => qmul,
970
972
  qinv: () => qinv,
971
973
  loadRig: () => loadRig,
974
+ hasSkin: () => hasSkin,
972
975
  checkUniformScale: () => checkUniformScale,
973
976
  bindVsRest: () => bindVsRest
974
977
  });
@@ -993,7 +996,7 @@ var qmul = (a, b) => [
993
996
  const th = Math.acos(d), s = Math.sin(th);
994
997
  const wa = Math.sin((1 - t) * th) / s, wb = Math.sin(t * th) / s;
995
998
  return [a[0] * wa + e[0] * wb, a[1] * wa + e[1] * wb, a[2] * wa + e[2] * wb, a[3] * wa + e[3] * wb];
996
- }, qrot = (q, v) => {
999
+ }, sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], scl = (a, k) => [a[0] * k, a[1] * k, a[2] * k], len = (v) => Math.hypot(v[0], v[1], v[2]), qrot = (q, v) => {
997
1000
  const [x, y, z, w] = q;
998
1001
  const tx = 2 * (y * v[2] - z * v[1]), ty = 2 * (z * v[0] - x * v[2]), tz = 2 * (x * v[1] - y * v[0]);
999
1002
  return [
@@ -1086,16 +1089,31 @@ var qmul = (a, b) => [
1086
1089
  }
1087
1090
  const k = Math.sqrt(1 + i - a - e) * 2;
1088
1091
  return qnorm([(g + c) / k, (h + f) / k, k / 4, (b - d) / k]);
1089
- }, buildBind = (parents, order, rest, ibmByNode) => {
1092
+ }, buildBind = (parents, order, rest, ibmByNode, spaceByNode) => {
1090
1093
  const world = new Array(parents.length);
1091
1094
  const fromSkin = new Array(parents.length).fill(false);
1095
+ const restWorld = new Array(parents.length);
1092
1096
  for (const i of order) {
1093
- const ibm = ibmByNode.get(i);
1094
- if (ibm) {
1095
- world[i] = matInvRigid(ibm);
1096
- fromSkin[i] = true;
1097
- }
1097
+ const local2 = matCompose(rest.t[i], rest.r[i], rest.s[i]);
1098
+ restWorld[i] = parents[i] >= 0 ? matMul(restWorld[parents[i]], local2) : local2;
1098
1099
  }
1100
+ let rawDist = 0, foldedDist = 0;
1101
+ const raw = new Array(parents.length), folded = new Array(parents.length);
1102
+ for (const i of order) {
1103
+ const ibm = ibmByNode.get(i);
1104
+ if (!ibm)
1105
+ continue;
1106
+ raw[i] = matInvRigid(ibm);
1107
+ const space = spaceByNode.get(i);
1108
+ folded[i] = space ? matMul(space, raw[i]) : raw[i];
1109
+ fromSkin[i] = true;
1110
+ rawDist += Math.hypot(raw[i][12] - restWorld[i][12], raw[i][13] - restWorld[i][13], raw[i][14] - restWorld[i][14]);
1111
+ foldedDist += Math.hypot(folded[i][12] - restWorld[i][12], folded[i][13] - restWorld[i][13], folded[i][14] - restWorld[i][14]);
1112
+ }
1113
+ const useFolded = foldedDist <= rawDist;
1114
+ for (const i of order)
1115
+ if (fromSkin[i])
1116
+ world[i] = useFolded ? folded[i] : raw[i];
1099
1117
  const accum = new Array(parents.length).fill(1);
1100
1118
  for (const i of order)
1101
1119
  accum[i] = (parents[i] >= 0 ? accum[parents[i]] : 1) * rest.s[i][0];
@@ -1139,9 +1157,19 @@ var qmul = (a, b) => [
1139
1157
  local.r[i] = qnorm(qmul(qconj(worldRot[p]), worldRot[i]));
1140
1158
  }
1141
1159
  return { worldRot, worldPos, fromSkin, local, scale: Math.abs(scale - 1) > 0.01 ? scale : 1 };
1142
- }, finish = (label, names, parents, rest, makeClips, ibmByNode) => {
1160
+ }, restWorldOf = (parents, rest, node) => {
1161
+ const chain = [];
1162
+ for (let a = node;a >= 0; a = parents[a])
1163
+ chain.unshift(a);
1164
+ let m = null;
1165
+ for (const i of chain) {
1166
+ const local = matCompose(rest.t[i], rest.r[i], rest.s[i]);
1167
+ m = m ? matMul(m, local) : local;
1168
+ }
1169
+ return m;
1170
+ }, finish = (label, names, parents, rest, makeClips, ibmByNode, spaceByNode = new Map) => {
1143
1171
  const order = buildOrder(parents);
1144
- const bind = buildBind(parents, order, rest, ibmByNode);
1172
+ const bind = buildBind(parents, order, rest, ibmByNode, spaceByNode);
1145
1173
  const rig = {
1146
1174
  label,
1147
1175
  names,
@@ -1205,13 +1233,19 @@ var qmul = (a, b) => [
1205
1233
  s: scene.nodes.map((n) => [...n.s])
1206
1234
  };
1207
1235
  const ibmByNode = new Map;
1208
- for (const skin of scene.skins) {
1236
+ const spaceByNode = new Map;
1237
+ scene.skins.forEach((skin, si) => {
1209
1238
  const ibm = f32(scene, skin.ibm, skin.joints.length * 16);
1239
+ const meshNode = scene.nodes.findIndex((n) => n.skin === si);
1240
+ const space = meshNode >= 0 ? restWorldOf(parents, rest, meshNode) : null;
1210
1241
  skin.joints.forEach((node, j) => {
1211
- if (!ibmByNode.has(node))
1212
- ibmByNode.set(node, Array.from(ibm.subarray(j * 16, j * 16 + 16)));
1242
+ if (ibmByNode.has(node))
1243
+ return;
1244
+ ibmByNode.set(node, Array.from(ibm.subarray(j * 16, j * 16 + 16)));
1245
+ if (space)
1246
+ spaceByNode.set(node, space);
1213
1247
  });
1214
- }
1248
+ });
1215
1249
  const makeClips = (base) => scene.animations.filter((a) => a.tracks.length > 0).map((anim) => {
1216
1250
  const tracks = anim.tracks.map((tr) => {
1217
1251
  const n = tr.path === "rotation" ? 4 : 3;
@@ -1235,7 +1269,7 @@ var qmul = (a, b) => [
1235
1269
  }
1236
1270
  };
1237
1271
  });
1238
- return finish(path, names, parents, rest, makeClips, ibmByNode);
1272
+ return finish(path, names, parents, rest, makeClips, ibmByNode, spaceByNode);
1239
1273
  }, clipsFromDocument = (doc, nodes, base) => {
1240
1274
  const idx = new Map(nodes.map((n, i) => [n, i]));
1241
1275
  return doc.getRoot().listAnimations().map((anim) => {
@@ -1285,18 +1319,127 @@ var qmul = (a, b) => [
1285
1319
  s: nodes.map((n) => [...n.getScale()])
1286
1320
  };
1287
1321
  const ibmByNode = new Map;
1322
+ const spaceByNode = new Map;
1288
1323
  for (const skin of doc.getRoot().listSkins()) {
1289
1324
  const acc = skin.getInverseBindMatrices();
1290
1325
  if (!acc)
1291
1326
  continue;
1327
+ const meshNode = nodes.findIndex((n) => n.getSkin() === skin);
1328
+ const space = meshNode >= 0 ? restWorldOf(parents, rest, meshNode) : null;
1292
1329
  skin.listJoints().forEach((joint, j) => {
1293
1330
  const i = idx.get(joint);
1294
- if (i !== undefined && !ibmByNode.has(i))
1295
- ibmByNode.set(i, acc.getElement(j, []));
1331
+ if (i === undefined || ibmByNode.has(i))
1332
+ return;
1333
+ ibmByNode.set(i, acc.getElement(j, []));
1334
+ if (space)
1335
+ spaceByNode.set(i, space);
1296
1336
  });
1297
1337
  }
1298
- return { ...finish(label, names, parents, rest, (base) => clipsFromDocument(doc, nodes, base), ibmByNode), doc, nodes };
1299
- }, loadRig = async (path) => rigFromFbx(path, new Uint8Array(readFileSync3(path))), rotateRig = (rig, q) => {
1338
+ return { ...finish(label, names, parents, rest, (base) => clipsFromDocument(doc, nodes, base), ibmByNode, spaceByNode), doc, nodes };
1339
+ }, loadRig = async (path) => rigFromFbx(path, new Uint8Array(readFileSync3(path))), hasSkin = (rig) => rig.bind.fromSkin.some(Boolean), shortBoneName = (n) => n.slice(n.search(/[^:|]*$/)), sameSkeleton = (rig, source) => {
1340
+ const short = new Map;
1341
+ rig.names.forEach((n, i) => {
1342
+ const s = shortBoneName(n);
1343
+ if (!short.has(s))
1344
+ short.set(s, i);
1345
+ });
1346
+ const map = source.names.map((n) => rig.index.get(n) ?? short.get(shortBoneName(n)) ?? -1);
1347
+ const share = map.filter((j) => j >= 0).length / Math.max(1, source.names.length);
1348
+ let worstDeg = 0, worstBone = "";
1349
+ source.names.forEach((_, i) => {
1350
+ const j = map[i];
1351
+ if (j < 0)
1352
+ return;
1353
+ const ps = source.parents[i], pt = rig.parents[j];
1354
+ if (ps < 0 || pt < 0 || map[ps] !== pt)
1355
+ return;
1356
+ const a = rig.rest.t[j], b = source.rest.t[i];
1357
+ const la = Math.hypot(...a), lb = Math.hypot(...b);
1358
+ if (la < 0.05 || lb < 0.05)
1359
+ return;
1360
+ const deg = Math.acos(Math.max(-1, Math.min(1, (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) / (la * lb)))) * 180 / Math.PI;
1361
+ if (deg > worstDeg) {
1362
+ worstDeg = deg;
1363
+ worstBone = source.names[i];
1364
+ }
1365
+ });
1366
+ return { same: share >= 0.8 && worstDeg < 45, share, worstDeg, worstBone };
1367
+ }, rebindClips = (rig, source) => {
1368
+ const short = new Map;
1369
+ rig.names.forEach((n, i) => {
1370
+ const s = shortBoneName(n);
1371
+ if (!short.has(s))
1372
+ short.set(s, i);
1373
+ });
1374
+ const map = source.names.map((n) => rig.index.get(n) ?? short.get(shortBoneName(n)) ?? -1);
1375
+ const unbound = source.names.filter((_, i) => map[i] < 0);
1376
+ const bound = map.filter((j) => j >= 0).length;
1377
+ const base = rig.bind.local, srcRest = source.rest;
1378
+ const animated = source.names.map(() => false);
1379
+ for (const c of source.clips)
1380
+ for (let k = 0;k <= 8; k++) {
1381
+ const p = c.sample(k / 8 * c.duration);
1382
+ p.t.forEach((t, i) => {
1383
+ if (!animated[i] && len(sub(t, srcRest.t[i])) > 0.000001)
1384
+ animated[i] = true;
1385
+ });
1386
+ }
1387
+ const sampleAt = (c, time, scale2) => {
1388
+ const p = c.sample(time);
1389
+ const out = { t: base.t.map((v) => [...v]), r: base.r.map((q) => [...q]), s: base.s.map((v) => [...v]) };
1390
+ map.forEach((j, i) => {
1391
+ if (j < 0)
1392
+ return;
1393
+ out.r[j] = p.r[i];
1394
+ if (animated[i])
1395
+ out.t[j] = scl(p.t[i], scale2);
1396
+ });
1397
+ return out;
1398
+ };
1399
+ let sameLengths = true, num = 0, den = 0;
1400
+ map.forEach((j, i) => {
1401
+ if (j < 0 || animated[i] || source.parents[i] < 0 || rig.parents[j] < 0)
1402
+ return;
1403
+ const a = len(base.t[j]), b = len(srcRest.t[i]);
1404
+ num += a;
1405
+ den += b;
1406
+ if (Math.abs(a - b) > 0.01 * Math.max(a, b, 0.01))
1407
+ sameLengths = false;
1408
+ });
1409
+ let scale = 1;
1410
+ if (!sameLengths && source.clips.length) {
1411
+ const skinned = rig.bind.fromSkin.some(Boolean) ? rig.bind.worldPos.filter((_, i) => rig.bind.fromSkin[i]) : rig.bind.worldPos;
1412
+ const ground = Math.min(...skinned.map((p) => p[1]));
1413
+ const lowest = (s) => {
1414
+ let lo = Infinity;
1415
+ for (const c of source.clips)
1416
+ for (let k = 0;k < 16; k++) {
1417
+ const w = worldOf(rig, sampleAt(c, k / 15 * c.duration, s));
1418
+ for (let i = 0;i < w.pos.length; i++)
1419
+ if (rig.bind.fromSkin[i] || !rig.bind.fromSkin.some(Boolean))
1420
+ lo = Math.min(lo, w.pos[i][1]);
1421
+ }
1422
+ return lo;
1423
+ };
1424
+ let a = 0.5, b = 2, fa = lowest(a) - ground, fb = lowest(b) - ground;
1425
+ if (fa < 0 && fb > 0) {
1426
+ for (let it = 0;it < 24; it++) {
1427
+ const m = (a + b) / 2, fm = lowest(m) - ground;
1428
+ if (fm < 0) {
1429
+ a = m;
1430
+ fa = fm;
1431
+ } else {
1432
+ b = m;
1433
+ fb = fm;
1434
+ }
1435
+ }
1436
+ scale = (a + b) / 2;
1437
+ } else
1438
+ scale = den > 0 && num > 0 ? num / den : 1;
1439
+ }
1440
+ const clips = source.clips.map((c) => ({ name: c.name, duration: c.duration, sample: (time) => sampleAt(c, time, scale) }));
1441
+ return { rig: { ...rig, clips }, bound, total: source.names.length, unbound, scale };
1442
+ }, rotateRig = (rig, q) => {
1300
1443
  const roots = rig.parents.map((p, i) => p < 0 ? i : -1).filter((i) => i >= 0);
1301
1444
  const turn = (pose) => {
1302
1445
  const out2 = { t: pose.t.map((v) => [...v]), r: pose.r.map((v) => [...v]), s: pose.s };
@@ -1339,9 +1482,9 @@ var u322 = (b, o) => (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>
1339
1482
  let palette = null;
1340
1483
  let trns = null;
1341
1484
  while (o + 8 <= b.length) {
1342
- const len = u322(b, o);
1485
+ const len2 = u322(b, o);
1343
1486
  const type = String.fromCharCode(b[o + 4], b[o + 5], b[o + 6], b[o + 7]);
1344
- const data = b.subarray(o + 8, o + 8 + len);
1487
+ const data = b.subarray(o + 8, o + 8 + len2);
1345
1488
  if (type === "IHDR") {
1346
1489
  width = u322(data, 0);
1347
1490
  height = u322(data, 4);
@@ -1356,7 +1499,7 @@ var u322 = (b, o) => (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>
1356
1499
  idat.push(data);
1357
1500
  else if (type === "IEND")
1358
1501
  break;
1359
- o += 12 + len;
1502
+ o += 12 + len2;
1360
1503
  }
1361
1504
  if (!width || !height || interlace !== 0 || depth !== 8 && depth !== 16)
1362
1505
  return null;
@@ -4047,12 +4190,12 @@ var init_lightmapUv = __esm(() => {
4047
4190
  var num2 = 0;
4048
4191
  for (var i2 = 0;i2 < iovcnt; i2++) {
4049
4192
  var ptr = HEAPU32[iov >> 2];
4050
- var len = HEAPU32[iov + 4 >> 2];
4193
+ var len2 = HEAPU32[iov + 4 >> 2];
4051
4194
  iov += 8;
4052
- for (var j = 0;j < len; j++) {
4195
+ for (var j = 0;j < len2; j++) {
4053
4196
  printChar(fd, HEAPU8[ptr + j]);
4054
4197
  }
4055
- num2 += len;
4198
+ num2 += len2;
4056
4199
  }
4057
4200
  HEAPU32[pnum >> 2] = num2;
4058
4201
  return 0;
@@ -4312,6 +4455,193 @@ var init_lightmapUv2 = __esm(() => {
4312
4455
  init_lightmapUv();
4313
4456
  });
4314
4457
 
4458
+ // src/doctor/lod.ts
4459
+ import { simplifyPrimitive, weldPrimitive } from "@gltf-transform/functions";
4460
+ import { MeshoptSimplifier } from "meshoptimizer";
4461
+ function planLod(doc, opts) {
4462
+ const ratios = (opts.lod ?? []).filter((r) => r > 0 && r < 1).sort((a, b) => b - a);
4463
+ if (!ratios.length)
4464
+ return null;
4465
+ const nodes = [];
4466
+ let existing = 0;
4467
+ for (const node of doc.getRoot().listNodes()) {
4468
+ const mesh = node.getMesh();
4469
+ if (!mesh)
4470
+ continue;
4471
+ if (LOD_NAME.test(node.getName())) {
4472
+ existing++;
4473
+ continue;
4474
+ }
4475
+ const tris = meshTris(mesh);
4476
+ if (tris < LOD_MIN_TRIS)
4477
+ continue;
4478
+ nodes.push({ node, name: node.getName() || mesh.getName() || `mesh${nodes.length}`, tris, skinned: !!node.getSkin() });
4479
+ }
4480
+ return { ratios, bones: !!opts.lodBones, nodes, existing };
4481
+ }
4482
+ async function applyLod(doc, plan) {
4483
+ await MeshoptSimplifier.ready;
4484
+ const result = { levels: plan.ratios.length, nodes: 0, tris: [], bones: [], skipped: [] };
4485
+ for (const entry of plan.nodes) {
4486
+ const { node } = entry;
4487
+ const baseMesh = node.getMesh();
4488
+ const parent = node.getParentNode();
4489
+ const scenes = parent ? [] : doc.getRoot().listScenes().filter((s) => s.listChildren().includes(node));
4490
+ const baseExtras = node.getExtras() ?? {};
4491
+ const baseLecodes = baseExtras.lecodes ?? {};
4492
+ node.setExtras({ ...baseExtras, lecodes: { ...baseLecodes, lod: { level: 0, levels: plan.ratios.length + 1, tris: entry.tris } } });
4493
+ const levels = [entry.tris];
4494
+ for (let i = 0;i < plan.ratios.length; i++) {
4495
+ const level = i + 1;
4496
+ const ratio = plan.ratios[i];
4497
+ const mesh = doc.createMesh(`${baseMesh.getName() || entry.name}_LOD${level}`);
4498
+ let tris = 0;
4499
+ for (const prim of baseMesh.listPrimitives()) {
4500
+ if (prim.getMode() !== 4)
4501
+ continue;
4502
+ let copy = clonePrimitive(doc, prim);
4503
+ weldPrimitive(copy);
4504
+ copy = simplifyPrimitive(copy, { simplifier: MeshoptSimplifier, ratio, error: 0.02, lockBorder: false });
4505
+ tris += primTris(copy);
4506
+ mesh.addPrimitive(copy);
4507
+ }
4508
+ if (tris > levels[levels.length - 1] * 0.85) {
4509
+ for (const p of mesh.listPrimitives()) {
4510
+ for (const sem of p.listSemantics())
4511
+ p.getAttribute(sem)?.dispose();
4512
+ p.getIndices()?.dispose();
4513
+ p.dispose();
4514
+ }
4515
+ mesh.dispose();
4516
+ result.skipped.push({ name: entry.name, level, tris });
4517
+ break;
4518
+ }
4519
+ const lodNode = doc.createNode(`${node.getName() || entry.name}_LOD${level}`).setMesh(mesh).setTranslation(node.getTranslation()).setRotation(node.getRotation()).setScale(node.getScale());
4520
+ if (node.getSkin())
4521
+ lodNode.setSkin(node.getSkin());
4522
+ lodNode.setExtras({ lecodes: { lod: { level, ratio, tris } } });
4523
+ if (parent)
4524
+ parent.addChild(lodNode);
4525
+ else
4526
+ for (const s of scenes)
4527
+ s.addChild(lodNode);
4528
+ levels.push(tris);
4529
+ if (plan.bones && i === plan.ratios.length - 1 && node.getSkin()) {
4530
+ const r = reduceSkinForLod(doc, lodNode);
4531
+ if (r)
4532
+ result.bones.push({ name: entry.name, ...r });
4533
+ }
4534
+ }
4535
+ result.nodes++;
4536
+ result.tris.push({ name: entry.name, levels });
4537
+ }
4538
+ return result;
4539
+ }
4540
+ var LOD_MIN_TRIS = 2000, LOD_JOINT_MASK, LOD_NAME, shortName3 = (s) => {
4541
+ const p = Math.max(s.lastIndexOf(":"), s.lastIndexOf("|"));
4542
+ return p < 0 ? s : s.slice(p + 1);
4543
+ }, primTris = (p) => {
4544
+ if (p.getMode() !== 4)
4545
+ return 0;
4546
+ const idx = p.getIndices();
4547
+ const n = idx ? idx.getCount() : p.getAttribute("POSITION")?.getCount() ?? 0;
4548
+ return Math.floor(n / 3);
4549
+ }, meshTris = (m) => m.listPrimitives().reduce((s, p) => s + primTris(p), 0), clonePrimitive = (doc, prim) => {
4550
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer();
4551
+ const cloneAcc = (a) => doc.createAccessor(a.getName(), buffer).setType(a.getType()).setNormalized(a.getNormalized()).setArray(a.getArray().slice());
4552
+ const out = doc.createPrimitive().setMode(prim.getMode()).setMaterial(prim.getMaterial());
4553
+ for (const sem of prim.listSemantics())
4554
+ out.setAttribute(sem, cloneAcc(prim.getAttribute(sem)));
4555
+ const idx = prim.getIndices();
4556
+ if (idx)
4557
+ out.setIndices(cloneAcc(idx));
4558
+ return out;
4559
+ }, reduceSkinForLod = (doc, lodNode) => {
4560
+ const skin = lodNode.getSkin();
4561
+ const mesh = lodNode.getMesh();
4562
+ if (!skin || !mesh)
4563
+ return null;
4564
+ const joints = skin.listJoints();
4565
+ const removed = new Set;
4566
+ joints.forEach((j, i) => {
4567
+ if (LOD_JOINT_MASK.test(shortName3(j.getName())))
4568
+ removed.add(i);
4569
+ });
4570
+ if (!removed.size)
4571
+ return null;
4572
+ const idxOf = new Map(joints.map((j, i) => [j, i]));
4573
+ const remap = joints.map((j, i) => {
4574
+ if (!removed.has(i))
4575
+ return i;
4576
+ for (let p = j.getParentNode();p; p = p.getParentNode()) {
4577
+ const k = idxOf.get(p);
4578
+ if (k !== undefined && !removed.has(k))
4579
+ return k;
4580
+ }
4581
+ return i;
4582
+ });
4583
+ const kept = [...new Set(remap)].sort((a, b) => a - b);
4584
+ const newIndex = new Map(kept.map((old, k) => [old, k]));
4585
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer();
4586
+ const newSkin = doc.createSkin(`${skin.getName() || "skin"}_LOD`);
4587
+ if (skin.getSkeleton())
4588
+ newSkin.setSkeleton(skin.getSkeleton());
4589
+ for (const i of kept)
4590
+ newSkin.addJoint(joints[i]);
4591
+ const ibm = skin.getInverseBindMatrices();
4592
+ if (ibm) {
4593
+ const src = ibm.getArray();
4594
+ const out = new Float32Array(kept.length * 16);
4595
+ kept.forEach((i, k) => out.set(src.subarray(i * 16, i * 16 + 16), k * 16));
4596
+ newSkin.setInverseBindMatrices(doc.createAccessor(`${ibm.getName() || "ibm"}_LOD`, buffer).setType("MAT4").setArray(out));
4597
+ }
4598
+ const jv = [], wv = [];
4599
+ for (const prim of mesh.listPrimitives()) {
4600
+ const sets = [];
4601
+ for (let s = 0;; s++) {
4602
+ const j = prim.getAttribute(`JOINTS_${s}`), w = prim.getAttribute(`WEIGHTS_${s}`);
4603
+ if (!j || !w)
4604
+ break;
4605
+ sets.push({ j, w });
4606
+ }
4607
+ if (!sets.length)
4608
+ continue;
4609
+ const count = sets[0].j.getCount();
4610
+ const folded = new Map;
4611
+ for (let v = 0;v < count; v++) {
4612
+ folded.clear();
4613
+ for (const { j, w } of sets) {
4614
+ j.getElement(v, jv);
4615
+ w.getElement(v, wv);
4616
+ for (let k = 0;k < 4; k++) {
4617
+ const weight = wv[k] ?? 0;
4618
+ if (weight <= 0)
4619
+ continue;
4620
+ const target = newIndex.get(remap[jv[k]] ?? jv[k]) ?? 0;
4621
+ folded.set(target, (folded.get(target) ?? 0) + weight);
4622
+ }
4623
+ }
4624
+ const pairs = [...folded].sort((a, b) => b[1] - a[1]);
4625
+ let n = 0;
4626
+ for (const { j, w } of sets) {
4627
+ for (let k = 0;k < 4; k++) {
4628
+ const p = pairs[n++];
4629
+ jv[k] = p ? p[0] : 0;
4630
+ wv[k] = p ? p[1] : 0;
4631
+ }
4632
+ j.setElement(v, jv);
4633
+ w.setElement(v, wv);
4634
+ }
4635
+ }
4636
+ }
4637
+ lodNode.setSkin(newSkin);
4638
+ return { before: joints.length, after: kept.length };
4639
+ };
4640
+ var init_lod = __esm(() => {
4641
+ LOD_JOINT_MASK = /finger|thumb|index|middle|ring|pinky|toe|twist|roll|leaf|_end$/i;
4642
+ LOD_NAME = /_LOD\d+$/;
4643
+ });
4644
+
4315
4645
  // src/doctor/doctor.ts
4316
4646
  var exports_doctor = {};
4317
4647
  __export(exports_doctor, {
@@ -4359,7 +4689,8 @@ async function analyzeDoc(doc, opts = {}) {
4359
4689
  const scaleInfo = measureScale(doc);
4360
4690
  const scalePlan = planScale(scaleInfo, opts);
4361
4691
  const dupNames = duplicateNodeNames(doc);
4362
- return { skinPlans, texPlans, ktx2Plans, metallicWarn, shadingPlan, lightmapPlan, scaleInfo, scalePlan, conflicts, target, dupNames };
4692
+ const lodPlan = planLod(doc, opts);
4693
+ return { skinPlans, texPlans, ktx2Plans, metallicWarn, shadingPlan, lightmapPlan, scaleInfo, scalePlan, lodPlan, conflicts, target, dupNames };
4363
4694
  }
4364
4695
  async function dumpTextures(doc, dir, log = console.log) {
4365
4696
  if (!existsSync8(dir))
@@ -4511,6 +4842,7 @@ async function applyPlan(doc, plan, opts = {}) {
4511
4842
  }
4512
4843
  const shading = applyShading(doc, plan.shadingPlan);
4513
4844
  const lightmapUv = plan.lightmapPlan.enabled ? await applyLightmapUv(doc, plan.lightmapPlan) : null;
4845
+ const lod = plan.lodPlan ? await applyLod(doc, plan.lodPlan) : null;
4514
4846
  let ktx2Encoded = 0;
4515
4847
  if (opts.ktx2) {
4516
4848
  const toktx = opts.toktx ?? findToktx();
@@ -4534,7 +4866,7 @@ async function applyPlan(doc, plan, opts = {}) {
4534
4866
  ext.dispose();
4535
4867
  }
4536
4868
  }
4537
- return { metallicZeroed, ktx2Encoded, scaled: plan.scalePlan?.factor ?? 1, unlit: shading.unlit, emissiveCleared: shading.emissiveCleared, lightmapUv };
4869
+ return { metallicZeroed, ktx2Encoded, scaled: plan.scalePlan?.factor ?? 1, unlit: shading.unlit, emissiveCleared: shading.emissiveCleared, lightmapUv, lod };
4538
4870
  }
4539
4871
  function outputFor(input, oVal, batch, ktx2) {
4540
4872
  const derived = input.replace(/\.glb$/i, ktx2 ? ".ktx2.glb" : ".fixed.glb");
@@ -4557,6 +4889,7 @@ var init_doctor = __esm(() => {
4557
4889
  init_normalizeScale();
4558
4890
  init_shading();
4559
4891
  init_lightmapUv2();
4892
+ init_lod();
4560
4893
  init_mergeSkins();
4561
4894
  init_normalizeScale();
4562
4895
  EXT_BY_MIME = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/ktx2": "ktx2" };
@@ -5002,7 +5335,26 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5002
5335
  if (rootBone !== null && rootIdx < 0)
5003
5336
  warn(`target has no root bone "${rootBone}" — no root motion`);
5004
5337
  const donorRoot = rootIdx >= 0 ? byTarget.get(rootIdx) : undefined;
5338
+ const rootTranslateOnly = rootIdx >= 0 && donorRoot !== undefined && pairs.some((p) => p.d !== rootIdx && p.s === donorRoot);
5005
5339
  const legs = opts.ik === "none" ? [] : detectLegs(target, turned, byTarget, target.bind.worldPos, opts.legs, warn);
5340
+ const hum = opts.humanoid;
5341
+ const humIdx = (av, k) => {
5342
+ const n = av.bones[k];
5343
+ return n === undefined ? undefined : target.index.get(n);
5344
+ };
5345
+ const hipsIdx = hum ? humIdx(hum.target, "Hips") ?? -1 : -1;
5346
+ if (hum)
5347
+ for (const leg of legs)
5348
+ for (const s of ["Left", "Right"]) {
5349
+ const foot = humIdx(hum.target, `${s}Foot`), toes = humIdx(hum.target, `${s}Toes`);
5350
+ if (leg.effector === foot && toes !== undefined && byTarget.has(toes) && target.parents[toes] === foot) {
5351
+ leg.contact = toes;
5352
+ leg.donorContact = byTarget.get(toes);
5353
+ const tf = len2(sub2(target.bind.worldPos[toes], target.bind.worldPos[foot])), df = len2(sub2(turned.bind.worldPos[leg.donorContact], turned.bind.worldPos[leg.donorEffector]));
5354
+ leg.contactRatio = tf > 0 && df > 0 ? tf / df : 1;
5355
+ }
5356
+ }
5357
+ const donorReach = (leg, pos) => add(scl2(sub2(pos[leg.donorEffector], pos[leg.donorHip]), leg.ratio), scl2(sub2(pos[leg.donorContact], pos[leg.donorEffector]), leg.contactRatio));
5006
5358
  const wanted = opts.clips?.length ? new Set(opts.clips) : null;
5007
5359
  const clips = turned.clips.filter((c) => !wanted || wanted.has(c.name));
5008
5360
  let donorGround = 0;
@@ -5014,7 +5366,7 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5014
5366
  for (let f = 0;f < frames; f++) {
5015
5367
  const dw = worldOf(turned, clip.sample(f / (frames - 1) * clip.duration));
5016
5368
  for (const l of legs)
5017
- l.baseDonor = Math.min(l.baseDonor, dw.pos[l.donorEffector][1]);
5369
+ l.baseDonor = Math.min(l.baseDonor, dw.pos[l.donorContact][1]);
5018
5370
  }
5019
5371
  }
5020
5372
  donorGround = Math.min(...legs.map((l) => l.baseDonor));
@@ -5039,8 +5391,8 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5039
5391
  })());
5040
5392
  if (kid === undefined || !byTarget.has(kid) || !byTarget.has(i))
5041
5393
  continue;
5042
- const ut = unit(sub(target.bind.worldPos[kid], target.bind.worldPos[i]));
5043
- const us = unit(sub(turned.bind.worldPos[byTarget.get(kid)], turned.bind.worldPos[byTarget.get(i)]));
5394
+ const ut = unit(sub2(target.bind.worldPos[kid], target.bind.worldPos[i]));
5395
+ const us = unit(sub2(turned.bind.worldPos[byTarget.get(kid)], turned.bind.worldPos[byTarget.get(i)]));
5044
5396
  if (!ut || !us)
5045
5397
  continue;
5046
5398
  legDisagreement += Math.acos(Math.max(-1, Math.min(1, ut[0] * us[0] + ut[1] * us[1] + ut[2] * us[2]))) * DEG;
@@ -5049,15 +5401,54 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5049
5401
  if (n)
5050
5402
  legDisagreement /= n;
5051
5403
  }
5052
- const donorRefMode = opts.donorRef ?? (legs.length && legDisagreement > 12 ? "auto" : "bind");
5404
+ const donorRefMode = opts.donorRef ?? (opts.humanoid ? "bind" : legs.length && legDisagreement > 12 ? "auto" : "bind");
5053
5405
  const { rig: donor, from: donorRefFrom } = donorReference(turned, donorRefMode, fps);
5054
5406
  for (const l of legs)
5055
- l.baseDonor = Math.min(l.baseDonor, donor.bind.worldPos[l.donorEffector][1]);
5407
+ l.baseDonor = opts.humanoid ? donor.bind.worldPos[l.donorContact][1] : Math.min(l.baseDonor, donor.bind.worldPos[l.donorContact][1]);
5056
5408
  if (legs.length)
5057
5409
  donorGround = Math.min(...legs.map((l) => l.baseDonor));
5058
5410
  for (const l of legs)
5059
- l.stanceDonor = sub(donor.bind.worldPos[l.donorEffector], donor.bind.worldPos[l.donorHip]);
5411
+ l.stanceDonor = donorReach(l, donor.bind.worldPos);
5060
5412
  const policies = { ...opts.alignTo ?? {}, ...opts.alignBones ?? {} };
5413
+ const forced = new Set;
5414
+ if (hum?.target.transfer?.align)
5415
+ for (const [k, v] of Object.entries(hum.target.transfer.align)) {
5416
+ const n = hum.target.bones[k];
5417
+ if (!n || n in policies)
5418
+ continue;
5419
+ const i = target.index.get(n);
5420
+ if (i === undefined || !byTarget.has(i))
5421
+ continue;
5422
+ if (v === "keep")
5423
+ policies[n] = "keep";
5424
+ else
5425
+ forced.add(i);
5426
+ }
5427
+ if (hum) {
5428
+ const T = hum.target;
5429
+ const set = (k, pol) => {
5430
+ const n = T.bones[k];
5431
+ if (n !== undefined && !(n in policies) && target.index.has(n) && byTarget.has(target.index.get(n)))
5432
+ policies[n] = pol;
5433
+ };
5434
+ for (const k of ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"])
5435
+ set(k, "keep");
5436
+ const refTo = (k, ...tos) => {
5437
+ const names = tos.map((t) => T.bones[t]).filter((n) => !!n);
5438
+ set(k, names.length ? names : "direction");
5439
+ };
5440
+ for (const s of ["Left", "Right"]) {
5441
+ refTo(`${s}Shoulder`, `${s}UpperArm`);
5442
+ refTo(`${s}UpperArm`, `${s}LowerArm`);
5443
+ refTo(`${s}LowerArm`, `${s}Hand`);
5444
+ refTo(`${s}Hand`, `${s} Index Proximal`, `${s} Middle Proximal`, `${s} Ring Proximal`, `${s} Little Proximal`);
5445
+ for (const f of ["Thumb", "Index", "Middle", "Ring", "Little"]) {
5446
+ refTo(`${s} ${f} Proximal`, `${s} ${f} Intermediate`);
5447
+ refTo(`${s} ${f} Intermediate`, `${s} ${f} Distal`);
5448
+ set(`${s} ${f} Distal`, "direction");
5449
+ }
5450
+ }
5451
+ }
5061
5452
  let stance = legs.length > 0;
5062
5453
  if (legs.length) {
5063
5454
  let lo = Infinity, hi = -Infinity;
@@ -5067,10 +5458,10 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5067
5458
  hi = Math.max(hi, target.bind.worldPos[i][1]);
5068
5459
  }
5069
5460
  const height = hi - lo || 1;
5070
- const footYs = legs.map((l) => target.bind.worldPos[l.effector][1]);
5461
+ const footYs = legs.map((l) => target.bind.worldPos[l.contact][1]);
5071
5462
  const lowestFoot = Math.min(...footYs);
5072
5463
  for (const leg of legs) {
5073
- const hipY = target.bind.worldPos[leg.chain[0]][1], footY = target.bind.worldPos[leg.effector][1];
5464
+ const hipY = target.bind.worldPos[leg.chain[0]][1], footY = target.bind.worldPos[leg.contact][1];
5074
5465
  const L = leg.lengths.reduce((a, b) => a + b, 0);
5075
5466
  if ((hipY - footY) / L < 0.5)
5076
5467
  stance = false;
@@ -5091,61 +5482,166 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5091
5482
  if (!(name in policies))
5092
5483
  policies[name] = "direction";
5093
5484
  }
5094
- const { ref: ref2, A, log: alignLog, kept } = alignReference(target, donor, byTarget, rootIdx, opts.align ?? "bone-direction", policies);
5485
+ for (const i of forced)
5486
+ if (policies[target.names[i]] === "keep")
5487
+ policies[target.names[i]] = "direction";
5488
+ const { ref: ref2, A, log: alignLog, kept, under: alignUnder } = alignReference(target, donor, byTarget, rootIdx, opts.align ?? "bone-direction", policies, hum ? 8 : 0, forced);
5489
+ if (opts.ik === "legs+hands" && hum) {
5490
+ const handNames = ["LeftHand", "RightHand"].map((k) => hum.target.bones[k]).filter((n) => !!n && byTarget.has(target.index.get(n)));
5491
+ for (const h of detectLegs(target, turned, byTarget, target.bind.worldPos, handNames, warn, 2)) {
5492
+ h.isFoot = false;
5493
+ h.baseDonor = donor.bind.worldPos[h.donorContact][1];
5494
+ h.stanceDonor = donorReach(h, donor.bind.worldPos);
5495
+ legs.push(h);
5496
+ }
5497
+ }
5095
5498
  const bridges = findBridges(target, byTarget);
5499
+ if (hum) {
5500
+ const hands = new Set([humIdx(hum.target, "LeftHand"), humIdx(hum.target, "RightHand")].filter((i) => i !== undefined));
5501
+ for (const b of bridges.values())
5502
+ if (hands.has(b.above))
5503
+ b.u = 0;
5504
+ }
5096
5505
  for (const [i, b] of bridges) {
5097
5506
  const a = qslerp(A[b.above], A[b.below], b.u);
5098
5507
  A[i] = a;
5099
5508
  ref2[i] = qnorm(qmul(a, target.bind.worldRot[i]));
5100
5509
  }
5510
+ const rollLog = [];
5511
+ if (hum && (opts.align ?? "bone-direction") !== "none") {
5512
+ const pt = posesFrom(target, ref2);
5513
+ const { donor: D, target: T } = hum;
5514
+ const dIdx = (k) => {
5515
+ const n = D.bones[k];
5516
+ return n === undefined ? undefined : donor.index.get(n);
5517
+ };
5518
+ const children2 = childrenOf(target);
5519
+ for (const s of ["Left", "Right"]) {
5520
+ const tLower = humIdx(T, `${s}LowerArm`), tHand = humIdx(T, `${s}Hand`), dLower = dIdx(`${s}LowerArm`), dHand = dIdx(`${s}Hand`);
5521
+ if (tLower === undefined || tHand === undefined || dLower === undefined || dHand === undefined)
5522
+ continue;
5523
+ const axisT = unit(sub2(pt[tHand], pt[tLower])), axisD = unit(sub2(donor.bind.worldPos[dHand], donor.bind.worldPos[dLower]));
5524
+ if (!axisT || !axisD)
5525
+ continue;
5526
+ const line = (av, pos, idx, hand) => {
5527
+ const i = idx(`${s} Index Proximal`), l = idx(`${s} Little Proximal`), th = idx(`${s} Thumb Proximal`);
5528
+ if (i !== undefined && l !== undefined)
5529
+ return { v: sub2(pos[i], pos[l]), via: "knuckle line" };
5530
+ if (th !== undefined)
5531
+ return { v: sub2(pos[th], pos[hand]), via: "thumb base" };
5532
+ return null;
5533
+ };
5534
+ const kt = line(T, pt, (k) => humIdx(T, k), tHand), kd = line(D, donor.bind.worldPos, dIdx, dHand);
5535
+ if (!kt || !kd) {
5536
+ warn(`${s.toLowerCase()} arm: no fingers on the ${kt ? "donor" : "target"} — palm roll not corrected`);
5537
+ continue;
5538
+ }
5539
+ const perp = (v, ax) => {
5540
+ const d = v[0] * ax[0] + v[1] * ax[1] + v[2] * ax[2];
5541
+ return unit([v[0] - ax[0] * d, v[1] - ax[1] * d, v[2] - ax[2] * d]);
5542
+ };
5543
+ const a = perp(kt.v, axisD), b = perp(kd.v, axisD);
5544
+ if (!a || !b)
5545
+ continue;
5546
+ const cross2 = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
5547
+ const ang = Math.atan2(cross2[0] * axisD[0] + cross2[1] * axisD[1] + cross2[2] * axisD[2], a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) + (hum.target.transfer?.roll?.[s] ?? 0) / DEG;
5548
+ const deg = Math.abs(ang) * DEG;
5549
+ if (deg < 1) {
5550
+ rollLog.push({ side: s, degrees: 0, via: kt.via });
5551
+ continue;
5552
+ }
5553
+ const R = qnorm([axisD[0] * Math.sin(ang / 2), axisD[1] * Math.sin(ang / 2), axisD[2] * Math.sin(ang / 2), Math.cos(ang / 2)]);
5554
+ const stack = [tLower];
5555
+ while (stack.length) {
5556
+ const i = stack.pop();
5557
+ ref2[i] = qnorm(qmul(R, ref2[i]));
5558
+ A[i] = qnorm(qmul(ref2[i], qinv(target.bind.worldRot[i])));
5559
+ stack.push(...children2[i]);
5560
+ }
5561
+ rollLog.push({ side: s, degrees: deg, via: `${kt.via} vs ${kd.via}` });
5562
+ }
5563
+ }
5101
5564
  const refPos = posesFrom(target, ref2);
5102
5565
  const skinned = target.bind.fromSkin.map((v, i) => v ? i : -1).filter((i) => i >= 0);
5103
5566
  const lowestBind = Math.min(...skinned.map((i) => target.bind.worldPos[i][1]));
5567
+ const donorChildren = childrenOf(donor);
5568
+ const donorSkinned = donor.bind.fromSkin.some(Boolean) ? donor.bind.fromSkin.map((v, i) => v ? i : -1).filter((i) => i >= 0) : donor.names.map((_, i) => i);
5569
+ const donorLowestBind = Math.min(...donorSkinned.map((i) => donor.bind.worldPos[i][1]));
5104
5570
  const ground = legs.length ? { target: lowestBind, donor: donorGround } : null;
5105
5571
  const feet = legs.filter((l) => l.isFoot);
5106
- const lowestOf = feet.length ? feet.map((l) => l.effector) : skinned;
5107
- let groundOffset = lowestBind - Math.min(...lowestOf.map((i) => refPos[i][1]));
5108
- for (const leg of feet) {
5109
- const hipY = refPos[leg.chain[0]][1] + groundOffset;
5110
- const reach = 0.98 * leg.lengths.reduce((a, b) => a + b, 0);
5111
- if (hipY - lowestBind > reach)
5112
- groundOffset -= hipY - lowestBind - reach;
5113
- }
5572
+ const restAbove = (leg) => stance ? target.bind.worldPos[leg.contact][1] - lowestBind : 0;
5573
+ let groundOffset = feet.length ? lowestBind - Math.min(...feet.map((l) => refPos[l.contact][1] - restAbove(l))) : lowestBind - Math.min(...skinned.map((i) => refPos[i][1]));
5574
+ if (!stance)
5575
+ for (const leg of feet) {
5576
+ const hipY = refPos[leg.chain[0]][1] + groundOffset;
5577
+ const reach = 0.98 * leg.lengths.reduce((a, b) => a + b, 0);
5578
+ const floor = lowestBind + restAbove(leg);
5579
+ if (hipY - floor > reach)
5580
+ groundOffset -= hipY - floor - reach;
5581
+ }
5114
5582
  const refPosGrounded = refPos.map((p) => [p[0], p[1] + groundOffset, p[2]]);
5115
5583
  const localOff = target.names.map((_, i) => {
5116
5584
  const p = target.parents[i];
5117
5585
  if (p < 0)
5118
5586
  return [0, 0, 0];
5119
- return qrot(qinv(target.bind.worldRot[p]), sub(target.bind.worldPos[i], target.bind.worldPos[p]));
5587
+ return qrot(qinv(target.bind.worldRot[p]), sub2(target.bind.worldPos[i], target.bind.worldPos[p]));
5120
5588
  });
5121
5589
  const children = childrenOf(target);
5122
- const placeLeg = (leg, goal, rot, pos) => {
5590
+ const donorBend = (leg, dpos) => {
5591
+ const hip = dpos[leg.donorHip], end = dpos[leg.donorEffector];
5592
+ const axis = unit(sub2(end, hip));
5593
+ if (!axis)
5594
+ return null;
5595
+ const acc = [0, 0, 0];
5596
+ let n = 0;
5597
+ for (let k = 1;k < leg.chain.length; k++) {
5598
+ const d = byTarget.get(leg.chain[k]);
5599
+ if (d === undefined)
5600
+ continue;
5601
+ const v = sub2(dpos[d], hip), t = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5602
+ acc[0] += v[0] - axis[0] * t;
5603
+ acc[1] += v[1] - axis[1] * t;
5604
+ acc[2] += v[2] - axis[2] * t;
5605
+ n++;
5606
+ }
5607
+ const dir = n ? unit(acc) : null;
5608
+ return dir ? { dir, off: len2(acc) / n / donorLegLen(leg) } : null;
5609
+ };
5610
+ const placeLeg = (leg, goal, rot, pos, bend = null) => {
5123
5611
  const full = [...leg.chain, leg.effector];
5124
5612
  const hip = pos[leg.chain[0]];
5125
5613
  const pts = full.map((i) => [...pos[i]]);
5126
5614
  const total = leg.lengths.reduce((a, b) => a + b, 0);
5127
- const short = len(sub(goal, hip)) - total;
5128
- const axis = unit(sub(pts[pts.length - 1], hip));
5615
+ const short = len2(sub2(goal, hip)) - total;
5616
+ const axis = unit(sub2(pts[pts.length - 1], hip));
5129
5617
  if (axis && short < 0) {
5130
- let flat = true;
5131
- for (let k = 1;k < pts.length - 1 && flat; k++) {
5132
- const v = sub(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5133
- if (len([v[0] - axis[0] * d, v[1] - axis[1] * d, v[2] - axis[2] * d]) > 0.0001 * total)
5134
- flat = false;
5135
- }
5136
- if (flat) {
5137
- const hipTurn = qmul(rot[leg.chain[0]], qinv(target.bind.worldRot[leg.chain[0]]));
5138
- const nudge = leg.pole ? qrot(hipTurn, leg.pole) : [0, 0, 1];
5139
- const dist = len(sub(goal, hip));
5140
- const sag = Math.max(0.001 * total, 0.5 * Math.sqrt(Math.max(0, total * total - dist * dist)));
5141
- for (let k = 1;k < pts.length - 1; k++)
5142
- pts[k] = add(pts[k], scl(nudge, sag));
5618
+ const hipTurn = qmul(rot[leg.chain[0]], qinv(target.bind.worldRot[leg.chain[0]]));
5619
+ const pole = leg.pole ? qrot(hipTurn, leg.pole) : [0, 0, 1];
5620
+ const acc = [0, 0, 0];
5621
+ for (let k = 1;k < pts.length - 1; k++) {
5622
+ const v = sub2(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5623
+ acc[0] += v[0] - axis[0] * d;
5624
+ acc[1] += v[1] - axis[1] * d;
5625
+ acc[2] += v[2] - axis[2] * d;
5626
+ }
5627
+ const off = bend ? bend.off * total : len2(acc) / (pts.length - 2);
5628
+ const cur = bend ? bend.dir : unit(acc);
5629
+ const against = cur ? cur[0] * pole[0] + cur[1] * pole[1] + cur[2] * pole[2] < 0 : false;
5630
+ const w = cur && !against ? Math.min(1, off / (BEND_TRUST * total)) : 0;
5631
+ if (w < 1) {
5632
+ const dir = unit(add(scl2(pole, 1 - w), scl2(cur ?? pole, w))) ?? pole;
5633
+ const dist = len2(sub2(goal, hip));
5634
+ const sag = Math.max(off, 0.001 * total, 0.5 * Math.sqrt(Math.max(0, total * total - dist * dist)));
5635
+ for (let k = 1;k < pts.length - 1; k++) {
5636
+ const v = sub2(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5637
+ pts[k] = add(add(hip, scl2(axis, d)), scl2(dir, sag));
5638
+ }
5143
5639
  }
5144
5640
  }
5145
5641
  fabrik(pts, leg.lengths, goal);
5146
5642
  for (let k = 0;k < leg.chain.length; k++) {
5147
5643
  const i = leg.chain[k], next = full[k + 1];
5148
- const cur = unit(qrot(rot[i], localOff[next])), want = unit(sub(pts[k + 1], pos[i]));
5644
+ const cur = unit(qrot(rot[i], localOff[next])), want = unit(sub2(pts[k + 1], pos[i]));
5149
5645
  if (cur && want)
5150
5646
  rot[i] = qnorm(qmul(arc(cur, want), rot[i]));
5151
5647
  pos[next] = add(pos[i], qrot(rot[i], localOff[next]));
@@ -5161,17 +5657,70 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5161
5657
  return short;
5162
5658
  };
5163
5659
  for (const leg of feet) {
5164
- const foot = refPosGrounded[leg.effector];
5165
- if (Math.abs(foot[1] - lowestBind) < 0.000001)
5660
+ const c = refPosGrounded[leg.contact], floor = lowestBind + restAbove(leg);
5661
+ if (Math.abs(c[1] - floor) < 0.000001)
5166
5662
  continue;
5167
- placeLeg(leg, [foot[0], lowestBind, foot[2]], ref2, refPosGrounded);
5663
+ const e = refPosGrounded[leg.effector];
5664
+ placeLeg(leg, [e[0], e[1] + floor - c[1], e[2]], ref2, refPosGrounded);
5168
5665
  }
5169
5666
  for (const leg of legs) {
5170
- leg.baseTarget = refPosGrounded[leg.effector][1];
5171
- leg.stanceTarget = sub(refPosGrounded[leg.effector], refPosGrounded[leg.chain[0]]);
5667
+ leg.baseTarget = refPosGrounded[leg.contact][1];
5668
+ leg.stanceTarget = sub2(refPosGrounded[leg.contact], refPosGrounded[leg.chain[0]]);
5669
+ leg.refReach = len2(sub2(refPosGrounded[leg.effector], refPosGrounded[leg.chain[0]]));
5172
5670
  }
5173
5671
  for (const i of byTarget.keys())
5174
5672
  A[i] = qnorm(qmul(ref2[i], qinv(target.bind.worldRot[i])));
5673
+ const twists = [];
5674
+ const twistFraction = opts.twist === false ? 0 : opts.twist ?? 0.5;
5675
+ if (hum && twistFraction > 0) {
5676
+ const NEXT = {
5677
+ LeftUpperArm: "LeftLowerArm",
5678
+ LeftLowerArm: "LeftHand",
5679
+ RightUpperArm: "RightLowerArm",
5680
+ RightLowerArm: "RightHand",
5681
+ LeftUpperLeg: "LeftLowerLeg",
5682
+ LeftLowerLeg: "LeftFoot",
5683
+ RightUpperLeg: "RightLowerLeg",
5684
+ RightLowerLeg: "RightFoot"
5685
+ };
5686
+ for (const [name, of] of Object.entries(hum.target.twist)) {
5687
+ const h = target.index.get(name), p = humIdx(hum.target, of), nxt = NEXT[of], c = nxt ? humIdx(hum.target, nxt) : undefined;
5688
+ if (h === undefined || p === undefined || c === undefined || target.parents[h] !== p || !byTarget.has(p) || !byTarget.has(c))
5689
+ continue;
5690
+ let g = target.parents[p];
5691
+ while (g >= 0 && !byTarget.has(g))
5692
+ g = target.parents[g];
5693
+ if (g < 0)
5694
+ continue;
5695
+ const axis = unit(sub2(target.bind.worldPos[c], target.bind.worldPos[p]));
5696
+ if (!axis)
5697
+ continue;
5698
+ const along = sub2(target.bind.worldPos[h], target.bind.worldPos[p]);
5699
+ const t = Math.max(0, Math.min(1, (along[0] * axis[0] + along[1] * axis[1] + along[2] * axis[2]) / len2(sub2(target.bind.worldPos[c], target.bind.worldPos[p]))));
5700
+ twists.push({ bone: h, parent: p, grand: g, child: c, t, axisLocal: qrot(qinv(target.bind.worldRot[p]), axis), name, of });
5701
+ }
5702
+ }
5703
+ const separateRoot = rootIdx >= 0 && hipsIdx >= 0 && hipsIdx !== rootIdx && donorRoot !== undefined;
5704
+ const rootMotion = (opts.rootMotion ?? "auto") === "auto" ? separateRoot ? "root" : "hips" : opts.rootMotion;
5705
+ const donorHipsForYaw = hum ? (() => {
5706
+ const n = hum.donor.bones.Hips;
5707
+ return n === undefined ? undefined : donor.index.get(n);
5708
+ })() : undefined;
5709
+ const rootRotation = (opts.rootRotation ?? "none") === "yaw" && rootTranslateOnly && donorHipsForYaw !== undefined ? "yaw" : "none";
5710
+ if ((opts.rootRotation ?? "none") === "yaw" && rootRotation === "none")
5711
+ warn(`rootRotation 'yaw' needs a target root above the hips mapped to the donor's hips${hum ? "" : " (humanoid avatars)"} — the root keeps its bind heading`);
5712
+ const hipsHeading = (dwRot) => {
5713
+ const q = qmul(dwRot[donorHipsForYaw], qinv(donor.bind.worldRot[donorHipsForYaw]));
5714
+ const f = qrot(q, [0, 0, 1]);
5715
+ return Math.atan2(f[0], f[2]);
5716
+ };
5717
+ const wrapRad = (a) => Math.atan2(Math.sin(a), Math.cos(a));
5718
+ const donorHips = hum ? (() => {
5719
+ const n = hum.donor.bones.Hips;
5720
+ return n === undefined ? undefined : donor.index.get(n);
5721
+ })() : undefined;
5722
+ const donorBob = separateRoot && donorHips !== undefined && donorHips !== donorRoot && donor.parents[donorHips] >= 0;
5723
+ const hipsParent = hipsIdx >= 0 ? target.parents[hipsIdx] : -1;
5175
5724
  let rootScaleFrom = "none";
5176
5725
  const rootScale = (() => {
5177
5726
  if (opts.rootScale !== undefined) {
@@ -5199,20 +5748,32 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5199
5748
  legDisagreement,
5200
5749
  stance,
5201
5750
  align: alignLog,
5751
+ underThreshold: alignUnder,
5202
5752
  kept,
5203
5753
  bridges: [...bridges.keys()].map((i) => target.names[i]),
5204
5754
  groundOffset,
5205
5755
  ground,
5206
- legs: legs.map((l) => ({ effector: l.name, donorEffector: donor.names[l.donorEffector], chain: l.chain.map((i) => target.names[i]), ratio: l.ratio, shortfall: 0 })),
5756
+ legs: legs.map((l) => ({ effector: l.name, donorEffector: donor.names[l.donorEffector], contact: target.names[l.contact], chain: l.chain.map((i) => target.names[i]), ratio: l.ratio, shortfall: 0, isFoot: l.isFoot, planted: 0 })),
5207
5757
  clips: [],
5208
5758
  mapped: pairs.map((p) => ({ target: p.target, donor: p.donor })),
5209
5759
  unmappedTarget: target.names.filter((n, i) => !byTarget.has(i) && !bridges.has(i) && target.nodes[i].getMesh() === null),
5210
5760
  rootScale,
5211
5761
  rootScaleFrom,
5212
- root: rootIdx >= 0 && donorRoot !== undefined ? { target: target.names[rootIdx], donor: donor.names[donorRoot] } : null
5762
+ root: rootIdx >= 0 && donorRoot !== undefined ? { target: target.names[rootIdx], donor: donor.names[donorRoot] } : null,
5763
+ rootTranslateOnly,
5764
+ roll: rollLog,
5765
+ rootMotion,
5766
+ rootRotation,
5767
+ hipsBob: separateRoot ? donorBob ? "donor" : rootMotion === "hips" ? null : "root" : null,
5768
+ twist: twists.map((t) => ({ bone: t.name, of: t.of, at: t.t }))
5213
5769
  };
5214
- const animated = new Set([...byTarget.keys(), ...bridges.keys()]);
5770
+ const animated = new Set([...byTarget.keys(), ...bridges.keys(), ...twists.map((t) => t.bone)]);
5215
5771
  const rootRefPos = rootIdx >= 0 ? refPosGrounded[rootIdx] : null;
5772
+ const plantOn = opts.plant !== false;
5773
+ const donorLegLen = (l) => len2(sub2(donor.bind.worldPos[l.donorEffector], donor.bind.worldPos[l.donorHip])) || 1;
5774
+ const PLANT_FADE = 4;
5775
+ const driven = new Set([...byTarget.keys(), ...bridges.keys()]);
5776
+ const hipsParentScale = hipsParent >= 0 ? restWorld.scale[hipsParent] : 1;
5216
5777
  for (const clip of clips) {
5217
5778
  const frames = Math.max(2, Math.round(clip.duration * fps) + 1);
5218
5779
  const channels = new Map;
@@ -5224,47 +5785,306 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5224
5785
  }
5225
5786
  return c;
5226
5787
  };
5788
+ const events = [];
5789
+ const open = new Map;
5790
+ const mark = (kind, bone, time, value, at) => {
5791
+ const key = `${kind}:${bone}`;
5792
+ const e = open.get(key);
5793
+ if (e) {
5794
+ e.to = time;
5795
+ if (value !== undefined)
5796
+ e.value = Math.max(e.value ?? 0, value);
5797
+ } else {
5798
+ const n = { kind, bone, from: time, to: time, ...value !== undefined ? { value } : {}, ...at ? { at } : {} };
5799
+ open.set(key, n);
5800
+ events.push(n);
5801
+ }
5802
+ };
5803
+ const rootXZ = () => rootIdx >= 0 ? wpos[rootIdx] : hipsIdx >= 0 ? wpos[hipsIdx] : [0, 0, 0];
5804
+ let rootFirst = [0, 0, 0], rootLast = [0, 0, 0];
5805
+ const rootTrack = [];
5806
+ const contactTrack = new Map;
5227
5807
  const wrot = new Array(target.names.length);
5228
5808
  const wpos = new Array(target.names.length);
5229
5809
  const delta = new Array(target.names.length);
5230
- for (let f = 0;f < frames; f++) {
5231
- const time = f / (frames - 1) * clip.duration;
5232
- const donorPose = clip.sample(time);
5810
+ const timeOf = (f) => f / (frames - 1) * clip.duration;
5811
+ const yaw0 = rootRotation === "yaw" ? hipsHeading(worldOf(donor, clip.sample(0)).rot) : 0;
5812
+ const turn = rootRotation === "yaw" ? wrapRad(hipsHeading(worldOf(donor, clip.sample(clip.duration)).rot) - yaw0) * DEG : 0;
5813
+ const frameFK = (f) => {
5814
+ const donorPose = clip.sample(timeOf(f));
5233
5815
  const dw = worldOf(donor, donorPose);
5234
5816
  for (const i of byTarget.keys()) {
5235
5817
  const s = byTarget.get(i);
5236
5818
  delta[i] = qmul(dw.rot[s], qinv(donor.bind.worldRot[s]));
5237
5819
  }
5820
+ if (rootTranslateOnly) {
5821
+ if (rootRotation === "yaw") {
5822
+ const y = wrapRad(hipsHeading(dw.rot) - yaw0);
5823
+ delta[rootIdx] = [0, Math.sin(y / 2), 0, Math.cos(y / 2)];
5824
+ } else
5825
+ delta[rootIdx] = IDENT;
5826
+ }
5238
5827
  for (const [i, b] of bridges)
5239
5828
  delta[i] = qslerp(delta[b.above], delta[b.below], b.u);
5829
+ let bob = null;
5830
+ if (donorBob && donorRoot !== undefined && donorHips !== undefined) {
5831
+ const v = sub2(dw.pos[donorHips], dw.pos[donorRoot]);
5832
+ const v0 = sub2(donor.bind.worldPos[donorHips], donor.bind.worldPos[donorRoot]);
5833
+ const dRoot = qmul(dw.rot[donorRoot], qinv(donor.bind.worldRot[donorRoot]));
5834
+ bob = scl2(sub2(v, qrot(dRoot, v0)), rootScale);
5835
+ }
5240
5836
  for (const i of target.order) {
5241
5837
  const p = target.parents[i];
5242
- if (animated.has(i))
5838
+ if (driven.has(i))
5243
5839
  wrot[i] = qnorm(qmul(delta[i], ref2[i]));
5244
5840
  else
5245
5841
  wrot[i] = p >= 0 ? qnorm(qmul(wrot[p], target.rest.r[i])) : qnorm(target.rest.r[i]);
5246
5842
  if (i === rootIdx && rootRefPos && donorRoot !== undefined) {
5247
5843
  const d = dw.pos[donorRoot], r = donor.bind.worldPos[donorRoot];
5248
- wpos[i] = add(rootRefPos, scl(sub(d, r), rootScale));
5844
+ wpos[i] = add(rootRefPos, scl2(sub2(d, r), rootScale));
5249
5845
  } else if (p < 0)
5250
5846
  wpos[i] = target.rest.t[i];
5251
5847
  else
5252
5848
  wpos[i] = add(wpos[p], qrot(wrot[p], localOff[i]));
5849
+ if (i === hipsIdx && bob)
5850
+ wpos[i] = add(wpos[i], bob);
5253
5851
  }
5852
+ return { dw };
5853
+ };
5854
+ const goalOf = (leg, dw) => {
5855
+ const hip = wpos[leg.chain[0]];
5856
+ const off = sub2(wpos[leg.contact], wpos[leg.effector]);
5857
+ const a = sub2(donorReach(leg, dw.pos), off);
5858
+ const c = [0, 1, 2].map((i) => leg.stanceTarget[i] - leg.stanceDonor[i]);
5859
+ const R = 0.995 * leg.lengths.reduce((x, y) => x + y, 0);
5860
+ let k = 1;
5861
+ if (len2(add(a, c)) > R) {
5862
+ const cc = c[0] * c[0] + c[1] * c[1] + c[2] * c[2], ac = a[0] * c[0] + a[1] * c[1] + a[2] * c[2], aa = a[0] * a[0] + a[1] * a[1] + a[2] * a[2] - R * R;
5863
+ const disc = ac * ac - cc * aa;
5864
+ k = cc > 0.000000000001 && disc >= 0 ? Math.max(0, Math.min(1, (-ac + Math.sqrt(disc)) / cc)) : 0;
5865
+ }
5866
+ let effector = [0, 1, 2].map((i) => hip[i] + a[i] + c[i] * k);
5867
+ if (leg.isFoot && leg.refReach > 0) {
5868
+ const ankle = add(scl2(sub2(dw.pos[leg.donorEffector], dw.pos[leg.donorHip]), leg.ratio), c);
5869
+ const dir = unit(ankle);
5870
+ const ext = len2(sub2(dw.pos[leg.donorEffector], dw.pos[leg.donorHip])) / donorLegLen(leg);
5871
+ if (dir)
5872
+ effector = add(hip, scl2(dir, Math.min(R / 0.995, ext * leg.refReach)));
5873
+ }
5874
+ return { effector, contact: add(effector, off) };
5875
+ };
5876
+ const spans = [];
5877
+ if (plantOn && legs.some((l) => l.isFoot)) {
5878
+ const feetIdx = legs.map((l, i) => l.isFoot ? i : -1).filter((i) => i >= 0);
5879
+ const goals = feetIdx.map(() => new Array(frames));
5880
+ const rel = feetIdx.map(() => new Array(frames));
5881
+ const still = feetIdx.map(() => new Array(frames).fill(false));
5882
+ let prev = null;
5883
+ for (let f = 0;f < frames; f++) {
5884
+ const { dw } = frameFK(f);
5885
+ feetIdx.forEach((li, k) => {
5886
+ const leg = legs[li], L = donorLegLen(leg), dFoot = dw.pos[leg.donorContact];
5887
+ goals[k][f] = goalOf(leg, dw).contact;
5888
+ rel[k][f] = sub2(dFoot, dw.pos[leg.donorHip]);
5889
+ const speed = prev ? len2(sub2(dFoot, prev[k])) * fps : Infinity;
5890
+ still[k][f] = speed < 0.15 * L && dFoot[1] - leg.baseDonor < 0.06 * L;
5891
+ });
5892
+ prev = feetIdx.map((li) => dw.pos[legs[li].donorContact]);
5893
+ }
5894
+ feetIdx.forEach((li, k) => {
5895
+ const s = still[k];
5896
+ const L = donorLegLen(legs[li]);
5897
+ const cyclic = len2(sub2(rel[k][0], rel[k][frames - 1])) < 0.02 && s[frames - 1];
5898
+ if (cyclic)
5899
+ s[0] = true;
5900
+ const runs = [];
5901
+ for (let i = 0;i < frames; i++) {
5902
+ if (!s[i])
5903
+ continue;
5904
+ const last = runs[runs.length - 1];
5905
+ if (last && last.start + last.length === i)
5906
+ last.length++;
5907
+ else
5908
+ runs.push({ start: i, length: 1 });
5909
+ }
5910
+ const travel2 = sub2(goals[k][frames - 1], goals[k][0]);
5911
+ const head = runs[0], tail = runs[runs.length - 1];
5912
+ const seam = cyclic && runs.length >= 2 && head.start === 0 && tail.start + tail.length === frames;
5913
+ const meanOf = (list) => {
5914
+ const acc = [0, 0, 0];
5915
+ let n = 0;
5916
+ for (const r of list)
5917
+ for (let i = r.start;i < r.start + r.length; i++) {
5918
+ const g = add(goals[k][i], r.shift);
5919
+ acc[0] += g[0];
5920
+ acc[1] += g[1];
5921
+ acc[2] += g[2];
5922
+ n++;
5923
+ }
5924
+ return { anchor: scl2(acc, 1 / Math.max(1, n)), n };
5925
+ };
5926
+ const fits = (r, anchor) => {
5927
+ let worst = 0;
5928
+ for (let i = r.start;i < r.start + r.length; i++)
5929
+ worst = Math.max(worst, Math.hypot(goals[k][i][0] - anchor[0], goals[k][i][2] - anchor[2]));
5930
+ return worst <= 0.05 * L;
5931
+ };
5932
+ for (const r of runs) {
5933
+ if (seam && (r === head || r === tail))
5934
+ continue;
5935
+ if (r.length < 3)
5936
+ continue;
5937
+ const { anchor } = meanOf([{ ...r, shift: [0, 0, 0] }]);
5938
+ if (fits(r, anchor))
5939
+ spans.push({ leg: li, start: r.start, length: r.length, anchor });
5940
+ }
5941
+ if (seam && head.length + tail.length >= 3) {
5942
+ const { anchor } = meanOf([{ ...head, shift: [0, 0, 0] }, { ...tail, shift: scl2(travel2, -1) }]);
5943
+ const tailAnchor = add(anchor, travel2);
5944
+ if (fits(head, anchor) && fits(tail, tailAnchor)) {
5945
+ spans.push({ leg: li, start: head.start, length: head.length, anchor, openStart: true });
5946
+ spans.push({ leg: li, start: tail.start, length: tail.length, anchor: tailAnchor, openEnd: true });
5947
+ }
5948
+ }
5949
+ });
5950
+ }
5951
+ const plantAt = (li, f) => {
5952
+ for (const s of spans) {
5953
+ if (s.leg !== li)
5954
+ continue;
5955
+ const n = f - s.start;
5956
+ if (n < 0 || n >= s.length)
5957
+ continue;
5958
+ const w = Math.min(1, s.openStart ? 1 : (n + 1) / PLANT_FADE, s.openEnd ? 1 : (s.length - n) / PLANT_FADE);
5959
+ return { anchor: s.anchor, w };
5960
+ }
5961
+ return null;
5962
+ };
5963
+ for (const s of spans)
5964
+ report.legs[s.leg].planted += s.length;
5965
+ for (let f = 0;f < frames; f++) {
5966
+ const time = timeOf(f);
5967
+ const { dw } = frameFK(f);
5968
+ const active = new Set;
5969
+ if (f === 0)
5970
+ rootFirst = [...rootXZ()];
5971
+ if (f === frames - 1)
5972
+ rootLast = [...rootXZ()];
5254
5973
  if (legs.length) {
5255
5974
  for (const [li, leg] of legs.entries()) {
5256
- const dHip = dw.pos[leg.donorHip], dFoot = dw.pos[leg.donorEffector];
5257
- const hip = wpos[leg.chain[0]];
5258
- const goal = [
5259
- hip[0] + leg.stanceTarget[0] + (dFoot[0] - dHip[0] - leg.stanceDonor[0]) * leg.ratio,
5260
- leg.baseTarget + (dFoot[1] - leg.baseDonor) * leg.ratio,
5261
- hip[2] + leg.stanceTarget[2] + (dFoot[2] - dHip[2] - leg.stanceDonor[2]) * leg.ratio
5262
- ];
5263
- const short = placeLeg(leg, goal, wrot, wpos);
5975
+ const g = goalOf(leg, dw), goal = g.effector;
5976
+ const pl = plantAt(li, f);
5977
+ if (pl) {
5978
+ goal[0] += (pl.anchor[0] - g.contact[0]) * pl.w;
5979
+ goal[2] += (pl.anchor[2] - g.contact[2]) * pl.w;
5980
+ }
5981
+ const slack = len2(sub2(scl2(sub2(dw.pos[leg.donorContact], dw.pos[leg.donorEffector]), leg.contactRatio), sub2(wpos[leg.contact], wpos[leg.effector])));
5982
+ const short = placeLeg(leg, goal, wrot, wpos, donorBend(leg, dw.pos)) - slack;
5983
+ if (pl) {
5984
+ const held = target.names[leg.contact];
5985
+ const r = rootXZ(), c = wpos[leg.contact];
5986
+ mark("plant", held, time, undefined, [c[0] - r[0], c[1], c[2] - r[2]]);
5987
+ active.add(`plant:${held}`);
5988
+ }
5264
5989
  if (short > report.legs[li].shortfall)
5265
5990
  report.legs[li].shortfall = short;
5991
+ if (short > 0.005) {
5992
+ mark("reach", leg.name, time, short);
5993
+ active.add(`reach:${leg.name}`);
5994
+ }
5995
+ if (leg.isFoot) {
5996
+ const deepest = () => {
5997
+ let worst = { bone: -1, dip: 0 };
5998
+ const below = [...children[leg.effector]];
5999
+ while (below.length) {
6000
+ const c = below.pop();
6001
+ const dip = lowestBind - wpos[c][1];
6002
+ if (dip > worst.dip)
6003
+ worst = { bone: c, dip };
6004
+ below.push(...children[c]);
6005
+ }
6006
+ return worst;
6007
+ };
6008
+ const d = deepest();
6009
+ let donorDip = 0;
6010
+ {
6011
+ const stack = [...donorChildren[leg.donorEffector]];
6012
+ let low = Infinity;
6013
+ while (stack.length) {
6014
+ const c = stack.pop();
6015
+ low = Math.min(low, dw.pos[c][1]);
6016
+ stack.push(...donorChildren[c]);
6017
+ }
6018
+ if (low < Infinity)
6019
+ donorDip = Math.max(0, donorLowestBind - low) * leg.ratio;
6020
+ }
6021
+ d.dip -= donorDip;
6022
+ if (d.dip > 0.002) {
6023
+ const v = sub2(wpos[d.bone], wpos[leg.effector]), r = len2(v);
6024
+ const axis = unit(cross(v, [0, 1, 0]));
6025
+ if (axis && r > 0.0001) {
6026
+ const want = Math.min(1, (v[1] + d.dip) / r), have = v[1] / r;
6027
+ let ang = Math.asin(want) - Math.asin(Math.max(-1, Math.min(1, have)));
6028
+ ang = Math.max(-FOOT_PITCH_CAP, Math.min(FOOT_PITCH_CAP, ang));
6029
+ const tryPitch = (a) => {
6030
+ const q2 = axisAngle(axis, a);
6031
+ const p = qrot(q2, v);
6032
+ return p[1] - v[1];
6033
+ };
6034
+ if (tryPitch(ang) < tryPitch(-ang))
6035
+ ang = -ang;
6036
+ const q = axisAngle(axis, ang);
6037
+ const stack = [leg.effector];
6038
+ while (stack.length) {
6039
+ const i = stack.pop();
6040
+ wrot[i] = qnorm(qmul(q, wrot[i]));
6041
+ for (const c of children[i]) {
6042
+ wpos[c] = add(wpos[i], qrot(wrot[i], localOff[c]));
6043
+ stack.push(c);
6044
+ }
6045
+ }
6046
+ }
6047
+ }
6048
+ const left = deepest();
6049
+ if (left.dip > 0.01) {
6050
+ mark("below", target.names[left.bone], time, left.dip);
6051
+ active.add(`below:${target.names[left.bone]}`);
6052
+ }
6053
+ }
5266
6054
  }
5267
6055
  }
6056
+ for (const k of [...open.keys()])
6057
+ if (!active.has(k))
6058
+ open.delete(k);
6059
+ for (const tw of twists) {
6060
+ const aRef = qrot(ref2[tw.parent], tw.axisLocal);
6061
+ const dP = qmul(wrot[tw.parent], qinv(ref2[tw.parent])), dG = qmul(wrot[tw.grand], qinv(ref2[tw.grand])), dC = qmul(wrot[tw.child], qinv(ref2[tw.child]));
6062
+ const twistIn = twistAbout(qmul(qinv(dG), dP), aRef), twistOut = twistAbout(qmul(qinv(dP), dC), aRef);
6063
+ const ang = twistFraction * (tw.t * twistOut - (1 - tw.t) * twistIn);
6064
+ const aNow = qrot(wrot[tw.parent], tw.axisLocal);
6065
+ wrot[tw.bone] = qnorm(qmul(axisAngle(aNow, ang), qmul(wrot[tw.parent], target.rest.r[tw.bone])));
6066
+ const stack = [tw.bone];
6067
+ while (stack.length) {
6068
+ const i = stack.pop();
6069
+ for (const c of children[i]) {
6070
+ if (!driven.has(c))
6071
+ wrot[c] = qnorm(qmul(wrot[i], target.rest.r[c]));
6072
+ wpos[c] = add(wpos[i], qrot(wrot[i], localOff[c]));
6073
+ stack.push(c);
6074
+ }
6075
+ }
6076
+ }
6077
+ rootTrack.push([...rootXZ()]);
6078
+ for (const [li, leg] of legs.entries()) {
6079
+ if (!leg.isFoot)
6080
+ continue;
6081
+ let tr = contactTrack.get(li);
6082
+ if (!tr) {
6083
+ tr = [];
6084
+ contactTrack.set(li, tr);
6085
+ }
6086
+ tr.push([...wpos[leg.contact]]);
6087
+ }
5268
6088
  for (const i of animated) {
5269
6089
  const p = target.parents[i];
5270
6090
  const local = p >= 0 ? qnorm(qmul(qinv(wrot[p]), wrot[i])) : wrot[i];
@@ -5275,9 +6095,28 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5275
6095
  }
5276
6096
  }
5277
6097
  if (rootIdx >= 0 && rootRefPos && donorRoot !== undefined) {
5278
- const rel = sub(wpos[rootIdx], parentPos);
6098
+ const travel2 = sub2(wpos[rootIdx], rootRefPos);
6099
+ let strip = [0, 0, 0], keep = [0, 0, 0];
6100
+ if (separateRoot) {
6101
+ if (rootMotion === "root") {
6102
+ strip = [0, travel2[1], 0];
6103
+ keep = strip;
6104
+ } else if (rootMotion === "none") {
6105
+ strip = travel2;
6106
+ keep = [0, travel2[1], 0];
6107
+ } else {
6108
+ strip = travel2;
6109
+ keep = travel2;
6110
+ }
6111
+ } else if (rootMotion === "none")
6112
+ strip = [travel2[0], 0, travel2[2]];
6113
+ const rel = sub2(sub2(wpos[rootIdx], strip), parentPos);
5279
6114
  const [x, y, z] = qrot(qinv(parentRot), rel);
5280
6115
  (chan(rootIdx).pos ??= []).push([x / parentScale, y / parentScale, z / parentScale]);
6116
+ if (separateRoot && hipsParent >= 0) {
6117
+ const [hx, hy, hz] = qrot(qinv(wrot[hipsParent]), add(sub2(wpos[hipsIdx], wpos[hipsParent]), keep));
6118
+ (chan(hipsIdx).pos ??= []).push([hx / hipsParentScale, hy / hipsParentScale, hz / hipsParentScale]);
6119
+ }
5281
6120
  }
5282
6121
  }
5283
6122
  for (const c of channels.values())
@@ -5297,7 +6136,54 @@ function retarget(target, donorIn, map, rootBone, opts = {}) {
5297
6136
  keys += c.times.length;
5298
6137
  }
5299
6138
  }
5300
- report.clips.push({ name: anim.getName(), duration: clip.duration, frames, keys });
6139
+ const sideOf = (li) => {
6140
+ const n = `${legs[li].name} ${target.names[legs[li].contact]}`;
6141
+ if (/left|(^|[_.\s])l([_.\s]|$)/i.test(n))
6142
+ return "l";
6143
+ if (/right|(^|[_.\s])r([_.\s]|$)/i.test(n))
6144
+ return "r";
6145
+ return null;
6146
+ };
6147
+ const contacts = { feet: { l: null, r: null }, l: [], r: [], first: null };
6148
+ const dtF = frames > 1 ? clip.duration / (frames - 1) : 1;
6149
+ for (const [li, leg] of legs.entries()) {
6150
+ if (!leg.isFoot)
6151
+ continue;
6152
+ const side = sideOf(li);
6153
+ const tr = contactTrack.get(li);
6154
+ if (!side || !tr || tr.length < 2)
6155
+ continue;
6156
+ contacts.feet[side] = target.names[leg.contact];
6157
+ const n = tr.length;
6158
+ const sp = tr.map((_, k2) => {
6159
+ const a = tr[Math.max(0, k2 - 1)], b = tr[Math.min(n - 1, k2 + 1)];
6160
+ const dt = (Math.min(n - 1, k2 + 1) - Math.max(0, k2 - 1)) * dtF;
6161
+ return Math.hypot(b[0] - a[0], b[2] - a[2]) / (dt || dtF);
6162
+ });
6163
+ const hs = tr.map((p) => p[1]);
6164
+ const lo = Math.min(...sp), hi = Math.max(...sp), hLo = Math.min(...hs), hHi = Math.max(...hs);
6165
+ const still = hi - lo < 0.05;
6166
+ const down2 = tr.map((_, k2) => still || sp[k2] < lo + 0.2 * (hi - lo) && hs[k2] < hLo + 0.5 * (hHi - hLo));
6167
+ let k = 0;
6168
+ while (k < n) {
6169
+ if (!down2[k]) {
6170
+ k++;
6171
+ continue;
6172
+ }
6173
+ const start = k;
6174
+ while (k < n && down2[k])
6175
+ k++;
6176
+ if (k - start < 2 && !still)
6177
+ continue;
6178
+ const r = rootTrack[start], c = tr[start];
6179
+ contacts[side].push({ from: start * dtF, to: (k - 1) * dtF, at: [c[0] - r[0], c[1], c[2] - r[2]] });
6180
+ }
6181
+ }
6182
+ const down = (side) => contacts[side].some((s) => s.from <= 0.000001);
6183
+ contacts.first = down("l") && down("r") ? "both" : down("l") ? "l" : down("r") ? "r" : null;
6184
+ const travel = [rootLast[0] - rootFirst[0], rootLast[2] - rootFirst[2]];
6185
+ anim.setExtras({ ...anim.getExtras(), lecodes: { contacts, travel } });
6186
+ report.clips.push({ name: anim.getName(), duration: clip.duration, frames, keys, events, turn, contacts, travel });
5301
6187
  }
5302
6188
  return report;
5303
6189
  }
@@ -5314,9 +6200,9 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5314
6200
  hi[i] = Math.max(hi[i], p[i]);
5315
6201
  }
5316
6202
  const fwd = head && tail ? [head[0] - tail[0], 0, head[2] - tail[2]] : [0, 0, 1];
5317
- const len = Math.hypot(...fwd) || 1;
6203
+ const len2 = Math.hypot(...fwd) || 1;
5318
6204
  return {
5319
- forward: [fwd[0] / len, fwd[1] / len, fwd[2] / len],
6205
+ forward: [fwd[0] / len2, fwd[1] / len2, fwd[2] / len2],
5320
6206
  up: [0, 1, 0],
5321
6207
  height: hi[1] - lo[1],
5322
6208
  length: Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2])
@@ -5358,8 +6244,8 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5358
6244
  ch.rot = keepR;
5359
6245
  if (keepP)
5360
6246
  ch.pos = keepP;
5361
- }, sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]], scl = (a, k) => [a[0] * k, a[1] * k, a[2] * k], len = (v) => Math.hypot(v[0], v[1], v[2]), unit = (v) => {
5362
- const l = len(v);
6247
+ }, sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]], scl2 = (a, k) => [a[0] * k, a[1] * k, a[2] * k], len2 = (v) => Math.hypot(v[0], v[1], v[2]), unit = (v) => {
6248
+ const l = len2(v);
5363
6249
  return l > 0.000000000001 ? [v[0] / l, v[1] / l, v[2] / l] : null;
5364
6250
  }, arc = (from, to) => {
5365
6251
  const d = from[0] * to[0] + from[1] * to[1] + from[2] * to[2];
@@ -5380,15 +6266,16 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5380
6266
  children[p].push(i);
5381
6267
  });
5382
6268
  return children;
5383
- }, alignReference = (target, donor, byTarget, rootIdx, align, policies) => {
6269
+ }, alignReference = (target, donor, byTarget, rootIdx, align, policies, minDeg = 0, force = new Set) => {
5384
6270
  const ref2 = target.bind.worldRot.map((q) => [...q]);
5385
6271
  const A = target.names.map(() => IDENT);
5386
6272
  const log = [];
6273
+ const under = [];
5387
6274
  const kept = [];
5388
6275
  if (align === "none") {
5389
6276
  for (const i of byTarget.keys())
5390
6277
  kept.push(target.names[i]);
5391
- return { ref: ref2, A, log, kept };
6278
+ return { ref: ref2, A, log, kept, under };
5392
6279
  }
5393
6280
  const children = childrenOf(target);
5394
6281
  const mappedChildCount = (i) => {
@@ -5441,7 +6328,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5441
6328
  const acc = [0, 0, 0];
5442
6329
  let n = 0;
5443
6330
  for (const to of tos) {
5444
- const u = unit(sub(to, from));
6331
+ const u = unit(sub2(to, from));
5445
6332
  if (!u)
5446
6333
  continue;
5447
6334
  acc[0] += u[0];
@@ -5475,15 +6362,24 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5475
6362
  } else if (children[i].length) {
5476
6363
  const donorKids = donor.parents.map((p, k) => [p, k]).filter(([p]) => p === s).map(([, k]) => k);
5477
6364
  if (donorKids.length) {
5478
- ut = unit(sub(target.bind.worldPos[children[i][0]], target.bind.worldPos[i]));
5479
- us = unit(sub(donor.bind.worldPos[donorKids[0]], donor.bind.worldPos[s]));
6365
+ ut = unit(sub2(target.bind.worldPos[children[i][0]], target.bind.worldPos[i]));
6366
+ us = unit(sub2(donor.bind.worldPos[donorKids[0]], donor.bind.worldPos[s]));
5480
6367
  via = `${target.names[children[i][0]]} (unmapped child)`;
5481
6368
  }
5482
6369
  }
5483
6370
  let a;
5484
- if (ut && us)
6371
+ if (ut && us) {
5485
6372
  a = arc(ut, us);
5486
- else {
6373
+ const off = 2 * Math.acos(Math.min(1, Math.abs(a[3]))) * DEG;
6374
+ if (minDeg > 0 && off < minDeg && !force.has(i)) {
6375
+ if (off > 0.5)
6376
+ under.push({ bone: target.names[i], degrees: off, via });
6377
+ a = IDENT;
6378
+ kept.push(target.names[i]);
6379
+ A[i] = a;
6380
+ continue;
6381
+ }
6382
+ } else {
5487
6383
  a = A[target.parents[i]] ?? IDENT;
5488
6384
  via = "inherited from parent";
5489
6385
  }
@@ -5494,7 +6390,8 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5494
6390
  log.push({ bone: target.names[i], degrees: deg, via });
5495
6391
  }
5496
6392
  log.sort((a, b) => b.degrees - a.degrees);
5497
- return { ref: ref2, A, log, kept };
6393
+ under.sort((a, b) => b.degrees - a.degrees);
6394
+ return { ref: ref2, A, log, kept, under };
5498
6395
  }, findBridges = (target, byTarget) => {
5499
6396
  const children = childrenOf(target);
5500
6397
  const subtreeSize = new Array(target.names.length).fill(1);
@@ -5535,7 +6432,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5535
6432
  out.set(i, { above: a, below: b.idx, u: up / (up + b.depth) });
5536
6433
  }
5537
6434
  return out;
5538
- }, detectLegs = (target, donor, byTarget, refPos, explicit, warn) => {
6435
+ }, BEND_TRUST = 0.05, FOOT_PITCH_CAP, cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]], detectLegs = (target, donor, byTarget, refPos, explicit, warn, maxAbove = 4) => {
5539
6436
  const children = childrenOf(target);
5540
6437
  const mappedKids = (i) => {
5541
6438
  const out = [];
@@ -5608,11 +6505,11 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5608
6505
  const path = [];
5609
6506
  for (let b = target.parents[e];b >= 0 && b !== target.parents[hipMapped]; b = target.parents[b])
5610
6507
  path.unshift(b);
5611
- const chain = path.slice(Math.max(0, path.length - 4));
6508
+ const chain = path.slice(Math.max(0, path.length - maxAbove));
5612
6509
  const legFull = [...chain, e];
5613
6510
  const lengths = [];
5614
6511
  for (let j = 1;j < legFull.length; j++)
5615
- lengths.push(len(sub(target.bind.worldPos[legFull[j]], target.bind.worldPos[legFull[j - 1]])));
6512
+ lengths.push(len2(sub2(target.bind.worldPos[legFull[j]], target.bind.worldPos[legFull[j - 1]])));
5616
6513
  let hipRef = chain[0];
5617
6514
  while (hipRef >= 0 && !byTarget.has(hipRef))
5618
6515
  hipRef = target.parents[hipRef];
@@ -5624,7 +6521,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5624
6521
  dlen = 0;
5625
6522
  break;
5626
6523
  }
5627
- dlen += len(sub(donor.bind.worldPos[b], donor.bind.worldPos[p]));
6524
+ dlen += len2(sub2(donor.bind.worldPos[b], donor.bind.worldPos[p]));
5628
6525
  }
5629
6526
  const dfull = [donorHip, donorEffector];
5630
6527
  const tlen = lengths.reduce((a, b) => a + b, 0);
@@ -5633,17 +6530,17 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5633
6530
  let pole = null;
5634
6531
  {
5635
6532
  const hip = target.bind.worldPos[legFull[0]], foot = target.bind.worldPos[legFull[legFull.length - 1]];
5636
- const axis = unit(sub(foot, hip));
6533
+ const axis = unit(sub2(foot, hip));
5637
6534
  if (axis) {
5638
6535
  const acc = [0, 0, 0];
5639
6536
  for (let j = 1;j < legFull.length - 1; j++) {
5640
- const v = sub(target.bind.worldPos[legFull[j]], hip);
6537
+ const v = sub2(target.bind.worldPos[legFull[j]], hip);
5641
6538
  const d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
5642
6539
  acc[0] += v[0] - axis[0] * d;
5643
6540
  acc[1] += v[1] - axis[1] * d;
5644
6541
  acc[2] += v[2] - axis[2] * d;
5645
6542
  }
5646
- if (len(acc) > 0.001 * tlen)
6543
+ if (len2(acc) > 0.001 * tlen)
5647
6544
  pole = unit(acc);
5648
6545
  }
5649
6546
  }
@@ -5652,6 +6549,9 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5652
6549
  effector: e,
5653
6550
  donorHip: dfull[0],
5654
6551
  donorEffector: dfull[dfull.length - 1],
6552
+ contact: e,
6553
+ donorContact: dfull[dfull.length - 1],
6554
+ contactRatio: 1,
5655
6555
  lengths,
5656
6556
  pole,
5657
6557
  ratio: tlen / dlen,
@@ -5660,34 +6560,42 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5660
6560
  baseTarget: 0,
5661
6561
  baseDonor: 0,
5662
6562
  stanceTarget: [0, 0, 0],
5663
- stanceDonor: [0, 0, 0]
6563
+ stanceDonor: [0, 0, 0],
6564
+ refReach: 0
5664
6565
  });
5665
6566
  }
5666
6567
  return legs;
6568
+ }, twistAbout = (q, a) => {
6569
+ const d = q[0] * a[0] + q[1] * a[1] + q[2] * a[2];
6570
+ const ang = 2 * Math.atan2(d, q[3]);
6571
+ return ang > Math.PI ? ang - 2 * Math.PI : ang < -Math.PI ? ang + 2 * Math.PI : ang;
6572
+ }, axisAngle = (a, ang) => {
6573
+ const s = Math.sin(ang / 2);
6574
+ return [a[0] * s, a[1] * s, a[2] * s, Math.cos(ang / 2)];
5667
6575
  }, fabrik = (p, seg, goal, iterations = 48) => {
5668
6576
  const n = p.length;
5669
6577
  const root = p[0];
5670
6578
  const total = seg.reduce((a, b) => a + b, 0);
5671
- const toGoal = sub(goal, root);
5672
- const dist = len(toGoal);
6579
+ const toGoal = sub2(goal, root);
6580
+ const dist = len2(toGoal);
5673
6581
  if (dist >= total * 0.9995) {
5674
6582
  const d = unit(toGoal) ?? [0, -1, 0];
5675
6583
  for (let i = 1;i < n; i++)
5676
- p[i] = add(p[i - 1], scl(d, seg[i - 1] * 0.9995));
6584
+ p[i] = add(p[i - 1], scl2(d, seg[i - 1] * 0.9995));
5677
6585
  return;
5678
6586
  }
5679
6587
  for (let it = 0;it < iterations; it++) {
5680
6588
  p[n - 1] = goal;
5681
6589
  for (let i = n - 2;i >= 0; i--) {
5682
- const d = unit(sub(p[i], p[i + 1])) ?? [0, 1, 0];
5683
- p[i] = add(p[i + 1], scl(d, seg[i]));
6590
+ const d = unit(sub2(p[i], p[i + 1])) ?? [0, 1, 0];
6591
+ p[i] = add(p[i + 1], scl2(d, seg[i]));
5684
6592
  }
5685
6593
  p[0] = root;
5686
6594
  for (let i = 1;i < n; i++) {
5687
- const d = unit(sub(p[i], p[i - 1])) ?? [0, -1, 0];
5688
- p[i] = add(p[i - 1], scl(d, seg[i - 1]));
6595
+ const d = unit(sub2(p[i], p[i - 1])) ?? [0, -1, 0];
6596
+ p[i] = add(p[i - 1], scl2(d, seg[i - 1]));
5689
6597
  }
5690
- if (len(sub(p[n - 1], goal)) < total * 0.000001)
6598
+ if (len2(sub2(p[n - 1], goal)) < total * 0.000001)
5691
6599
  break;
5692
6600
  }
5693
6601
  }, donorReference = (donor, ref2, fps) => {
@@ -5733,7 +6641,7 @@ var DEG, IDENT, analyzeFacing = (rig, boneNames) => {
5733
6641
  pos[i] = target.bind.worldPos[i];
5734
6642
  continue;
5735
6643
  }
5736
- const off = sub(target.bind.worldPos[i], target.bind.worldPos[p]);
6644
+ const off = sub2(target.bind.worldPos[i], target.bind.worldPos[p]);
5737
6645
  const local = qrot(qinv(target.bind.worldRot[p]), off);
5738
6646
  pos[i] = add(pos[p], qrot(worldRot[p], local));
5739
6647
  }
@@ -5748,6 +6656,7 @@ var init_retarget = __esm(() => {
5748
6656
  init_rig();
5749
6657
  DEG = 180 / Math.PI;
5750
6658
  IDENT = [0, 0, 0, 1];
6659
+ FOOT_PITCH_CAP = 20 * Math.PI / 180;
5751
6660
  });
5752
6661
 
5753
6662
  // src/retarget/unityAnim.ts
@@ -5855,9 +6764,9 @@ function attachUnityClips(rig, clips) {
5855
6764
  }
5856
6765
  return res;
5857
6766
  };
5858
- const rot = bind(uc.rotation, 4), pos = bind(uc.position, 3), scl2 = bind(uc.scale, 3);
6767
+ const rot = bind(uc.rotation, 4), pos = bind(uc.position, 3), scl3 = bind(uc.scale, 3);
5859
6768
  const total = uc.rotation.length + uc.position.length + uc.scale.length;
5860
- const bound = rot.length + pos.length + scl2.length;
6769
+ const bound = rot.length + pos.length + scl3.length;
5861
6770
  const unbound = [...uc.rotation, ...uc.position, ...uc.scale].filter((c) => !rig.index.has(c.bone)).map((c) => c.bone);
5862
6771
  report.push({ name: uc.name, bound, total, unbound: [...new Set(unbound)] });
5863
6772
  const base = rig.bind.local;
@@ -5870,7 +6779,7 @@ function attachUnityClips(rig, clips) {
5870
6779
  pose.r[node] = qnorm(sample(c, time, 4));
5871
6780
  for (const { node, c } of pos)
5872
6781
  pose.t[node] = sample(c, time, 3);
5873
- for (const { node, c } of scl2)
6782
+ for (const { node, c } of scl3)
5874
6783
  pose.s[node] = sample(c, time, 3);
5875
6784
  return pose;
5876
6785
  }
@@ -5907,6 +6816,625 @@ var init_unityAnim = __esm(() => {
5907
6816
  };
5908
6817
  });
5909
6818
 
6819
+ // src/retarget/humanoid.ts
6820
+ var exports_humanoid = {};
6821
+ __export(exports_humanoid, {
6822
+ parseUnityAvatar: () => parseUnityAvatar,
6823
+ mapAvatars: () => mapAvatars,
6824
+ isHumanBone: () => isHumanBone,
6825
+ isBiped: () => isBiped,
6826
+ describeHumanoidMap: () => describeHumanoidMap,
6827
+ describeBiped: () => describeBiped,
6828
+ classifyBiped: () => classifyBiped,
6829
+ avatarToFile: () => avatarToFile,
6830
+ avatarFromUnity: () => avatarFromUnity,
6831
+ avatarFromRig: () => avatarFromRig,
6832
+ avatarFromFile: () => avatarFromFile,
6833
+ avatarFromBiped: () => avatarFromBiped,
6834
+ SIDES: () => SIDES,
6835
+ PHALANGES: () => PHALANGES,
6836
+ HUMAN_BONES: () => HUMAN_BONES,
6837
+ FINGERS: () => FINGERS
6838
+ });
6839
+ var SIDES, FINGERS, PHALANGES, BODY, LIMB, HUMAN_BONES, HUMAN_SET, isHumanBone = (s) => HUMAN_SET.has(s), parseUnityAvatar = (yaml) => {
6840
+ const bones = {};
6841
+ const unknown = [];
6842
+ const unq = (s) => s.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
6843
+ const re = /boneName:\s*([^\n]+)\n\s*humanName:\s*([^\n]+)/g;
6844
+ for (let m = re.exec(yaml);m; m = re.exec(yaml)) {
6845
+ const bone = unq(m[1]), human = unq(m[2]);
6846
+ if (isHumanBone(human))
6847
+ bones[human] = bone;
6848
+ else
6849
+ unknown.push(human);
6850
+ }
6851
+ const rm = /rootMotionBoneName:\s*([^\n]*)/.exec(yaml);
6852
+ const rootMotionBone = rm && unq(rm[1]) ? unq(rm[1]) : null;
6853
+ return { bones, rootMotionBone, unknown };
6854
+ }, sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2], len3 = (v) => Math.hypot(v[0], v[1], v[2]), norm = (v) => {
6855
+ const l = len3(v) || 1;
6856
+ return [v[0] / l, v[1] / l, v[2] / l];
6857
+ }, cross2 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]], angleDeg = (a, b) => Math.acos(Math.max(-1, Math.min(1, dot(norm(a), norm(b))))) * 180 / Math.PI, TOKEN, classifyBiped = (rig) => {
6858
+ const notes = [];
6859
+ const N = rig.names.length;
6860
+ const pos = rig.bind.worldPos;
6861
+ const skinned = (i) => rig.bind.fromSkin[i];
6862
+ const children = rig.names.map(() => []);
6863
+ rig.parents.forEach((p, i) => {
6864
+ if (p >= 0)
6865
+ children[p].push(i);
6866
+ });
6867
+ const skids = (i) => {
6868
+ const out = [];
6869
+ const q = [...children[i]];
6870
+ while (q.length) {
6871
+ const c = q.shift();
6872
+ if (skinned(c))
6873
+ out.push(c);
6874
+ else
6875
+ q.push(...children[c]);
6876
+ }
6877
+ return out;
6878
+ };
6879
+ const sparent = (i) => {
6880
+ let p = rig.parents[i];
6881
+ while (p >= 0 && !skinned(p))
6882
+ p = rig.parents[p];
6883
+ return p;
6884
+ };
6885
+ const joints = rig.order.filter(skinned);
6886
+ if (joints.length < 12)
6887
+ throw new Error(`${rig.label}: ${joints.length} skinned joints — not a humanoid`);
6888
+ const subtree = (i) => {
6889
+ const out = [];
6890
+ const q = [i];
6891
+ while (q.length) {
6892
+ const j = q.shift();
6893
+ out.push(j);
6894
+ q.push(...skids(j));
6895
+ }
6896
+ return out;
6897
+ };
6898
+ const minY = (i) => Math.min(...subtree(i).map((j) => pos[j][1]));
6899
+ const maxY = (i) => Math.max(...subtree(i).map((j) => pos[j][1]));
6900
+ const extent = (i) => {
6901
+ const s = subtree(i);
6902
+ let e = 0;
6903
+ for (const j of s)
6904
+ e = Math.max(e, len3(sub3(pos[j], pos[i])));
6905
+ return e;
6906
+ };
6907
+ let lo = Infinity, hi = -Infinity;
6908
+ for (const i of joints) {
6909
+ lo = Math.min(lo, pos[i][1]);
6910
+ hi = Math.max(hi, pos[i][1]);
6911
+ }
6912
+ const height = hi - lo || 1, ground = lo;
6913
+ const up = [0, 1, 0];
6914
+ const horiz = (v) => [v[0], 0, v[2]];
6915
+ const descend = (from) => {
6916
+ const chain = [from];
6917
+ for (let guard = 0;guard < 64; guard++) {
6918
+ const ks = skids(chain[chain.length - 1]);
6919
+ if (!ks.length)
6920
+ break;
6921
+ chain.push(ks.sort((a, b) => minY(a) - minY(b))[0]);
6922
+ }
6923
+ return chain;
6924
+ };
6925
+ let hips = -1;
6926
+ let legChains = [];
6927
+ for (const h2 of joints) {
6928
+ const cands = skids(h2).map((c) => descend(c)).filter((ch) => ch.length >= 3 && minY(ch[0]) < ground + 0.12 * height && len3(horiz(sub3(pos[ch[0]], pos[h2]))) > 0.02 * height);
6929
+ let best = null, bestScore = -Infinity;
6930
+ for (let a = 0;a < cands.length; a++)
6931
+ for (let b = a + 1;b < cands.length; b++) {
6932
+ const oa = horiz(sub3(pos[cands[a][0]], pos[h2])), ob = horiz(sub3(pos[cands[b][0]], pos[h2]));
6933
+ if (dot(oa, ob) >= 0)
6934
+ continue;
6935
+ const score = cands[a].length + cands[b].length + len3(sub3(oa, ob)) / height * 10;
6936
+ if (score > bestScore) {
6937
+ bestScore = score;
6938
+ best = [cands[a], cands[b]];
6939
+ }
6940
+ }
6941
+ if (!best)
6942
+ continue;
6943
+ if (hips < 0 || pos[h2][1] < pos[hips][1]) {
6944
+ hips = h2;
6945
+ legChains = best;
6946
+ }
6947
+ }
6948
+ if (hips < 0)
6949
+ throw new Error(`${rig.label}: no pelvis with two legs reaching the ground — not a biped`);
6950
+ const legJoints = new Set(legChains.flat());
6951
+ const centreX = pos[hips];
6952
+ const handScore = (i) => {
6953
+ if (legJoints.has(i) || i === hips)
6954
+ return -1;
6955
+ const ks = skids(i).filter((c) => subtree(c).length <= 6 && extent(c) < 0.12 * height);
6956
+ if (ks.length < 3)
6957
+ return -1;
6958
+ const lat = len3(horiz(sub3(pos[i], centreX)));
6959
+ return lat > 0.12 * height ? lat : -1;
6960
+ };
6961
+ const handCands = joints.filter((i) => handScore(i) > 0);
6962
+ const ancestors = (i) => {
6963
+ const out = [];
6964
+ for (let a = i;a >= 0; a = sparent(a))
6965
+ out.push(a);
6966
+ return out;
6967
+ };
6968
+ const lca = (list) => {
6969
+ let common = ancestors(list[0]);
6970
+ for (const i of list.slice(1)) {
6971
+ const set = new Set(ancestors(i));
6972
+ common = common.filter((a) => set.has(a));
6973
+ }
6974
+ return common[0];
6975
+ };
6976
+ const footOf = (chain) => {
6977
+ const last = chain[chain.length - 1];
6978
+ if (chain.length >= 5) {
6979
+ const ball = chain[chain.length - 2], ankle = chain[chain.length - 3];
6980
+ const endSite = /(_end|end|nub|tip)$/i.test(rig.names[last]) && children[last].length === 0;
6981
+ if ((endSite || pos[ball][1] - ground < 0.03 * height) && pos[ball][1] - ground < 0.06 * height && pos[ankle][1] - ground < 0.14 * height)
6982
+ return { foot: ankle, toes: ball };
6983
+ }
6984
+ if (chain.length >= 4 && pos[last][1] - ground < 0.06 * height && pos[chain[chain.length - 2]][1] - ground < 0.14 * height)
6985
+ return { foot: chain[chain.length - 2], toes: last };
6986
+ return { foot: last, toes: null };
6987
+ };
6988
+ let forward = [0, 0, 1];
6989
+ {
6990
+ const dirs = legChains.map(footOf).filter((f) => f.toes !== null).map((f) => horiz(sub3(pos[f.toes], pos[f.foot])));
6991
+ if (dirs.length)
6992
+ forward = norm(dirs.reduce((a, d) => [a[0] + d[0], 0, a[2] + d[2]], [0, 0, 0]));
6993
+ else
6994
+ notes.push("no toe bones — facing assumed +Z");
6995
+ }
6996
+ let lateral = norm(cross2(up, forward));
6997
+ const latOf = (i) => dot(sub3(pos[i], pos[hips]), lateral);
6998
+ const handOn = (sign) => {
6999
+ const side = handCands.filter((i) => Math.sign(latOf(i)) === sign).sort((a, b) => Math.abs(latOf(b)) - Math.abs(latOf(a)));
7000
+ if (side.length)
7001
+ return side[0];
7002
+ const leaves = joints.filter((i) => !legJoints.has(i) && skids(i).length === 0 && Math.sign(latOf(i)) === sign && Math.abs(latOf(i)) > 0.15 * height && pos[i][1] > pos[hips][1]).sort((a, b) => Math.abs(latOf(b)) - Math.abs(latOf(a)));
7003
+ if (leaves.length) {
7004
+ notes.push(`no finger chains on the ${sign > 0 ? "left" : "right"} — hand = furthest lateral joint ${rig.names[leaves[0]]}`);
7005
+ return leaves[0];
7006
+ }
7007
+ return null;
7008
+ };
7009
+ const handL = handOn(1), handR = handOn(-1);
7010
+ if (handL === null || handR === null)
7011
+ throw new Error(`${rig.label}: ${handL === null ? "left" : "right"} hand not found — not a biped`);
7012
+ const upTo = (from, stop) => {
7013
+ const out = [];
7014
+ for (let a = from;a >= 0 && !stop(a); a = sparent(a))
7015
+ out.unshift(a);
7016
+ return out;
7017
+ };
7018
+ const chest = lca([handL, handR]);
7019
+ if (chest === hips || !ancestors(chest).includes(hips))
7020
+ throw new Error(`${rig.label}: arms meet at ${rig.names[chest]}, not above the pelvis — not a biped`);
7021
+ const spine = upTo(chest, (j) => j === hips);
7022
+ const spineSet = new Set(spine);
7023
+ const armOf = (hand, side) => {
7024
+ const path2 = upTo(hand, (j) => spineSet.has(j));
7025
+ const attach = sparent(path2[0]);
7026
+ const links = path2.slice(0, -1);
7027
+ if (links.length < 2)
7028
+ notes.push(`${side} arm has ${links.length} link(s) between ${rig.names[attach]} and the hand — check the map`);
7029
+ const lower = links[links.length - 1], upper = links[links.length - 2] ?? lower;
7030
+ const shoulder = links.length >= 3 ? links[0] : null;
7031
+ const extra = links.length > 3 ? links.slice(1, -2) : [];
7032
+ return { side, attach, shoulder, extra, upper, lower, hand, fingers: {}, metacarpals: [] };
7033
+ };
7034
+ const arms = [armOf(handL, "Left"), armOf(handR, "Right")];
7035
+ const armJoints = new Set(arms.flatMap((a) => [a.shoulder ?? -1, ...a.extra, a.upper, a.lower, a.hand].filter((i) => i >= 0)));
7036
+ const centred = (i) => Math.abs(latOf(i)) < 0.05 * height;
7037
+ const path = [];
7038
+ {
7039
+ let cur = chest;
7040
+ for (let guard = 0;guard < 64; guard++) {
7041
+ const ks = skids(cur).filter((c) => !armJoints.has(c) && !legJoints.has(c) && centred(c) && pos[c][1] > pos[cur][1] - 0.01 * height);
7042
+ if (!ks.length)
7043
+ break;
7044
+ cur = ks.sort((a, b) => maxY(b) - maxY(a))[0];
7045
+ path.push(cur);
7046
+ }
7047
+ }
7048
+ if (!path.length)
7049
+ throw new Error(`${rig.label}: nothing above ${rig.names[chest]} — no neck/head`);
7050
+ let h = path.length - 1;
7051
+ while (h > 0 && skids(path[h]).length < 2)
7052
+ h--;
7053
+ if (skids(path[h]).length < 2)
7054
+ h = path.length - 1;
7055
+ const named = path.findIndex((i) => TOKEN.head.test(rig.names[i]));
7056
+ if (named >= 0 && named !== h) {
7057
+ notes.push(`head: shape picked ${rig.names[path[h]]}, name says ${rig.names[path[named]]} — using the name`);
7058
+ h = named;
7059
+ }
7060
+ const head = path[h];
7061
+ const neck = path.slice(0, h);
7062
+ let eyes = null, jaw = null;
7063
+ {
7064
+ const kids = skids(head);
7065
+ const pair = kids.filter((c) => Math.abs(latOf(c) - latOf(head)) > 0.01 * height);
7066
+ const fwdOf = (c) => dot(sub3(pos[c], pos[head]), forward);
7067
+ let best = -Infinity;
7068
+ for (const a of pair)
7069
+ for (const b of pair) {
7070
+ if (latOf(a) - latOf(head) <= 0 || latOf(b) - latOf(head) >= 0)
7071
+ continue;
7072
+ if (Math.abs(pos[a][1] - pos[b][1]) > 0.03 * height)
7073
+ continue;
7074
+ const score = fwdOf(a) + fwdOf(b) + (TOKEN.eye.test(rig.names[a]) && TOKEN.eye.test(rig.names[b]) ? height : 0);
7075
+ if (score > best) {
7076
+ best = score;
7077
+ eyes = [a, b];
7078
+ }
7079
+ }
7080
+ if (eyes && best < 0.005 * height)
7081
+ eyes = null;
7082
+ const eyeSet = new Set(eyes ?? []);
7083
+ const byName = kids.find((c) => !eyeSet.has(c) && TOKEN.jaw.test(rig.names[c]));
7084
+ const j = byName ?? kids.filter((c) => !eyeSet.has(c) && Math.abs(latOf(c) - latOf(head)) < 0.03 * height && fwdOf(c) > 0.01 * height && pos[c][1] < pos[head][1] + 0.03 * height).sort((a, b) => pos[a][1] - pos[b][1])[0];
7085
+ if (j !== undefined)
7086
+ jaw = j;
7087
+ }
7088
+ const legs = legChains.map((chain) => {
7089
+ const { foot, toes } = footOf(chain);
7090
+ const body = chain.slice(0, chain.indexOf(foot));
7091
+ const side = latOf(chain[0]) > 0 ? "Left" : "Right";
7092
+ if (body.length < 2) {
7093
+ notes.push(`${side} leg has ${body.length} link(s) above the foot — check the map`);
7094
+ return { side, upper: body[0] ?? foot, extra: [], lower: body[body.length - 1] ?? foot, foot, toes, chain };
7095
+ }
7096
+ return { side, upper: body[0], extra: body.slice(1, -1), lower: body[body.length - 1], foot, toes, chain };
7097
+ }).sort((a, b) => (a.side === "Left" ? 0 : 1) - (b.side === "Left" ? 0 : 1));
7098
+ if (legs[0].side === legs[1].side)
7099
+ notes.push("both legs on the same side — facing is wrong");
7100
+ const rootAbove = ancestors(hips).slice(1);
7101
+ const root = rootAbove.length ? rootAbove[rootAbove.length - 1] : null;
7102
+ const fingerChain = (from) => {
7103
+ const chain = [from];
7104
+ for (let guard = 0;guard < 8; guard++) {
7105
+ const cur = chain[chain.length - 1];
7106
+ const ks = skids(cur);
7107
+ if (ks.length) {
7108
+ chain.push(ks.sort((a, b) => extent(b) - extent(a))[0]);
7109
+ continue;
7110
+ }
7111
+ const raw = children[cur].filter((c) => !skinned(c));
7112
+ if (chain.length < 3 && raw.length === 1 && !/end|nub|tip|leaf/i.test(rig.names[raw[0]]))
7113
+ chain.push(raw[0]);
7114
+ break;
7115
+ }
7116
+ return chain;
7117
+ };
7118
+ for (const arm of arms) {
7119
+ let chains = skids(arm.hand).filter((c) => subtree(c).length <= 6 && extent(c) < 0.12 * height).map(fingerChain);
7120
+ if (!chains.length)
7121
+ continue;
7122
+ const FINGER_TOKENS = ["thumb", "index", "middle", "ring", "little"];
7123
+ const nameOf = (c) => FINGER_TOKENS.find((t) => c.some((i) => TOKEN[t].test(rig.names[i])));
7124
+ const named2 = chains.map(nameOf);
7125
+ const namedSet = new Set(named2.filter(Boolean));
7126
+ let useNames = false;
7127
+ if (namedSet.size === 5 && named2.filter(Boolean).length === 5) {
7128
+ chains = chains.filter((_, k) => named2[k]);
7129
+ useNames = true;
7130
+ } else if (chains.length > 5) {
7131
+ chains = [...chains].sort((a, b) => b.length - a.length || extent(b[0]) - extent(a[0])).slice(0, 5);
7132
+ }
7133
+ const meta = [];
7134
+ const phal = chains.map((c) => c.length >= 4 ? (meta.push(c[0]), c.slice(1, 4)) : c.slice(0, 3));
7135
+ arm.metacarpals = meta;
7136
+ const byName = chains.map(nameOf);
7137
+ const assigned = {};
7138
+ if (useNames) {
7139
+ byName.forEach((t, k) => {
7140
+ assigned[t.charAt(0).toUpperCase() + t.slice(1)] = phal[k];
7141
+ });
7142
+ } else {
7143
+ const order = phal.map((c, k) => ({ k, d: len3(sub3(pos[c[0]], pos[arm.hand])) })).sort((a, b) => a.d - b.d);
7144
+ const thumb = order[0].k;
7145
+ const rest = phal.map((_, k) => k).filter((k) => k !== thumb);
7146
+ const mean = rest.reduce((a, k) => [a[0] + pos[phal[k][0]][0], a[1] + pos[phal[k][0]][1], a[2] + pos[phal[k][0]][2]], [0, 0, 0]).map((v) => v / (rest.length || 1));
7147
+ const axis = norm(sub3(pos[phal[thumb][0]], mean));
7148
+ rest.sort((a, b) => dot(sub3(pos[phal[b][0]], mean), axis) - dot(sub3(pos[phal[a][0]], mean), axis));
7149
+ assigned.Thumb = phal[thumb];
7150
+ const names = ["Index", "Middle", "Ring", "Little"];
7151
+ rest.slice(0, 4).forEach((k, n) => {
7152
+ assigned[names[n]] = phal[k];
7153
+ });
7154
+ if (phal.length !== 5)
7155
+ notes.push(`${arm.side} hand has ${phal.length} finger chains — identities by position`);
7156
+ else
7157
+ notes.push(`${arm.side} fingers identified by position (no finger names)`);
7158
+ }
7159
+ arm.fingers = assigned;
7160
+ }
7161
+ const twist = [];
7162
+ const limbLinks = [];
7163
+ for (const l of legs) {
7164
+ const seq = [l.upper, ...l.extra, l.lower, l.foot];
7165
+ for (let k = 0;k + 1 < seq.length; k++)
7166
+ limbLinks.push([seq[k], seq[k + 1]]);
7167
+ }
7168
+ for (const a of arms) {
7169
+ const seq = [...a.shoulder !== null ? [a.shoulder] : [], ...a.extra, a.upper, a.lower, a.hand];
7170
+ for (let k = 0;k + 1 < seq.length; k++)
7171
+ limbLinks.push([seq[k], seq[k + 1]]);
7172
+ }
7173
+ const structural = new Set([hips, ...spine, ...neck, head, ...jaw !== null ? [jaw] : [], ...eyes ?? [], ...legs.flatMap((l) => l.chain), ...armJoints, ...arms.flatMap((a) => [...a.metacarpals, ...Object.values(a.fingers).flat()]), ...root !== null ? [root] : []]);
7174
+ for (const [b, c] of limbLinks) {
7175
+ const dir = sub3(pos[c], pos[b]);
7176
+ for (const t of skids(b)) {
7177
+ if (structural.has(t) || subtree(t).length > 2)
7178
+ continue;
7179
+ const d = sub3(pos[t], pos[b]);
7180
+ const onAxis = len3(d) < 0.000001 || angleDeg(d, dir) < 25;
7181
+ if (onAxis && (TOKEN.twist.test(rig.names[t]) || subtree(t).length === 1)) {
7182
+ twist.push({ bone: t, of: b });
7183
+ structural.add(t);
7184
+ }
7185
+ }
7186
+ }
7187
+ const helpers = joints.filter((i) => !structural.has(i));
7188
+ if (eyes) {
7189
+ const eyeFwd = horiz(sub3([(pos[eyes[0]][0] + pos[eyes[1]][0]) / 2, 0, (pos[eyes[0]][2] + pos[eyes[1]][2]) / 2], pos[head]));
7190
+ if (len3(eyeFwd) > 0.005 * height && angleDeg(eyeFwd, forward) > 90)
7191
+ notes.push("eyes look the other way than the toes point — check facing");
7192
+ }
7193
+ lateral = norm(cross2(up, forward));
7194
+ return { root, hips, forward, lateral, up, height, ground, spine, chest, neck, head, jaw, eyes, legs, arms, twist, helpers, notes };
7195
+ }, isBiped = (rig) => {
7196
+ try {
7197
+ classifyBiped(rig);
7198
+ return true;
7199
+ } catch {
7200
+ return false;
7201
+ }
7202
+ }, avatarFromBiped = (rig, b) => {
7203
+ const n = (i) => rig.names[i];
7204
+ const bones = {};
7205
+ const bridged = [];
7206
+ bones.Hips = n(b.hips);
7207
+ const s = b.spine;
7208
+ if (s.length === 1)
7209
+ bones.Spine = n(s[0]);
7210
+ else if (s.length === 2) {
7211
+ bones.Spine = n(s[0]);
7212
+ bones.Chest = n(s[1]);
7213
+ } else if (s.length === 3) {
7214
+ bones.Spine = n(s[0]);
7215
+ bones.Chest = n(s[1]);
7216
+ bones.UpperChest = n(s[2]);
7217
+ } else if (s.length > 3) {
7218
+ const pos = rig.bind.worldPos;
7219
+ let acc = 0;
7220
+ const arc2 = s.map((i, k) => acc += len3(sub3(pos[i], pos[k ? s[k - 1] : b.hips])));
7221
+ const mid = arc2.slice(1, -1).map((v, k) => ({ k: k + 1, d: Math.abs(v - acc / 2) })).sort((a, c) => a.d - c.d)[0].k;
7222
+ bones.Spine = n(s[0]);
7223
+ bones.Chest = n(s[mid]);
7224
+ bones.UpperChest = n(s[s.length - 1]);
7225
+ s.forEach((i, k) => {
7226
+ if (k !== 0 && k !== mid && k !== s.length - 1)
7227
+ bridged.push(n(i));
7228
+ });
7229
+ }
7230
+ if (b.neck.length) {
7231
+ bones.Neck = n(b.neck[0]);
7232
+ bridged.push(...b.neck.slice(1).map(n));
7233
+ }
7234
+ bones.Head = n(b.head);
7235
+ if (b.jaw !== null)
7236
+ bones.Jaw = n(b.jaw);
7237
+ if (b.eyes) {
7238
+ bones.LeftEye = n(b.eyes[0]);
7239
+ bones.RightEye = n(b.eyes[1]);
7240
+ }
7241
+ for (const l of b.legs) {
7242
+ bones[`${l.side}UpperLeg`] = n(l.upper);
7243
+ bones[`${l.side}LowerLeg`] = n(l.lower);
7244
+ bones[`${l.side}Foot`] = n(l.foot);
7245
+ if (l.toes !== null)
7246
+ bones[`${l.side}Toes`] = n(l.toes);
7247
+ bridged.push(...l.extra.map(n));
7248
+ }
7249
+ for (const a of b.arms) {
7250
+ if (a.shoulder !== null)
7251
+ bones[`${a.side}Shoulder`] = n(a.shoulder);
7252
+ bones[`${a.side}UpperArm`] = n(a.upper);
7253
+ bones[`${a.side}LowerArm`] = n(a.lower);
7254
+ bones[`${a.side}Hand`] = n(a.hand);
7255
+ bridged.push(...a.extra.map(n), ...a.metacarpals.map(n));
7256
+ for (const f of FINGERS) {
7257
+ const chain = a.fingers[f];
7258
+ if (!chain)
7259
+ continue;
7260
+ chain.forEach((i, k) => {
7261
+ if (k < 3)
7262
+ bones[`${a.side} ${f} ${PHALANGES[k]}`] = n(i);
7263
+ });
7264
+ }
7265
+ }
7266
+ const byIndex = new Map;
7267
+ for (const [k, v] of Object.entries(bones))
7268
+ byIndex.set(rig.index.get(v), k);
7269
+ const twist = {};
7270
+ for (const t of b.twist) {
7271
+ const of = byIndex.get(t.of);
7272
+ if (of)
7273
+ twist[n(t.bone)] = of;
7274
+ }
7275
+ return { root: b.root !== null ? n(b.root) : n(b.hips), bones, twist, bridged, notes: [...b.notes] };
7276
+ }, avatarFromRig = (rig) => avatarFromBiped(rig, classifyBiped(rig)), avatarFromUnity = (rig, unity) => {
7277
+ const notes = [];
7278
+ const bones = {};
7279
+ for (const [k, v] of Object.entries(unity.bones)) {
7280
+ if (rig.index.has(v))
7281
+ bones[k] = v;
7282
+ else
7283
+ notes.push(`.meta maps ${k} to '${v}', which the rig does not have — dropped`);
7284
+ }
7285
+ if (unity.unknown.length)
7286
+ notes.push(`.meta human names not in the canon: ${unity.unknown.join(", ")}`);
7287
+ if (!bones.Hips)
7288
+ throw new Error(`${rig.label}: the .meta avatar has no Hips`);
7289
+ let shape = null;
7290
+ try {
7291
+ shape = classifyBiped(rig);
7292
+ } catch (e) {
7293
+ notes.push(`shape classification failed (${e.message}) — no twist helpers, root = skinned ancestor of Hips`);
7294
+ }
7295
+ const hips = rig.index.get(bones.Hips);
7296
+ let root = bones.Hips;
7297
+ if (unity.rootMotionBone && rig.index.has(unity.rootMotionBone))
7298
+ root = unity.rootMotionBone;
7299
+ else {
7300
+ let top = -1;
7301
+ for (let a = rig.parents[hips];a >= 0; a = rig.parents[a])
7302
+ if (rig.bind.fromSkin[a])
7303
+ top = a;
7304
+ if (top >= 0)
7305
+ root = rig.names[top];
7306
+ }
7307
+ const twist = {};
7308
+ if (shape) {
7309
+ const byIndex = new Map;
7310
+ for (const [k, v] of Object.entries(bones))
7311
+ byIndex.set(rig.index.get(v), k);
7312
+ for (const t of shape.twist) {
7313
+ const of = byIndex.get(t.of);
7314
+ if (of)
7315
+ twist[rig.names[t.bone]] = of;
7316
+ }
7317
+ }
7318
+ return { root, bones, twist, bridged: [], notes };
7319
+ }, avatarFromFile = (rig, file) => {
7320
+ const bones = {};
7321
+ const notes = [];
7322
+ for (const [k, v] of Object.entries(file.bones)) {
7323
+ if (!isHumanBone(k)) {
7324
+ notes.push(`'${k}' is not a canonical humanoid bone — ignored`);
7325
+ continue;
7326
+ }
7327
+ if (!rig.index.has(v)) {
7328
+ notes.push(`${k} → '${v}': the rig has no such bone — dropped`);
7329
+ continue;
7330
+ }
7331
+ bones[k] = v;
7332
+ }
7333
+ if (!bones.Hips)
7334
+ throw new Error(`${rig.label}: avatar has no Hips`);
7335
+ const twist = {};
7336
+ for (const [t, of] of Object.entries(file.twist ?? {}))
7337
+ if (isHumanBone(of) && rig.index.has(t))
7338
+ twist[t] = of;
7339
+ let transfer;
7340
+ if (file.transfer && typeof file.transfer === "object") {
7341
+ transfer = { ...file.transfer };
7342
+ if (transfer.align) {
7343
+ const align = {};
7344
+ for (const [k, v] of Object.entries(transfer.align)) {
7345
+ if (!isHumanBone(k)) {
7346
+ notes.push(`transfer.align: '${k}' is not a canonical bone — ignored`);
7347
+ continue;
7348
+ }
7349
+ if (v === "keep" || v === "align")
7350
+ align[k] = v;
7351
+ }
7352
+ transfer.align = align;
7353
+ }
7354
+ }
7355
+ return { root: file.root && rig.index.has(file.root) ? file.root : bones.Hips, bones, twist, bridged: [], notes, ...transfer ? { transfer } : {} };
7356
+ }, avatarToFile = (a) => ({ root: a.root, bones: { ...a.bones }, twist: Object.keys(a.twist).length ? { ...a.twist } : undefined, ...a.transfer && Object.keys(a.transfer).length ? { transfer: { ...a.transfer } } : {} }), mapAvatars = (target, donor) => {
7357
+ const map = {};
7358
+ const shared = [], targetOnly = [], donorOnly = [];
7359
+ for (const k of HUMAN_BONES) {
7360
+ const t = target.bones[k], d = donor.bones[k];
7361
+ if (t && d) {
7362
+ map[t] = d;
7363
+ shared.push(k);
7364
+ } else if (t)
7365
+ targetOnly.push(k);
7366
+ else if (d)
7367
+ donorOnly.push(k);
7368
+ }
7369
+ map[target.root] = donor.root;
7370
+ if (target.root !== target.bones.Hips && donor.root === donor.bones.Hips)
7371
+ map[target.bones.Hips] = donor.bones.Hips;
7372
+ const legs = SIDES.map((s) => target.bones[`${s}Foot`]).filter((x) => !!x);
7373
+ return { map, root: target.root, legs, shared, targetOnly, donorOnly };
7374
+ }, describeBiped = (rig, b) => {
7375
+ const n = (i) => i === null ? "-" : rig.names[i];
7376
+ const fingers = b.arms.map((a) => Object.keys(a.fingers).length).join("/");
7377
+ return `hips ${n(b.hips)}${b.root !== null ? ` (root ${n(b.root)})` : ""} · spine ${b.spine.length} · neck ${b.neck.length} · head ${n(b.head)} · jaw ${n(b.jaw)} · eyes ${b.eyes ? `${n(b.eyes[0])}/${n(b.eyes[1])}` : "-"} · arms ${b.arms.map((a) => `${a.side[0]}:${(a.shoulder !== null ? 1 : 0) + 2 + a.extra.length}`).join(" ")} · fingers ${fingers} · legs ${b.legs.map((l) => `${l.side[0]}:${2 + l.extra.length}${l.toes !== null ? "+toes" : ""}`).join(" ")} · twist ${b.twist.length} · helpers ${b.helpers.length} · facing [${b.forward.map((x) => x.toFixed(2)).join(",")}]`;
7378
+ }, describeHumanoidMap = (target, donor, hm) => {
7379
+ const lines = [];
7380
+ const pair = (k) => `${target.bones[k]}←${donor.bones[k]}`;
7381
+ const group = (label, keys) => {
7382
+ const have = keys.filter((k) => hm.shared.includes(k));
7383
+ if (have.length)
7384
+ lines.push(` ${label}: ${have.map(pair).join(", ")}`);
7385
+ };
7386
+ lines.push(` root: ${target.root}←${donor.root}`);
7387
+ group("torso", ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"]);
7388
+ for (const s of SIDES)
7389
+ group(`${s.toLowerCase()} arm`, [`${s}Shoulder`, `${s}UpperArm`, `${s}LowerArm`, `${s}Hand`]);
7390
+ for (const s of SIDES)
7391
+ group(`${s.toLowerCase()} leg`, [`${s}UpperLeg`, `${s}LowerLeg`, `${s}Foot`, `${s}Toes`]);
7392
+ for (const s of SIDES) {
7393
+ const fs = FINGERS.filter((f) => hm.shared.includes(`${s} ${f} Proximal`));
7394
+ if (fs.length)
7395
+ lines.push(` ${s.toLowerCase()} fingers: ${fs.map((f) => `${f.toLowerCase()} ×${PHALANGES.filter((p) => hm.shared.includes(`${s} ${f} ${p}`)).length}`).join(", ")}`);
7396
+ }
7397
+ if (hm.targetOnly.length)
7398
+ lines.push(` target only (kept at bind): ${hm.targetOnly.join(", ")}`);
7399
+ if (hm.donorOnly.length)
7400
+ lines.push(` donor only (dropped): ${hm.donorOnly.join(", ")}`);
7401
+ const tw = Object.entries(target.twist);
7402
+ if (tw.length)
7403
+ lines.push(` target twist helpers: ${tw.map(([b, of]) => `${b}→${of}`).join(", ")}`);
7404
+ if (target.bridged.length)
7405
+ lines.push(` target bridged: ${target.bridged.join(", ")}`);
7406
+ for (const n of target.notes)
7407
+ lines.push(` ⚠ target: ${n}`);
7408
+ for (const n of donor.notes)
7409
+ lines.push(` ⚠ donor: ${n}`);
7410
+ return lines;
7411
+ };
7412
+ var init_humanoid = __esm(() => {
7413
+ SIDES = ["Left", "Right"];
7414
+ FINGERS = ["Thumb", "Index", "Middle", "Ring", "Little"];
7415
+ PHALANGES = ["Proximal", "Intermediate", "Distal"];
7416
+ BODY = ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"];
7417
+ LIMB = ["Shoulder", "UpperArm", "LowerArm", "Hand", "UpperLeg", "LowerLeg", "Foot", "Toes"];
7418
+ HUMAN_BONES = [
7419
+ ...BODY,
7420
+ ...SIDES.flatMap((s) => LIMB.map((l) => `${s}${l}`)),
7421
+ ...SIDES.flatMap((s) => FINGERS.flatMap((f) => PHALANGES.map((p) => `${s} ${f} ${p}`)))
7422
+ ];
7423
+ HUMAN_SET = new Set(HUMAN_BONES);
7424
+ TOKEN = {
7425
+ head: /head/i,
7426
+ neck: /neck/i,
7427
+ jaw: /jaw/i,
7428
+ eye: /eye(?!.*(brow|lash|lid))/i,
7429
+ thumb: /thumb/i,
7430
+ index: /index/i,
7431
+ middle: /middle/i,
7432
+ ring: /ring(?!.*(ear|fore))/i,
7433
+ little: /pinky|little/i,
7434
+ twist: /twist|roll/i
7435
+ };
7436
+ });
7437
+
5910
7438
  // src/retarget/automap.ts
5911
7439
  var exports_automap = {};
5912
7440
  __export(exports_automap, {
@@ -5914,7 +7442,7 @@ __export(exports_automap, {
5914
7442
  classify: () => classify,
5915
7443
  autoMap: () => autoMap
5916
7444
  });
5917
- var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2], len2 = (v) => Math.hypot(v[0], v[1], v[2]), classify = (rig) => {
7445
+ var sub4 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2], len4 = (v) => Math.hypot(v[0], v[1], v[2]), classify = (rig) => {
5918
7446
  const notes = [];
5919
7447
  const skinned = (i) => rig.bind.fromSkin[i];
5920
7448
  const pos = rig.bind.worldPos;
@@ -5964,12 +7492,12 @@ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[
5964
7492
  const theta = 0.5 * Math.atan2(2 * sxz, sxx - szz);
5965
7493
  let forward = [Math.cos(theta), 0, Math.sin(theta)];
5966
7494
  const top = [...joints].sort((a, b) => pos[b][1] - pos[a][1]).slice(0, Math.max(1, Math.ceil(joints.length * 0.1)));
5967
- const topProj = top.reduce((a, i) => a + dot(sub2(pos[i], [cx, 0, cz]), forward), 0) / top.length;
7495
+ const topProj = top.reduce((a, i) => a + dot2(sub4(pos[i], [cx, 0, cz]), forward), 0) / top.length;
5968
7496
  if (topProj < 0)
5969
7497
  forward = [-forward[0], 0, -forward[2]];
5970
7498
  const lateral = [forward[2], 0, -forward[0]];
5971
- const fwd = (i) => dot(sub2(pos[i], pos[root]), forward);
5972
- const lat = (i) => dot(sub2(pos[i], pos[root]), lateral);
7499
+ const fwd = (i) => dot2(sub4(pos[i], pos[root]), forward);
7500
+ const lat = (i) => dot2(sub4(pos[i], pos[root]), lateral);
5973
7501
  const centre = (i) => Math.abs(lat(i)) < 0.03 * size;
5974
7502
  const legs = [];
5975
7503
  {
@@ -6114,7 +7642,7 @@ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], dot = (a, b) => a[
6114
7642
  const out = [];
6115
7643
  let acc = 0, prev = anchor;
6116
7644
  for (const c of chain) {
6117
- acc += len2(sub2(rig.bind.worldPos[c], rig.bind.worldPos[prev]));
7645
+ acc += len4(sub4(rig.bind.worldPos[c], rig.bind.worldPos[prev]));
6118
7646
  out.push(acc);
6119
7647
  prev = c;
6120
7648
  }
@@ -6560,16 +8088,16 @@ var matMul2 = (a, b) => {
6560
8088
  lowfront: { r: [1, 0, 0], u: [0, 0.87, -0.5], d: [0, 0.5, -0.87] }
6561
8089
  };
6562
8090
  const B = bases[VIEW] ?? bases.side;
6563
- const dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8091
+ const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
6564
8092
  const corners = [];
6565
8093
  for (let i = 0;i < 8; i++)
6566
8094
  corners.push([i & 1 ? hi[0] : lo[0], i & 2 ? hi[1] : lo[1], i & 4 ? hi[2] : lo[2]]);
6567
- const ru = corners.map((c) => [dot2(B.r, c), dot2(B.u, c)]);
8095
+ const ru = corners.map((c) => [dot3(B.r, c), dot3(B.u, c)]);
6568
8096
  const rlo = [Math.min(...ru.map((p) => p[0])), Math.min(...ru.map((p) => p[1]))];
6569
8097
  const rhi = [Math.max(...ru.map((p) => p[0])), Math.max(...ru.map((p) => p[1]))];
6570
8098
  const k = (CELL - 2 * PAD) / Math.max(rhi[0] - rlo[0], rhi[1] - rlo[1], 0.000000001);
6571
- const sx = (ox, q) => ox + PAD + (dot2(B.r, q) - rlo[0]) * k + (CELL - 2 * PAD - (rhi[0] - rlo[0]) * k) / 2;
6572
- const sy = (q) => HEAD + CELL - PAD - (dot2(B.u, q) - rlo[1]) * k;
8099
+ const sx = (ox, q) => ox + PAD + (dot3(B.r, q) - rlo[0]) * k + (CELL - 2 * PAD - (rhi[0] - rlo[0]) * k) / 2;
8100
+ const sy = (q) => HEAD + CELL - PAD - (dot3(B.u, q) - rlo[1]) * k;
6573
8101
  const W = CELL * NF, H = CELL + HEAD;
6574
8102
  const img = new Uint8Array(W * H * 4);
6575
8103
  for (let i = 0;i < W * H; i++) {
@@ -6595,7 +8123,7 @@ var matMul2 = (a, b) => {
6595
8123
  const nl = Math.hypot(nx, ny, nz) || 1;
6596
8124
  const shade = 0.22 + 0.78 * Math.abs(nx / nl * light[0] + ny / nl * light[1] + nz / nl * light[2]);
6597
8125
  const col = [Math.round(base[0] * shade), Math.round(base[1] * shade), Math.round(base[2] * shade)];
6598
- const sp = p.map((q) => [sx(ox, q), sy(q), -dot2(B.d, q)]);
8126
+ const sp = p.map((q) => [sx(ox, q), sy(q), -dot3(B.d, q)]);
6599
8127
  const minX = Math.max(ox, Math.floor(Math.min(sp[0][0], sp[1][0], sp[2][0]))), maxX = Math.min(ox + CELL - 1, Math.ceil(Math.max(sp[0][0], sp[1][0], sp[2][0])));
6600
8128
  const minY = Math.max(HEAD, Math.floor(Math.min(sp[0][1], sp[1][1], sp[2][1]))), maxY = Math.min(H - 1, Math.ceil(Math.max(sp[0][1], sp[1][1], sp[2][1])));
6601
8129
  const d = (sp[1][0] - sp[0][0]) * (sp[2][1] - sp[0][1]) - (sp[2][0] - sp[0][0]) * (sp[1][1] - sp[0][1]);
@@ -6666,8 +8194,146 @@ var init_filmstrip = __esm(() => {
6666
8194
  };
6667
8195
  });
6668
8196
 
8197
+ // src/terrainPack.ts
8198
+ var exports_terrainPack = {};
8199
+ __export(exports_terrainPack, {
8200
+ resolveTerrainLayer: () => resolveTerrainLayer,
8201
+ packTerrainTextures: () => packTerrainTextures
8202
+ });
8203
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync11, statSync as statSync6, unlinkSync } from "node:fs";
8204
+ import { basename as basename6, extname as extname4, join as join9, resolve as resolve5 } from "node:path";
8205
+ import { spawnSync as spawnSync4 } from "node:child_process";
8206
+ var IMAGE_EXT, resolveTerrainLayer = (input, normal, name) => {
8207
+ const p = resolve5(input);
8208
+ if (!existsSync10(p))
8209
+ throw new Error(`not found: ${input}`);
8210
+ if (statSync6(p).isDirectory()) {
8211
+ const files = readdirSync5(p).filter((f) => IMAGE_EXT.test(f));
8212
+ const albedo = files.find((f) => /albedo/i.test(f) && !/smooth/i.test(f)) ?? files.find((f) => /albedo|basecolor|base_color|diffuse|_d\./i.test(f));
8213
+ if (!albedo)
8214
+ throw new Error(`${input}: no albedo image (a file named *Albedo* / *BaseColor* / *Diffuse*)`);
8215
+ const nrm2 = normal ?? files.find((f) => /normal|_n\./i.test(f) && !/normalmap_?dx/i.test(f));
8216
+ return { name: name ?? cleanName(basename6(p)), albedo: join9(p, albedo), normal: nrm2 ? normal ? resolve5(normal) : join9(p, nrm2) : undefined };
8217
+ }
8218
+ if (!IMAGE_EXT.test(p))
8219
+ throw new Error(`${input}: not an image (png/jpg/webp/tiff/tga/bmp)`);
8220
+ let nrm = normal ? resolve5(normal) : undefined;
8221
+ if (!nrm) {
8222
+ const stem = p.slice(0, -extname4(p).length);
8223
+ for (const cand of [stem.replace(/albedo/i, "Normal"), stem.replace(/albedo/i, "normal"), `${stem}_Normal`, `${stem}_normal`, stem.replace(/_?(basecolor|diffuse|_d)$/i, "_Normal")]) {
8224
+ if (cand === stem)
8225
+ continue;
8226
+ const hit = [".png", ".jpg", ".jpeg", ".tga", ".webp", ".tif", ".tiff", ".bmp"].map((e) => cand + e).find((f) => existsSync10(f));
8227
+ if (hit) {
8228
+ nrm = hit;
8229
+ break;
8230
+ }
8231
+ }
8232
+ }
8233
+ return { name: name ?? cleanName(basename6(p, extname4(p)).replace(/[_-]?(albedo|basecolor|base_color|diffuse)$/i, "")), albedo: p, normal: nrm };
8234
+ }, cleanName = (s) => s.trim().replace(/\s+/g, "_").replace(/[^A-Za-z0-9_-]/g, "").toLowerCase() || "layer", loadRgba = async (path, size) => {
8235
+ const sharp3 = (await import("sharp")).default;
8236
+ let bytes2 = new Uint8Array(readFileSync11(path));
8237
+ const mime = sniffMime(bytes2, path);
8238
+ if (mime === "image/x-tga" || mime === "image/bmp") {
8239
+ const rgba = mime === "image/x-tga" ? decodeTga(bytes2) : decodeBmp(bytes2);
8240
+ if (!rgba)
8241
+ throw new Error(`${path}: could not decode the ${mime === "image/bmp" ? "BMP" : "TGA"}`);
8242
+ bytes2 = encodePng(rgba);
8243
+ }
8244
+ const img = sharp3(Buffer.from(bytes2));
8245
+ const meta = await img.metadata();
8246
+ const { data, info } = await img.resize(size, size, { fit: "fill", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
8247
+ return { data, width: info.width, height: info.height, sourceSize: [meta.width ?? 0, meta.height ?? 0] };
8248
+ }, encodeKtx2 = (toktx, png, ktx2, srgb, quality) => {
8249
+ const args = ["--t2", "--encode", "uastc", "--uastc_quality", String(quality), "--zcmp", "18", "--genmipmap", "--assign_oetf", srgb ? "srgb" : "linear", ktx2, png];
8250
+ const p = spawnSync4(toktx, args, { encoding: "utf8" });
8251
+ if (p.status !== 0)
8252
+ throw new Error(`toktx failed (${p.status ?? p.error?.message ?? "?"}) on ${basename6(png)}:
8253
+ ${(p.stderr || p.stdout || "").trim()}`);
8254
+ }, packTerrainTextures = async (layers, opts) => {
8255
+ if (layers.length === 0)
8256
+ throw new Error("no layers");
8257
+ if (layers.length > 4)
8258
+ throw new Error(`${layers.length} layers — a terrain material takes at most four`);
8259
+ const size = opts.size ?? 1024;
8260
+ if (!(size >= 16 && size <= 8192) || (size & size - 1) !== 0)
8261
+ throw new Error(`--size wants a power of two in 16..8192, got ${size}`);
8262
+ const log = opts.log ?? (() => {});
8263
+ const sharp3 = (await import("sharp")).default;
8264
+ const toktx = opts.png ? null : findToktx();
8265
+ if (toktx && !checkToktx(toktx)) {
8266
+ throw new Error("KTX-Software's toktx is needed for KTX2 output — install it (https://github.com/KhronosGroup/KTX-Software/releases, on PATH or KTX_TOKTX=<path>) or pass --png");
8267
+ }
8268
+ const seen = new Set;
8269
+ for (const l of layers) {
8270
+ if (seen.has(l.name))
8271
+ throw new Error(`two layers named "${l.name}" — pass --names`);
8272
+ seen.add(l.name);
8273
+ }
8274
+ mkdirSync4(opts.outDir, { recursive: true });
8275
+ const ext = opts.png ? ".png" : ".ktx2";
8276
+ const writeImage = async (raw, channels, out, srgb) => {
8277
+ let pipe = sharp3(raw.data, { raw: { width: raw.width, height: raw.height, channels: 4 } });
8278
+ if (channels === 3)
8279
+ pipe = pipe.removeAlpha();
8280
+ const png = opts.png ? out : out.slice(0, -ext.length) + ".png";
8281
+ await pipe.png({ compressionLevel: opts.png ? 9 : 1 }).toFile(png);
8282
+ if (toktx) {
8283
+ encodeKtx2(toktx, png, out, srgb, opts.quality ?? 1);
8284
+ unlinkSync(png);
8285
+ }
8286
+ };
8287
+ const result = { layers: [], normals: [], snippet: "" };
8288
+ const normalRaws = [];
8289
+ for (const l of layers) {
8290
+ const alb = await loadRgba(l.albedo, size);
8291
+ const file = join9(opts.outDir, l.name + ext);
8292
+ await writeImage(alb, 3, file, true);
8293
+ let nrm = null;
8294
+ if (l.normal) {
8295
+ if (!existsSync10(l.normal))
8296
+ throw new Error(`${l.name}: normal map not found: ${l.normal}`);
8297
+ nrm = await loadRgba(l.normal, size);
8298
+ }
8299
+ normalRaws.push(nrm);
8300
+ result.layers.push({ name: l.name, file, albedo: l.albedo, normal: l.normal ?? null, sourceSize: alb.sourceSize });
8301
+ log(` ${l.name.padEnd(16)} ${alb.sourceSize[0]}×${alb.sourceSize[1]} → ${size}² ${nrm ? `+ normal ${nrm.sourceSize[0]}×${nrm.sourceSize[1]}` : "(flat normal)"}`);
8302
+ }
8303
+ const packCount = layers.length > 2 ? 2 : 1;
8304
+ for (let k = 0;k < packCount; k++) {
8305
+ const a = normalRaws[k * 2] ?? null, b = normalRaws[k * 2 + 1] ?? null;
8306
+ const n = size * size;
8307
+ const out = Buffer.alloc(n * 4);
8308
+ for (let i = 0;i < n; i++) {
8309
+ const o = i * 4;
8310
+ out[o] = a ? a.data[o] : 128;
8311
+ out[o + 1] = a ? opts.flipY ? 255 - a.data[o + 1] : a.data[o + 1] : 128;
8312
+ out[o + 2] = b ? b.data[o] : 128;
8313
+ out[o + 3] = b ? opts.flipY ? 255 - b.data[o + 1] : b.data[o + 1] : 128;
8314
+ }
8315
+ const file = join9(opts.outDir, `normals${k * 2}${k * 2 + 1}${ext}`);
8316
+ await writeImage({ data: out, width: size, height: size }, 4, file, false);
8317
+ result.normals.push(file);
8318
+ log(` normals${k * 2}${k * 2 + 1}${"".padEnd(7)} ${a ? layers[k * 2].name : "flat"} + ${b ? layers[k * 2 + 1].name : layers[k * 2 + 1] ? "flat" : "—"}`);
8319
+ }
8320
+ const rel = (f) => "./" + basename6(f);
8321
+ result.snippet = [
8322
+ "layers: [",
8323
+ ...result.layers.map((l, i) => ` { albedo: asset('${rel(l.file)}'), tiling: 4${normalRaws[i] ? ", normalScale: 1" : ""} },`),
8324
+ `], normals: [ ${result.normals.map((f) => `asset('${rel(f)}')`).join(", ")} ]`
8325
+ ].join(`
8326
+ `);
8327
+ return result;
8328
+ };
8329
+ var init_terrainPack = __esm(() => {
8330
+ init_resolveImage();
8331
+ init_transcodeKtx2();
8332
+ IMAGE_EXT = /\.(png|jpe?g|webp|tiff?|tga|bmp)$/i;
8333
+ });
8334
+
6669
8335
  // src/cli.ts
6670
- import { existsSync as existsSync10, statSync as statSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "node:fs";
8336
+ import { existsSync as existsSync11, statSync as statSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync12, readdirSync as readdirSync6 } from "node:fs";
6671
8337
 
6672
8338
  // src/sky/index.ts
6673
8339
  import { spawnSync as spawnSync2 } from "node:child_process";
@@ -6952,7 +8618,7 @@ var formatSkyReport = (r, name = "SKY") => {
6952
8618
  };
6953
8619
 
6954
8620
  // src/cli.ts
6955
- import { basename as basename6, dirname as dirname7, extname as extname4, join as join9 } from "node:path";
8621
+ import { basename as basename7, dirname as dirname7, extname as extname5, join as join10, resolve as resolve6 } from "node:path";
6956
8622
  var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animation-plan.md §2.9)
6957
8623
 
6958
8624
  convert <model.fbx|.glb> [options] FBX (or an existing GLB) → LeCodes-ready GLB
@@ -6992,16 +8658,35 @@ var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animati
6992
8658
  "align": { bone: "keep" | "direction" | [ refBones ] },
6993
8659
  "legs": [ footBones ] } (align/legs optional)
6994
8660
  --skeletons print both bone lists and exit (to write that map)
6995
- --auto-map [out.json] | --map auto derive the map from the two skeletons' SHAPE (quadrupeds:
6996
- spine/neck/head/tail/legs/ears/jaw) — write it, or use it directly
8661
+ --auto-map [out.json] | --map auto derive the map from the two skeletons' SHAPE — write it, or use it
8662
+ directly. Quadrupeds: spine/neck/head/tail/legs/ears/jaw chains.
8663
+ Humanoids: both rigs' AVATARS (see \`avatar\`) joined on the canon
8664
+ --avatar <a.avatar.json|model.meta> the target's avatar instead of deriving it (a Unity .meta works)
8665
+ --donor-avatar <…> the donor's
8666
+ --donor-rig <model.fbx|.glb> the rig a skeleton-only clip file (mocap packs: nodes + curves,
8667
+ no mesh) was animated on — its clips bind to it by bone name
6997
8668
  --clips a b … only these donor clips --keep-clips keep the target's own
6998
8669
  --align none|bone-direction posture alignment (default bone-direction)
6999
- --ik none|legs foot placement against the ground (default legs)
8670
+ --ik none|legs|hands foot placement against the ground (default legs); hands = the
8671
+ hands as effectors too (clips where they meet or hold)
8672
+ --root-motion auto|hips|root|none where the travel lands: a root above the hips takes the horizontal
8673
+ travel, the hips the bob (auto/root); all on the hips; none = in place
8674
+ --no-plant no foot planting (a standing foot is otherwise held where it landed)
8675
+ --twist N|off twist helpers' share of their limb's axial turn (default 0.5)
7000
8676
  --donor-ref bind|auto|<clip> pose the donor's motion is measured from (auto = mean of its idle/walk)
7001
8677
  --fps N --root-scale N --prefix s sampling rate / root-motion scale / clip name prefix
7002
8678
  --clip-names n1 n2 … rename the produced clips, positionally
7003
8679
  --anim a.anim b.anim … Unity AnimationClip assets to use as the donor's clips
7004
8680
  (for packs shipping an empty FBX rig + .anim files)
8681
+ avatar <model.fbx|.glb> [--meta model.fbx.meta] [-o model.avatar.json]
8682
+ a humanoid rig's AVATAR: its bones named in the Unity/VRM canon
8683
+ (Hips, Spine…, LeftUpperArm…, Left Thumb Proximal…), read off the
8684
+ skeleton's shape — hips = the joint two ground-reaching chains hang
8685
+ from, hands = the lateral joints fanning into fingers, head = the
8686
+ last branch above the arms; twist/roll helpers recorded, not mapped.
8687
+ --meta checks the result against Unity's own humanoid map (and
8688
+ imports it verbatim when you trust it more: -o with --meta writes
8689
+ Unity's). The file is what \`retarget --avatar\` reads
7005
8690
  sky <env.exr|.hdr> equirectangular HDR → the TWO cubemaps a scene needs
7006
8691
  -o dir where they land (default: beside the input)
7007
8692
  --size N edge of the DRAWN sky cubemap (default 512)
@@ -7017,6 +8702,19 @@ var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animati
7017
8702
  can be aimed to match instead of by eye.
7018
8703
  Needs filament's cmgen (LECODES_CMGEN / ~/.lecodes/cmgen).
7019
8704
 
8705
+ terrain-pack <layer> [<layer> …] a terrain's splat layers (≤ 4) → <name>.ktx2 per layer + the PACKED
8706
+ normal maps normals01.ktx2 / normals23.ktx2 (layer XY pairs in
8707
+ RG/BA — terrain.mat has nine samplers, so normals ship two per
8708
+ texture). A layer = an albedo file (its normal guessed from the
8709
+ name, or --normals) or a FOLDER holding *Albedo* + *Normal*
8710
+ (Unity terrain packs). Prints the Terrain.create fragment
8711
+ -o dir where they land (default: ./terrain)
8712
+ --normals n0 n1 … normal map per layer, positionally ("-" = none)
8713
+ --names a b … layer names → file names (default: from the inputs)
8714
+ --size N output edge (1024) --quality N UASTC quality 0–4 (1)
8715
+ --png PNGs instead of KTX2 (no toktx needed)
8716
+ --flip-y DirectX-style normal maps (green inverted)
8717
+
7020
8718
  probe <file.fbx|.glb> skeleton, clips (+ per-clip root motion / first-frame-vs-rest),
7021
8719
  meshes, materials, textures, bind sanity, world size, duplicate names
7022
8720
  --filmstrip [out.png] (GLB) CPU-skinned PNG strip of every clip — the fastest "does it play
@@ -7035,8 +8733,12 @@ var USAGE = `lecodes-assets — FBX → GLB converter + GLB doctor (docs/animati
7035
8733
  [--unlit] flat base colour, no lighting (KHR_materials_unlit) — for stylised assets
7036
8734
  [--lightmap-uv] add a lightmap UV set (TEXCOORD_1, xatlas) — what \`lecodes lightmap bake\`
7037
8735
  samples; skinned meshes are skipped. --lightmap-padding N (4),
7038
- --lightmap-iterations N (1, more = better charts, slower)`;
7039
- var MULTI = new Set(["--clips", "--clip-names", "--rename", "--anim", "--only", "--keep", "--merge-skins"]);
8736
+ --lightmap-iterations N (1, more = better charts, slower)
8737
+ [--lod 0.5,0.2] [--lod-bones] add mesh LOD levels (<name>_LOD1... siblings at these triangle
8738
+ ratios, same skin; >= 2000-tri meshes only) - creator-gl shows one
8739
+ by screen size (docs/lod-plan.md); --lod-bones folds fingers /
8740
+ toes / twists onto their parents in the last level's skin`;
8741
+ var MULTI = new Set(["--clips", "--clip-names", "--rename", "--anim", "--only", "--keep", "--merge-skins", "--normals", "--names"]);
7040
8742
  var SINGLE = new Set([
7041
8743
  "-o",
7042
8744
  "--max-size",
@@ -7055,6 +8757,12 @@ var SINGLE = new Set([
7055
8757
  "--prefix",
7056
8758
  "--unity-mats",
7057
8759
  "--unity-index",
8760
+ "--avatar",
8761
+ "--donor-avatar",
8762
+ "--meta",
8763
+ "--root-motion",
8764
+ "--twist",
8765
+ "--donor-rig",
7058
8766
  "--split",
7059
8767
  "--list-nodes",
7060
8768
  "--filmstrip",
@@ -7072,7 +8780,9 @@ var SINGLE = new Set([
7072
8780
  "--name",
7073
8781
  "--ibl-name",
7074
8782
  "--lightmap-padding",
7075
- "--lightmap-iterations"
8783
+ "--lightmap-iterations",
8784
+ "--quality",
8785
+ "--lod"
7076
8786
  ]);
7077
8787
  var isValue = (s) => !s.startsWith("-") || /^-(\d|\.\d)/.test(s);
7078
8788
  var parse = (argv) => {
@@ -7105,7 +8815,7 @@ var cmdConvert = async (p) => {
7105
8815
  log(USAGE);
7106
8816
  return 2;
7107
8817
  }
7108
- if (!existsSync10(input)) {
8818
+ if (!existsSync11(input)) {
7109
8819
  console.error(`✗ not found: ${input}`);
7110
8820
  return 1;
7111
8821
  }
@@ -7124,7 +8834,7 @@ var cmdConvert = async (p) => {
7124
8834
  log(`\uD83D\uDD04 ${input}${clips.length ? ` + ${clips.length} clip file(s)` : ""}`);
7125
8835
  let doc;
7126
8836
  let report = { clips: [] };
7127
- if (extname4(input).toLowerCase() === ".fbx") {
8837
+ if (extname5(input).toLowerCase() === ".fbx") {
7128
8838
  const r = await convertFbx2(input, {
7129
8839
  clips,
7130
8840
  clipNames,
@@ -7146,8 +8856,8 @@ var cmdConvert = async (p) => {
7146
8856
  } else {
7147
8857
  doc = await makeIO3().read(input);
7148
8858
  for (let i = 0;i < clips.length; i++) {
7149
- const cs = await readFbx2(new Uint8Array(readFileSync11(clips[i])));
7150
- report.clips.push(...mergeClipScene2(doc, cs, basename6(clips[i]), clipNames?.[i], rename, warn));
8859
+ const cs = await readFbx2(new Uint8Array(readFileSync12(clips[i])));
8860
+ report.clips.push(...mergeClipScene2(doc, cs, basename7(clips[i]), clipNames?.[i], rename, warn));
7151
8861
  }
7152
8862
  }
7153
8863
  for (const c of report.clips)
@@ -7158,12 +8868,12 @@ var cmdConvert = async (p) => {
7158
8868
  const { convertFbx: convertFbx3 } = await Promise.resolve().then(() => (init_convert(), exports_convert));
7159
8869
  const donors = [];
7160
8870
  for (const f of mergeFiles) {
7161
- if (!existsSync10(f)) {
8871
+ if (!existsSync11(f)) {
7162
8872
  console.error(`✗ not found: ${f}`);
7163
8873
  return 1;
7164
8874
  }
7165
- const d = extname4(f).toLowerCase() === ".fbx" ? (await convertFbx3(f, { dropModelClips: true, warn, unity: p.flags.has("--no-unity") ? false : p.values.get("--unity-mats")?.[0] ?? true, unityIndex: p.values.get("--unity-index")?.[0] })).doc : await makeIO3().read(f);
7166
- donors.push({ doc: d, label: basename6(f) });
8875
+ const d = extname5(f).toLowerCase() === ".fbx" ? (await convertFbx3(f, { dropModelClips: true, warn, unity: p.flags.has("--no-unity") ? false : p.values.get("--unity-mats")?.[0] ?? true, unityIndex: p.values.get("--unity-index")?.[0] })).doc : await makeIO3().read(f);
8876
+ donors.push({ doc: d, label: basename7(f) });
7167
8877
  }
7168
8878
  const mr = await mergeSkins2(doc, donors, warn);
7169
8879
  for (const d of mr.donors)
@@ -7208,32 +8918,42 @@ var cmdConvert = async (p) => {
7208
8918
  log(` baked scale ×${+res.scaled.toPrecision(6)}`);
7209
8919
  if (res.lightmapUv)
7210
8920
  log(` lightmap UV: ${res.lightmapUv.charts} charts over ${res.lightmapUv.primitives} primitive(s) (xatlas ${res.lightmapUv.width}×${res.lightmapUv.height}); vertices ${res.lightmapUv.verticesBefore} → ${res.lightmapUv.verticesAfter}`);
7211
- }
7212
- const out = p.values.get("-o")?.[0] ?? join9(dirname7(input), basename6(input, extname4(input)) + ".glb");
8921
+ if (res.lod) {
8922
+ for (const t of res.lod.tris)
8923
+ log(` lod: ${t.name} ${t.levels.join(" / ")} tris`);
8924
+ for (const b of res.lod.bones)
8925
+ log(` lod bones: ${b.name} last level ${b.before} → ${b.after} joints`);
8926
+ for (const k of res.lod.skipped)
8927
+ log(` lod: ${k.name} level ${k.level} dropped — the simplifier reached only ${k.tris} tris (seams / hard surfaces; try a smaller ratio)`);
8928
+ if (!res.lod.nodes)
8929
+ log(` lod: no mesh with >= 2000 triangles (or the _LOD nodes are already there)`);
8930
+ }
8931
+ }
8932
+ const out = p.values.get("-o")?.[0] ?? join10(dirname7(input), basename7(input, extname5(input)) + ".glb");
7213
8933
  const { validateGlb: validateGlb2, formatReport: formatReport2 } = await Promise.resolve().then(() => (init_validateGlb(), exports_validateGlb));
7214
8934
  const { probeDocument: probeDocument2 } = await Promise.resolve().then(() => (init_probe(), exports_probe));
7215
8935
  if (p.values.has("--split")) {
7216
8936
  const { splitDoc: splitDoc2, fileStem: fileStem2 } = await Promise.resolve().then(() => (init_props(), exports_props));
7217
- const dir2 = p.values.get("--split")?.[0] ?? (p.values.has("-o") ? out.replace(/\.glb$/i, "") : join9(dirname7(input), basename6(input, extname4(input))));
7218
- if (!existsSync10(dir2))
7219
- mkdirSync4(dir2, { recursive: true });
8937
+ const dir2 = p.values.get("--split")?.[0] ?? (p.values.has("-o") ? out.replace(/\.glb$/i, "") : join10(dirname7(input), basename7(input, extname5(input))));
8938
+ if (!existsSync11(dir2))
8939
+ mkdirSync5(dir2, { recursive: true });
7220
8940
  const parts = await splitDoc2(doc, { ground: !p.flags.has("--no-ground") });
7221
8941
  let total = 0;
7222
8942
  for (const part of parts) {
7223
- const f = join9(dir2, fileStem2(part.name) + ".glb");
8943
+ const f = join10(dir2, fileStem2(part.name) + ".glb");
7224
8944
  const glb2 = await writeGlb2(part.doc);
7225
8945
  writeFileSync4(f, glb2);
7226
8946
  total += glb2.byteLength;
7227
8947
  const v = validateGlb2(glb2);
7228
- log(` ${v.ok ? "✅" : "✗"} ${basename6(f).padEnd(40)} ${part.size.map((x) => x.toFixed(2)).join("×")} m ${(glb2.byteLength / 1024).toFixed(0)} KB`);
8948
+ log(` ${v.ok ? "✅" : "✗"} ${basename7(f).padEnd(40)} ${part.size.map((x) => x.toFixed(2)).join("×")} m ${(glb2.byteLength / 1024).toFixed(0)} KB`);
7229
8949
  }
7230
8950
  log(`\uD83D\uDCBE wrote ${parts.length} file(s) → ${dir2} (${(total / 1024 / 1024).toFixed(2)} MB, ${((performance.now() - t0) / 1000).toFixed(1)}s)`);
7231
8951
  return 0;
7232
8952
  }
7233
8953
  const glb = await writeGlb2(doc);
7234
8954
  const dir = dirname7(out);
7235
- if (dir && !existsSync10(dir))
7236
- mkdirSync4(dir, { recursive: true });
8955
+ if (dir && !existsSync11(dir))
8956
+ mkdirSync5(dir, { recursive: true });
7237
8957
  writeFileSync4(out, glb);
7238
8958
  log(formatReport2(validateGlb2(glb), out));
7239
8959
  log(probeDocument2(doc, out).split(`
@@ -7258,6 +8978,7 @@ var real = (p, key) => {
7258
8978
  var scaleOpts = (p) => ({ scale: real(p, "--scale"), scaleTo: real(p, "--scale-to"), fixScale: p.flags.has("--fix-scale") });
7259
8979
  var shadingOpts = (p) => ({ unlit: p.flags.has("--unlit"), fixEmissive: p.flags.has("--fix-emissive") });
7260
8980
  var lightmapOpts = (p) => ({ lightmapUv: p.flags.has("--lightmap-uv"), lightmapPadding: num2(p, "--lightmap-padding"), lightmapIterations: num2(p, "--lightmap-iterations") });
8981
+ var lodOpts = (p) => ({ lod: (p.values.get("--lod") ?? []).flatMap((v) => v.split(",")).map(Number).filter((n) => n > 0 && n < 1), lodBones: p.flags.has("--lod-bones") });
7261
8982
  var cmdRetarget = async (p) => {
7262
8983
  const input = p.positionals[0];
7263
8984
  const donorPath = p.values.get("--from")?.[0];
@@ -7266,27 +8987,41 @@ var cmdRetarget = async (p) => {
7266
8987
  return 2;
7267
8988
  }
7268
8989
  for (const f of [input, donorPath])
7269
- if (!existsSync10(f)) {
8990
+ if (!existsSync11(f)) {
7270
8991
  console.error(`✗ not found: ${f}`);
7271
8992
  return 1;
7272
8993
  }
7273
- const { loadRig: loadRig2, rigFromDocument: rigFromDocument2, checkUniformScale: checkUniformScale2 } = await Promise.resolve().then(() => (init_rig(), exports_rig));
8994
+ const { loadRig: loadRig2, rigFromDocument: rigFromDocument2, checkUniformScale: checkUniformScale2, hasSkin: hasSkin2, rebindClips: rebindClips2 } = await Promise.resolve().then(() => (init_rig(), exports_rig));
7274
8995
  const { retarget: retarget2, bindAccessors: bindAccessors2, analyzeFacing: analyzeFacing2, facingMismatch: facingMismatch2 } = await Promise.resolve().then(() => (init_retarget(), exports_retarget));
7275
8996
  const { makeIO: makeIO3, writeGlb: writeGlb2 } = await Promise.resolve().then(() => (init_convert(), exports_convert));
7276
8997
  const { probeDocument: probeDocument2 } = await Promise.resolve().then(() => (init_probe(), exports_probe));
7277
8998
  const doc = await makeIO3().read(input);
7278
- const target = rigFromDocument2(doc, basename6(input));
7279
- let donor = donorPath.toLowerCase().endsWith(".fbx") ? await loadRig2(donorPath) : rigFromDocument2(await makeIO3().read(donorPath), basename6(donorPath));
8999
+ const target = rigFromDocument2(doc, basename7(input));
9000
+ const loadAny = async (path) => path.toLowerCase().endsWith(".fbx") ? loadRig2(path) : rigFromDocument2(await makeIO3().read(path), basename7(path));
9001
+ let donor = await loadAny(donorPath);
9002
+ const donorRigPath = p.values.get("--donor-rig")?.[0];
9003
+ if (donorRigPath) {
9004
+ if (!existsSync11(donorRigPath)) {
9005
+ console.error(`✗ not found: ${donorRigPath}`);
9006
+ return 1;
9007
+ }
9008
+ const r = rebindClips2(await loadAny(donorRigPath), donor);
9009
+ log(` \uD83D\uDCCE ${basename7(donorPath)}: ${r.bound}/${r.total} bones bound to ${basename7(donorRigPath)} by name${Math.abs(r.scale - 1) > 0.01 ? ` · the clip's skeleton is ×${(1 / r.scale).toFixed(2)} the rig's size (rotations taken, lengths the rig's)` : ""}${r.unbound.length ? ` (no bone: ${r.unbound.slice(0, 3).join(", ")}${r.unbound.length > 3 ? ` +${r.unbound.length - 3}` : ""})` : ""}`);
9010
+ donor = r.rig;
9011
+ } else if (!hasSkin2(donor) && !p.flags.has("--skeletons")) {
9012
+ console.error(`✗ ${basename7(donorPath)} carries no skin (a clip-only file) — name the rig it was animated on: --donor-rig <model.fbx|.glb>`);
9013
+ return 2;
9014
+ }
7280
9015
  const animFiles = p.values.get("--anim") ?? [];
7281
9016
  if (animFiles.length) {
7282
9017
  const { parseUnityAnim: parseUnityAnim2, attachUnityClips: attachUnityClips2 } = await Promise.resolve().then(() => (init_unityAnim(), exports_unityAnim));
7283
9018
  const parsed = [];
7284
9019
  for (const f of animFiles) {
7285
- if (!existsSync10(f)) {
9020
+ if (!existsSync11(f)) {
7286
9021
  console.error(`✗ not found: ${f}`);
7287
9022
  return 1;
7288
9023
  }
7289
- parsed.push(parseUnityAnim2(readFileSync11(f, "utf8"), basename6(f).replace(/\.anim$/i, "")));
9024
+ parsed.push(parseUnityAnim2(readFileSync12(f, "utf8"), basename7(f).replace(/\.anim$/i, "")));
7290
9025
  }
7291
9026
  const { clips: attached, report } = attachUnityClips2(donor, parsed);
7292
9027
  donor = { ...donor, clips: [...donor.clips, ...attached] };
@@ -7310,31 +9045,76 @@ var cmdRetarget = async (p) => {
7310
9045
  };
7311
9046
  const a = col(target), b = col(donor);
7312
9047
  log(`
7313
- ${`TARGET ${basename6(input)} (${target.names.length})`.padEnd(46)}DONOR ${basename6(donorPath)} (${donor.names.length})`);
9048
+ ${`TARGET ${basename7(input)} (${target.names.length})`.padEnd(46)}DONOR ${basename7(donorPath)} (${donor.names.length})`);
7314
9049
  log(`${"(* = bound by a skin)".padEnd(46)}(* = bound by a skin)`);
7315
9050
  for (let i = 0;i < Math.max(a.length, b.length); i++)
7316
9051
  log(`${(a[i] ?? "").slice(0, 44).padEnd(46)}${b[i] ?? ""}`);
7317
9052
  log(`
7318
9053
  donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).join(", ") || "none"}`);
9054
+ const { classifyBiped: classifyBiped3, describeBiped: describeBiped3 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9055
+ for (const [label, rig] of [["target", target], ["donor", donor]]) {
9056
+ try {
9057
+ log(`${label} reads as a humanoid: ${describeBiped3(rig, classifyBiped3(rig))}`);
9058
+ } catch {}
9059
+ }
7319
9060
  return 0;
7320
9061
  }
9062
+ const { avatarFromRig: avatarFromRig2, avatarFromUnity: avatarFromUnity2, avatarFromFile: avatarFromFile2, parseUnityAvatar: parseUnityAvatar2, mapAvatars: mapAvatars2, describeHumanoidMap: describeHumanoidMap2, describeBiped: describeBiped2, classifyBiped: classifyBiped2, isBiped: isBiped2 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9063
+ const loadAvatar = (rig, file) => {
9064
+ if (!file)
9065
+ return isBiped2(rig) ? avatarFromRig2(rig) : null;
9066
+ if (!existsSync11(file))
9067
+ throw new Error(`not found: ${file}`);
9068
+ const text = readFileSync12(file, "utf8");
9069
+ return /\.meta$/i.test(file) ? avatarFromUnity2(rig, parseUnityAvatar2(text)) : avatarFromFile2(rig, JSON.parse(text));
9070
+ };
9071
+ let humanoid;
7321
9072
  const mapPath = p.values.get("--map")?.[0];
7322
9073
  const autoOut = p.values.get("--auto-map");
7323
9074
  let spec;
7324
- if (autoOut !== undefined || mapPath === "auto") {
7325
- const { autoMap: autoMap2, describeSkeleton: describeSkeleton2 } = await Promise.resolve().then(() => exports_automap);
7326
- const am = autoMap2(target, donor);
7327
- log(`\uD83E\uDDED auto-map ${basename6(input)} ← ${basename6(donorPath)}`);
7328
- log(` target: ${describeSkeleton2(target, am.target)}`);
7329
- log(` donor: ${describeSkeleton2(donor, am.donor)}`);
7330
- for (const l of am.lines)
7331
- log(l);
7332
- spec = { _comment: `auto-mapped ${basename6(input)} ← ${basename6(donorPath)} by skeleton shape — review before trusting`, root: am.root, bones: am.map, legs: am.legs };
9075
+ const avatarFile = p.values.get("--avatar")?.[0], donorAvatarFile = p.values.get("--donor-avatar")?.[0];
9076
+ if (autoOut !== undefined || mapPath === "auto" || avatarFile || donorAvatarFile) {
9077
+ let tAv, dAv;
9078
+ try {
9079
+ tAv = loadAvatar(target, avatarFile);
9080
+ dAv = loadAvatar(donor, donorAvatarFile);
9081
+ } catch (e) {
9082
+ console.error(`✗ ${e.message}`);
9083
+ return 1;
9084
+ }
9085
+ if (tAv && dAv) {
9086
+ const hm = mapAvatars2(tAv, dAv);
9087
+ log(`\uD83E\uDDED humanoid map ${basename7(input)} ← ${basename7(donorPath)}`);
9088
+ for (const [label, rig, file] of [["target", target, avatarFile], ["donor", donor, donorAvatarFile]]) {
9089
+ try {
9090
+ log(` ${label}: ${file ? `avatar from ${basename7(file)} · ` : ""}${describeBiped2(rig, classifyBiped2(rig))}`);
9091
+ } catch {
9092
+ log(` ${label}: avatar from ${basename7(file ?? "?")} (shape did not classify)`);
9093
+ }
9094
+ }
9095
+ for (const l of describeHumanoidMap2(tAv, dAv, hm))
9096
+ log(l);
9097
+ humanoid = { target: tAv, donor: dAv };
9098
+ const { avatarToFile: avatarToFile2 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9099
+ spec = { _comment: `humanoid map ${basename7(input)} ← ${basename7(donorPath)} — target and donor avatars joined on the Unity/VRM canon; review before trusting`, root: hm.root, bones: hm.map, legs: hm.legs, twist: tAv.twist, avatars: { target: avatarToFile2(tAv), donor: avatarToFile2(dAv) } };
9100
+ } else if (tAv || dAv) {
9101
+ console.error(`✗ ${tAv ? "the donor" : "the target"} does not read as a humanoid while ${tAv ? "the target" : "the donor"} does — a biped↔quadruped retarget needs a hand-written map`);
9102
+ return 1;
9103
+ } else {
9104
+ const { autoMap: autoMap2, describeSkeleton: describeSkeleton2 } = await Promise.resolve().then(() => exports_automap);
9105
+ const am = autoMap2(target, donor);
9106
+ log(`\uD83E\uDDED auto-map ${basename7(input)} ← ${basename7(donorPath)}`);
9107
+ log(` target: ${describeSkeleton2(target, am.target)}`);
9108
+ log(` donor: ${describeSkeleton2(donor, am.donor)}`);
9109
+ for (const l of am.lines)
9110
+ log(l);
9111
+ spec = { _comment: `auto-mapped ${basename7(input)} ← ${basename7(donorPath)} by skeleton shape — review before trusting`, root: am.root, bones: am.map, legs: am.legs };
9112
+ }
7333
9113
  if (autoOut !== undefined) {
7334
- const out2 = autoOut[0] ?? input.replace(/\.glb$/i, "") + "-from-" + basename6(donorPath).replace(/\.[^.]+$/, "") + ".map.json";
9114
+ const out2 = autoOut[0] ?? input.replace(/\.glb$/i, "") + "-from-" + basename7(donorPath).replace(/\.[^.]+$/, "") + ".map.json";
7335
9115
  writeFileSync4(out2, JSON.stringify(spec, null, 2) + `
7336
9116
  `);
7337
- log(`\uD83D\uDCBE wrote ${out2} (${Object.keys(am.map).length} bones) — edit it, then: retarget … --map ${basename6(out2)}`);
9117
+ log(`\uD83D\uDCBE wrote ${out2} (${Object.keys(spec.bones).length} bones) — edit it, then: retarget … --map ${basename7(out2)}`);
7338
9118
  return 0;
7339
9119
  }
7340
9120
  } else {
@@ -7342,27 +9122,41 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7342
9122
  console.error("✗ --map <map.json> is required (--map auto / --auto-map [out.json] to derive one from the skeletons, --skeletons to list them)");
7343
9123
  return 2;
7344
9124
  }
7345
- if (!existsSync10(mapPath)) {
9125
+ if (!existsSync11(mapPath)) {
7346
9126
  console.error(`✗ not found: ${mapPath}`);
7347
9127
  return 1;
7348
9128
  }
7349
- spec = JSON.parse(readFileSync11(mapPath, "utf8"));
9129
+ spec = JSON.parse(readFileSync12(mapPath, "utf8"));
7350
9130
  if (!spec.bones) {
7351
9131
  console.error(`✗ ${mapPath} has no "bones" object`);
7352
9132
  return 2;
7353
9133
  }
9134
+ if (spec.avatars) {
9135
+ try {
9136
+ humanoid = { target: avatarFromFile2(target, spec.avatars.target), donor: avatarFromFile2(donor, spec.avatars.donor) };
9137
+ } catch (e) {
9138
+ log(` ⚠ ${basename7(mapPath)} carries avatars that do not fit these rigs (${e.message}) — plain map`);
9139
+ }
9140
+ }
7354
9141
  }
7355
9142
  checkUniformScale2(target);
7356
9143
  checkUniformScale2(donor);
7357
9144
  const t0 = performance.now();
7358
- log(`\uD83D\uDD01 ${basename6(input)} ← ${basename6(donorPath)}`);
9145
+ log(`\uD83D\uDD01 ${basename7(input)} ← ${basename7(donorPath)}`);
7359
9146
  let donorYawDeg = 0;
7360
9147
  {
7361
9148
  let fa = null, da = null;
7362
9149
  try {
7363
9150
  const { classify: classify2 } = await Promise.resolve().then(() => exports_automap);
7364
- fa = classify2(target);
7365
- da = classify2(donor);
9151
+ const facingOf = (rig) => {
9152
+ try {
9153
+ return classifyBiped2(rig);
9154
+ } catch {
9155
+ return classify2(rig);
9156
+ }
9157
+ };
9158
+ fa = facingOf(target);
9159
+ da = facingOf(donor);
7366
9160
  } catch {
7367
9161
  const spine = Object.entries(spec.bones);
7368
9162
  const headPair = spine.find(([t]) => /head/i.test(t)), tailPair = spine.find(([t]) => /tail|root|hip|pelvis/i.test(t));
@@ -7389,6 +9183,27 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7389
9183
  const prefix = p.values.get("--prefix")?.[0] ?? "";
7390
9184
  const names = p.values.get("--clip-names");
7391
9185
  let produced = 0;
9186
+ const tr = humanoid?.target.transfer;
9187
+ if (tr && Object.keys(tr).length) {
9188
+ const parts = [];
9189
+ if (tr.rootMotion)
9190
+ parts.push(`root motion ${tr.rootMotion}`);
9191
+ if (tr.plant !== undefined)
9192
+ parts.push(tr.plant ? "plant" : "no plant");
9193
+ if (tr.hands)
9194
+ parts.push("hands IK");
9195
+ if (tr.twist !== undefined)
9196
+ parts.push(`twist ${tr.twist}`);
9197
+ for (const s of ["Left", "Right"])
9198
+ if (tr.roll?.[s])
9199
+ parts.push(`${s[0]} roll ${tr.roll[s] > 0 ? "+" : ""}${tr.roll[s]}°`);
9200
+ const keep = Object.entries(tr.align ?? {}).filter(([, v]) => v === "keep").map(([k]) => k), al = Object.entries(tr.align ?? {}).filter(([, v]) => v === "align").map(([k]) => k);
9201
+ if (keep.length)
9202
+ parts.push(`keep ${keep.join(", ")}`);
9203
+ if (al.length)
9204
+ parts.push(`align ${al.join(", ")}`);
9205
+ log(` avatar transfer: ${parts.join(" · ")}`);
9206
+ }
7392
9207
  const rep = retarget2(target, donor, spec.bones, spec.root ?? null, {
7393
9208
  align: p.values.get("--align")?.[0] ?? "bone-direction",
7394
9209
  alignTo: spec.alignTo,
@@ -7398,35 +9213,51 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7398
9213
  const v = p.values.get("--donor-ref")?.[0];
7399
9214
  return v === undefined ? spec.donorRef : v === "bind" || v === "auto" ? v : { clip: v };
7400
9215
  })(),
7401
- ik: p.values.get("--ik")?.[0] === "none" || spec.ik === false ? "none" : "legs",
9216
+ ik: p.values.get("--ik")?.[0] === "none" || spec.ik === false ? "none" : p.values.get("--ik")?.[0] === "hands" || p.values.get("--ik") === undefined && tr?.hands ? "legs+hands" : "legs",
9217
+ rootMotion: p.values.get("--root-motion")?.[0] ?? tr?.rootMotion ?? "auto",
9218
+ rootRotation: p.values.get("--root-rotation")?.[0] ?? "none",
9219
+ plant: p.flags.has("--no-plant") ? false : tr?.plant ?? true,
9220
+ twist: (() => {
9221
+ const v = p.values.get("--twist")?.[0];
9222
+ return v === undefined ? tr?.twist !== undefined ? tr.twist > 0 ? tr.twist : false : 0.5 : v === "off" ? false : Number(v);
9223
+ })(),
7402
9224
  legs: spec.legs,
7403
9225
  fps: num2(p, "--fps"),
7404
9226
  rootScale: real(p, "--root-scale"),
7405
9227
  clips: p.values.get("--clips"),
7406
9228
  rename: names?.length || prefix ? (n) => prefix + (names?.[produced++] ?? n) : undefined,
9229
+ humanoid,
7407
9230
  warn: (w) => log(` ⚠ ${w}`)
7408
9231
  });
9232
+ if (humanoid)
9233
+ log(` humanoid: torso/legs keep, arms + fingers aligned (≥ 8°)${rep.roll.length ? ` · palm roll ${rep.roll.map((r) => `${r.side[0]} ${r.degrees.toFixed(0)}° (${r.via})`).join(", ")}` : ""}${rep.rootTranslateOnly ? " · root takes the hips' travel only" : ""}`);
7409
9234
  if (rep.bindGap.donor.worst > 0.001)
7410
9235
  log(` ℹ donor's node pose sits ${rep.bindGap.donor.worst.toFixed(3)}m from its bind pose (worst: ${rep.bindGap.donor.bone}) — anchoring on the bind pose`);
7411
- log(` ${rep.mapped.length} bones mapped · ${rep.align.length} direction-aligned (worst ${rep.align[0]?.degrees.toFixed(0) ?? 0}° on ${rep.align[0]?.bone ?? "-"}) · ${rep.kept.length} keep bind (${rep.kept.join(", ") || "-"})`);
9236
+ log(` ${rep.mapped.length} bones mapped · ${rep.align.length} direction-aligned (worst ${rep.align[0]?.degrees.toFixed(0) ?? 0}° on ${rep.align[0]?.bone ?? "-"})${rep.underThreshold.length ? ` · ${rep.underThreshold.length} under 8° kept (worst ${rep.underThreshold[0].degrees.toFixed(0)}° on ${rep.underThreshold[0].bone})` : ""} · ${rep.kept.length} keep bind (${rep.kept.join(", ") || "-"})`);
7412
9237
  log(` donor reference: ${rep.donorRef} (leg directions ${rep.legDisagreement.toFixed(0)}° apart between the binds) · target binds ${rep.stance ? "in a stance — legs keep it" : "posed — legs aligned to the donor"}`);
7413
9238
  log(` root ${rep.root ? `${rep.root.target}←${rep.root.donor} ×${+rep.rootScale.toPrecision(3)} (from ${rep.rootScaleFrom})` : "none"}${rep.ground ? ` · ground target ${rep.ground.target.toFixed(3)}m / donor ${rep.ground.donor.toFixed(3)}m` : ""}`);
7414
9239
  if (rep.legs.length)
7415
9240
  for (const l of rep.legs)
7416
- log(` \uD83E\uDDB6 ${l.effector.padEnd(14)} ← ${l.donorEffector.padEnd(12)} chain ${l.chain.join("→")} ×${l.ratio.toFixed(3)}${l.shortfall > 0.0001 ? ` ⚠ out of reach by up to ${(l.shortfall * 1000).toFixed(0)}mm` : ""}`);
9241
+ log(` ${l.isFoot ? "\uD83E\uDDB6" : "✋"} ${l.effector.padEnd(14)} ← ${l.donorEffector.padEnd(12)} chain ${l.chain.join("→")}${l.contact !== l.effector ? ` contact ${l.contact}` : ""} ×${l.ratio.toFixed(3)}${l.planted ? ` planted ${l.planted} frames` : ""}${l.shortfall > 0.0001 ? ` ⚠ out of reach by up to ${(l.shortfall * 1000).toFixed(0)}mm` : ""}`);
7417
9242
  else if (p.values.get("--ik")?.[0] !== "none" && spec.ik !== false)
7418
9243
  log(` ℹ no leg chains detected — feet follow FK only (name them in the map: "legs": [ … ])`);
9244
+ if (rep.root)
9245
+ log(` root motion: ${rep.rootMotion}${rep.hipsBob ? ` · hips carry the ${rep.hipsBob === "donor" ? "donor's own bob" : "vertical bob"}` : ""}`);
9246
+ if (rep.twist.length)
9247
+ log(` twist helpers: ${rep.twist.map((t) => `${t.bone} (${(t.at * 100).toFixed(0)}% of ${t.of})`).join(", ")}`);
7419
9248
  if (rep.bridges.length)
7420
9249
  log(` bridged (interpolated between mapped bones): ${rep.bridges.join(", ")}`);
7421
9250
  if (rep.unmappedTarget.length)
7422
9251
  log(` unmapped (left at rest): ${rep.unmappedTarget.join(", ")}`);
7423
- for (const c of rep.clips)
7424
- log(` \uD83C\uDF9E ${c.name.padEnd(22)} ${c.duration.toFixed(2)}s ${c.frames} frames → ${c.keys} keys`);
9252
+ for (const c of rep.clips) {
9253
+ const reach = c.events.filter((e) => e.kind === "reach"), below = c.events.filter((e) => e.kind === "below");
9254
+ log(` \uD83C\uDF9E ${c.name.padEnd(22)} ${c.duration.toFixed(2)}s ${c.frames} frames → ${c.keys} keys${reach.length ? ` ⚠ ${reach.length}× out of reach (${(Math.max(...reach.map((e) => e.value ?? 0)) * 1000).toFixed(0)} mm)` : ""}${below.length ? ` ⚠ ${below.length}× toe under the ground (${(Math.max(...below.map((e) => e.value ?? 0)) * 1000).toFixed(0)} mm)` : ""}`);
9255
+ }
7425
9256
  bindAccessors2(doc);
7426
9257
  const out = p.values.get("-o")?.[0] ?? input.replace(/\.glb$/i, ".retargeted.glb");
7427
9258
  const dir = dirname7(out);
7428
- if (dir && !existsSync10(dir))
7429
- mkdirSync4(dir, { recursive: true });
9259
+ if (dir && !existsSync11(dir))
9260
+ mkdirSync5(dir, { recursive: true });
7430
9261
  const glb = await writeGlb2(doc);
7431
9262
  writeFileSync4(out, glb);
7432
9263
  const { validateGlb: validateGlb2, formatReport: formatReport2 } = await Promise.resolve().then(() => (init_validateGlb(), exports_validateGlb));
@@ -7437,22 +9268,103 @@ donor clips: ${donor.clips.map((c) => `"${c.name}" ${c.duration.toFixed(2)}s`).j
7437
9268
  log(`\uD83D\uDCBE wrote ${out} (${(glb.byteLength / 1024 / 1024).toFixed(2)} MB, ${((performance.now() - t0) / 1000).toFixed(1)}s)`);
7438
9269
  return 0;
7439
9270
  };
9271
+ var cmdAvatar = async (p) => {
9272
+ const input = p.positionals[0];
9273
+ if (!input) {
9274
+ log(USAGE);
9275
+ return 2;
9276
+ }
9277
+ if (!existsSync11(input)) {
9278
+ console.error(`✗ not found: ${input}`);
9279
+ return 1;
9280
+ }
9281
+ const { loadRig: loadRig2, rigFromDocument: rigFromDocument2 } = await Promise.resolve().then(() => (init_rig(), exports_rig));
9282
+ const { classifyBiped: classifyBiped2, avatarFromBiped: avatarFromBiped2, avatarFromUnity: avatarFromUnity2, avatarToFile: avatarToFile2, parseUnityAvatar: parseUnityAvatar2, describeBiped: describeBiped2, HUMAN_BONES: HUMAN_BONES2 } = await Promise.resolve().then(() => (init_humanoid(), exports_humanoid));
9283
+ const { makeIO: makeIO3 } = await Promise.resolve().then(() => (init_convert(), exports_convert));
9284
+ const rig = /\.fbx$/i.test(input) ? await loadRig2(input) : rigFromDocument2(await makeIO3().read(input), basename7(input));
9285
+ let shape = null;
9286
+ try {
9287
+ const b = classifyBiped2(rig);
9288
+ shape = avatarFromBiped2(rig, b);
9289
+ log(`\uD83E\uDDCD ${basename7(input)}: ${describeBiped2(rig, b)}`);
9290
+ const groups = [
9291
+ ["torso", ["Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", "Jaw", "LeftEye", "RightEye"]],
9292
+ ["left arm", ["LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand"]],
9293
+ ["right arm", ["RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand"]],
9294
+ ["left leg", ["LeftUpperLeg", "LeftLowerLeg", "LeftFoot", "LeftToes"]],
9295
+ ["right leg", ["RightUpperLeg", "RightLowerLeg", "RightFoot", "RightToes"]]
9296
+ ];
9297
+ const av = shape;
9298
+ for (const [g, keys] of groups)
9299
+ log(` ${g}: ${keys.filter((k) => av.bones[k]).map((k) => `${k}=${av.bones[k]}`).join(" ")}`);
9300
+ const fingers = HUMAN_BONES2.filter((k) => k.includes(" ") && shape.bones[k]);
9301
+ if (fingers.length)
9302
+ log(` fingers: ${fingers.length} phalanges (${fingers.filter((k) => k.endsWith("Proximal")).map((k) => shape.bones[k]).join(", ")})`);
9303
+ if (Object.keys(shape.twist).length)
9304
+ log(` twist helpers: ${Object.entries(shape.twist).map(([t, of]) => `${t}→${of}`).join(", ")}`);
9305
+ if (shape.bridged.length)
9306
+ log(` bridged: ${shape.bridged.join(", ")}`);
9307
+ if (b.helpers.length)
9308
+ log(` not part of the body (${b.helpers.length}): ${b.helpers.map((i) => rig.names[i]).slice(0, 16).join(", ")}${b.helpers.length > 16 ? " …" : ""}`);
9309
+ for (const n of shape.notes)
9310
+ log(` ⚠ ${n}`);
9311
+ } catch (e) {
9312
+ log(`✗ ${basename7(input)} does not read as a humanoid: ${e.message}`);
9313
+ }
9314
+ const metaPath = p.values.get("--meta")?.[0];
9315
+ let unity = null;
9316
+ if (metaPath) {
9317
+ if (!existsSync11(metaPath)) {
9318
+ console.error(`✗ not found: ${metaPath}`);
9319
+ return 1;
9320
+ }
9321
+ unity = avatarFromUnity2(rig, parseUnityAvatar2(readFileSync12(metaPath, "utf8")));
9322
+ for (const n of unity.notes)
9323
+ log(` ⚠ .meta: ${n}`);
9324
+ if (shape) {
9325
+ let agree = 0;
9326
+ const differ = [];
9327
+ for (const k of HUMAN_BONES2) {
9328
+ const a = shape.bones[k], m = unity.bones[k];
9329
+ if (a === m) {
9330
+ if (a)
9331
+ agree++;
9332
+ continue;
9333
+ }
9334
+ differ.push(`${k}: shape=${a ?? "-"} .meta=${m ?? "-"}`);
9335
+ }
9336
+ log(` vs ${basename7(metaPath)}: ${agree} bones agree${differ.length ? `, ${differ.length} differ:` : ""}`);
9337
+ for (const d of differ)
9338
+ log(` ${d}`);
9339
+ }
9340
+ }
9341
+ const chosen = unity ?? shape;
9342
+ if (!chosen)
9343
+ return 1;
9344
+ const out = p.values.get("-o")?.[0];
9345
+ if (out) {
9346
+ writeFileSync4(out, JSON.stringify(avatarToFile2(chosen), null, 2) + `
9347
+ `);
9348
+ log(`\uD83D\uDCBE wrote ${out} (${Object.keys(chosen.bones).length} bones, ${unity ? "Unity's map" : "from shape"}) — retarget … --avatar ${basename7(out)}`);
9349
+ }
9350
+ return 0;
9351
+ };
7440
9352
  var cmdUnpack = async (p) => {
7441
9353
  const pkg = p.positionals[0];
7442
9354
  if (!pkg) {
7443
9355
  log(USAGE);
7444
9356
  return 2;
7445
9357
  }
7446
- if (!existsSync10(pkg)) {
9358
+ if (!existsSync11(pkg)) {
7447
9359
  console.error(`✗ not found: ${pkg}`);
7448
9360
  return 1;
7449
9361
  }
7450
9362
  const { indexUnityPackage: indexUnityPackage2, writeUnityIndex: writeUnityIndex2, readUnityIndex: readUnityIndex2, extractUnityPackage: extractUnityPackage2, INDEX_NAME: INDEX_NAME2 } = await Promise.resolve().then(() => (init_unpack(), exports_unpack));
7451
- const outDir = p.values.get("-o")?.[0] ?? join9(dirname7(pkg), basename6(pkg, extname4(pkg)));
9363
+ const outDir = p.values.get("-o")?.[0] ?? join10(dirname7(pkg), basename7(pkg, extname5(pkg)));
7452
9364
  const t0 = performance.now();
7453
- const indexFile = join9(outDir, INDEX_NAME2);
9365
+ const indexFile = join10(outDir, INDEX_NAME2);
7454
9366
  let index;
7455
- if (existsSync10(indexFile)) {
9367
+ if (existsSync11(indexFile)) {
7456
9368
  index = readUnityIndex2(indexFile);
7457
9369
  log(`\uD83D\uDCC7 ${index.size} assets (cached ${indexFile})`);
7458
9370
  } else {
@@ -7481,14 +9393,14 @@ var cmdProbe = async (p) => {
7481
9393
  log(USAGE);
7482
9394
  return 2;
7483
9395
  }
7484
- if (!existsSync10(input)) {
9396
+ if (!existsSync11(input)) {
7485
9397
  console.error(`✗ not found: ${input}`);
7486
9398
  return 1;
7487
9399
  }
7488
9400
  const { probeFile: probeFile2 } = await Promise.resolve().then(() => (init_probe(), exports_probe));
7489
9401
  log(await probeFile2(input));
7490
9402
  if (p.values.has("--filmstrip")) {
7491
- if (extname4(input).toLowerCase() === ".fbx") {
9403
+ if (extname5(input).toLowerCase() === ".fbx") {
7492
9404
  console.error("✗ --filmstrip wants a GLB (convert first)");
7493
9405
  return 2;
7494
9406
  }
@@ -7519,7 +9431,7 @@ var cmdScene = async (p) => {
7519
9431
  log(USAGE);
7520
9432
  return 2;
7521
9433
  }
7522
- if (!existsSync10(input)) {
9434
+ if (!existsSync11(input)) {
7523
9435
  console.error(`✗ not found: ${input}`);
7524
9436
  return 1;
7525
9437
  }
@@ -7553,8 +9465,8 @@ var cmdScene = async (p) => {
7553
9465
  log(` ${l}`);
7554
9466
  const out = p.values.get("-o")?.[0] ?? input.replace(/\.unity$/i, "") + ".level.json";
7555
9467
  const dir = dirname7(out);
7556
- if (dir && !existsSync10(dir))
7557
- mkdirSync4(dir, { recursive: true });
9468
+ if (dir && !existsSync11(dir))
9469
+ mkdirSync5(dir, { recursive: true });
7558
9470
  writeFileSync4(out, JSON.stringify(r, null, 1) + `
7559
9471
  `);
7560
9472
  log(`\uD83D\uDCBE wrote ${out} (${r.instances.length} instances, ${((performance.now() - t0) / 1000).toFixed(1)}s) — frame: ${r.frame}`);
@@ -7567,8 +9479,8 @@ var resolveInputs = (args) => {
7567
9479
  out.push(f);
7568
9480
  };
7569
9481
  const walk = (dir, recursive, pattern) => {
7570
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
7571
- const p = join9(dir, entry.name).replace(/\\/g, "/");
9482
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
9483
+ const p = join10(dir, entry.name).replace(/\\/g, "/");
7572
9484
  if (entry.isDirectory()) {
7573
9485
  if (recursive)
7574
9486
  walk(p, true, pattern);
@@ -7586,9 +9498,9 @@ var resolveInputs = (args) => {
7586
9498
  if (recursive)
7587
9499
  segs.pop();
7588
9500
  const dir = segs.join("/") || ".";
7589
- if (existsSync10(dir))
9501
+ if (existsSync11(dir))
7590
9502
  walk(dir, recursive, toRegex(last));
7591
- } else if (existsSync10(a) && statSync6(a).isDirectory()) {
9503
+ } else if (existsSync11(a) && statSync7(a).isDirectory()) {
7592
9504
  walk(a.replace(/\/+$/, ""), false, /\.glb$/i);
7593
9505
  } else
7594
9506
  out.push(a);
@@ -7609,12 +9521,13 @@ var cmdDoctor = async (p) => {
7609
9521
  const sh = shadingOpts(p);
7610
9522
  const sc = scaleOpts(p);
7611
9523
  const lmo = lightmapOpts(p);
9524
+ const lod = lodOpts(p);
7612
9525
  const wantsScale = sc.fixScale || sc.scale !== undefined || sc.scaleTo !== undefined;
7613
- const doFix = p.flags.has("--fix") || useKtx2 || fixMetallic || wantsScale || sh.unlit || sh.fixEmissive || lmo.lightmapUv;
9526
+ const doFix = p.flags.has("--fix") || useKtx2 || fixMetallic || wantsScale || sh.unlit || sh.fixEmissive || lmo.lightmapUv || lod.lod.length > 0;
7614
9527
  const dumpMode = p.values.has("--dump");
7615
9528
  const dumpVal = p.values.get("--dump")?.[0] ?? null;
7616
9529
  const oVal = p.values.get("-o")?.[0] ?? null;
7617
- const opts = { ktx2: useKtx2, ktx2Hq: p.flags.has("--ktx2-hq"), ktx2Uastc: p.flags.has("--ktx2-uastc"), maxSize: num2(p, "--max-size"), target: num2(p, "--target"), ...sc, ...sh, ...lmo };
9530
+ const opts = { ktx2: useKtx2, ktx2Hq: p.flags.has("--ktx2-hq"), ktx2Uastc: p.flags.has("--ktx2-uastc"), maxSize: num2(p, "--max-size"), target: num2(p, "--target"), ...sc, ...sh, ...lmo, ...lod };
7618
9531
  let toktx = "";
7619
9532
  if (useKtx2) {
7620
9533
  toktx = findToktx2();
@@ -7632,7 +9545,7 @@ var cmdDoctor = async (p) => {
7632
9545
  log(`Processing ${files.length} files…`);
7633
9546
  let exitCode = 0;
7634
9547
  for (const input of files) {
7635
- if (!existsSync10(input)) {
9548
+ if (!existsSync11(input)) {
7636
9549
  console.error(`
7637
9550
  ✗ not found: ${input}`);
7638
9551
  exitCode = 1;
@@ -7668,13 +9581,23 @@ var cmdDoctor = async (p) => {
7668
9581
  log(` baked scale ×${+res.scaled.toPrecision(6)}`);
7669
9582
  if (res.lightmapUv)
7670
9583
  log(` lightmap UV: ${res.lightmapUv.charts} charts over ${res.lightmapUv.primitives} primitive(s) (xatlas ${res.lightmapUv.width}×${res.lightmapUv.height}); vertices ${res.lightmapUv.verticesBefore} → ${res.lightmapUv.verticesAfter}`);
9584
+ if (res.lod) {
9585
+ for (const t of res.lod.tris)
9586
+ log(` lod: ${t.name} ${t.levels.join(" / ")} tris`);
9587
+ for (const b of res.lod.bones)
9588
+ log(` lod bones: ${b.name} last level ${b.before} → ${b.after} joints`);
9589
+ for (const k of res.lod.skipped)
9590
+ log(` lod: ${k.name} level ${k.level} dropped — the simplifier reached only ${k.tris} tris (seams / hard surfaces; try a smaller ratio)`);
9591
+ if (!res.lod.nodes)
9592
+ log(` lod: no mesh with >= 2000 triangles (or the _LOD nodes are already there)`);
9593
+ }
7671
9594
  const output = outputFor2(input, oVal, files.length > 1, useKtx2);
7672
9595
  const root = doc.getRoot();
7673
9596
  if (!root.getDefaultScene() && root.listScenes().length > 0)
7674
9597
  root.setDefaultScene(root.listScenes()[0]);
7675
9598
  await io.write(output, doc);
7676
- const beforeBytes = statSync6(input).size;
7677
- const after = readFileSync11(output);
9599
+ const beforeBytes = statSync7(input).size;
9600
+ const after = readFileSync12(output);
7678
9601
  log(`\uD83D\uDCBE wrote ${output} (${kb2(beforeBytes)} → ${kb2(after.byteLength)})`);
7679
9602
  log(formatReport2(validateGlb2(after), output));
7680
9603
  }
@@ -7705,6 +9628,42 @@ var cmdSky = async (p) => {
7705
9628
  return 1;
7706
9629
  }
7707
9630
  };
9631
+ var cmdTerrainPack = async (p) => {
9632
+ if (p.positionals.length === 0) {
9633
+ console.error("✗ usage: lecodes-assets terrain-pack <layer> [<layer> …] [-o dir] [--normals n0 n1 …] [--names a b …] [--size N] [--png] [--flip-y]");
9634
+ return 2;
9635
+ }
9636
+ const { resolveTerrainLayer: resolveTerrainLayer2, packTerrainTextures: packTerrainTextures2 } = await Promise.resolve().then(() => (init_terrainPack(), exports_terrainPack));
9637
+ const normals = p.values.get("--normals") ?? [];
9638
+ const names = p.values.get("--names") ?? [];
9639
+ const outDir = resolve6(p.values.get("-o")?.[0] ?? "terrain");
9640
+ try {
9641
+ const layers = p.positionals.map((input, i) => {
9642
+ const n = normals[i];
9643
+ return resolveTerrainLayer2(input, n && n !== "-" ? n : undefined, names[i]);
9644
+ });
9645
+ log(`
9646
+ \uD83C\uDFD4 ${layers.length} layer(s) → ${outDir}`);
9647
+ const res = await packTerrainTextures2(layers, {
9648
+ outDir,
9649
+ size: num2(p, "--size"),
9650
+ png: p.flags.has("--png"),
9651
+ flipY: p.flags.has("--flip-y"),
9652
+ quality: num2(p, "--quality"),
9653
+ log
9654
+ });
9655
+ log(`
9656
+ Terrain.create({ …,
9657
+ ${res.snippet.split(`
9658
+ `).map((l) => " " + l).join(`
9659
+ `)}
9660
+ }) (asset() paths relative to the calling file)`);
9661
+ return 0;
9662
+ } catch (e) {
9663
+ console.error(`✗ ${e.message}`);
9664
+ return 1;
9665
+ }
9666
+ };
7708
9667
  var runAssetsCli = async (argv) => {
7709
9668
  const [cmd, ...rest] = argv;
7710
9669
  const p = parse(rest);
@@ -7719,8 +9678,12 @@ var runAssetsCli = async (argv) => {
7719
9678
  return cmdDoctor(p);
7720
9679
  case "retarget":
7721
9680
  return cmdRetarget(p);
9681
+ case "avatar":
9682
+ return cmdAvatar(p);
7722
9683
  case "sky":
7723
9684
  return cmdSky(p);
9685
+ case "terrain-pack":
9686
+ return cmdTerrainPack(p);
7724
9687
  case "scene":
7725
9688
  return cmdScene(p);
7726
9689
  default: