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.
- package/README.md +449 -326
- package/dist/cli.js +2161 -198
- package/dist/index.js +2268 -198
- 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
|
|
1094
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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 (
|
|
1212
|
-
|
|
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
|
|
1295
|
-
|
|
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))),
|
|
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
|
|
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 +
|
|
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 +
|
|
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
|
|
4193
|
+
var len2 = HEAPU32[iov + 4 >> 2];
|
|
4051
4194
|
iov += 8;
|
|
4052
|
-
for (var j = 0;j <
|
|
4195
|
+
for (var j = 0;j < len2; j++) {
|
|
4053
4196
|
printChar(fd, HEAPU8[ptr + j]);
|
|
4054
4197
|
}
|
|
4055
|
-
num2 +=
|
|
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
|
-
|
|
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.
|
|
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(
|
|
5043
|
-
const us = unit(
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
5107
|
-
let groundOffset = lowestBind - Math.min(...
|
|
5108
|
-
|
|
5109
|
-
const
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
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]),
|
|
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
|
|
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 =
|
|
5128
|
-
const axis = unit(
|
|
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
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
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(
|
|
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
|
|
5165
|
-
if (Math.abs(
|
|
5660
|
+
const c = refPosGrounded[leg.contact], floor = lowestBind + restAbove(leg);
|
|
5661
|
+
if (Math.abs(c[1] - floor) < 0.000001)
|
|
5166
5662
|
continue;
|
|
5167
|
-
|
|
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.
|
|
5171
|
-
leg.stanceTarget =
|
|
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
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
5257
|
-
const
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6203
|
+
const len2 = Math.hypot(...fwd) || 1;
|
|
5318
6204
|
return {
|
|
5319
|
-
forward: [fwd[0] /
|
|
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
|
-
},
|
|
5362
|
-
const l =
|
|
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(
|
|
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(
|
|
5479
|
-
us = unit(
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
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(
|
|
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 +=
|
|
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(
|
|
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 =
|
|
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 (
|
|
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 =
|
|
5672
|
-
const dist =
|
|
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],
|
|
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(
|
|
5683
|
-
p[i] = add(p[i + 1],
|
|
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(
|
|
5688
|
-
p[i] = add(p[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 (
|
|
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 =
|
|
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),
|
|
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 +
|
|
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
|
|
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
|
|
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 +
|
|
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) =>
|
|
5972
|
-
const lat = (i) =>
|
|
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 +=
|
|
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
|
|
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) => [
|
|
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 + (
|
|
6572
|
-
const sy = (q) => HEAD + CELL - PAD - (
|
|
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), -
|
|
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
|
|
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
|
|
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
|
|
6996
|
-
spine/neck/head/tail/legs/ears/jaw
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
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(
|
|
7150
|
-
report.clips.push(...mergeClipScene2(doc, cs,
|
|
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 (!
|
|
8871
|
+
if (!existsSync11(f)) {
|
|
7162
8872
|
console.error(`✗ not found: ${f}`);
|
|
7163
8873
|
return 1;
|
|
7164
8874
|
}
|
|
7165
|
-
const d =
|
|
7166
|
-
donors.push({ doc: d, label:
|
|
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
|
-
|
|
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, "") :
|
|
7218
|
-
if (!
|
|
7219
|
-
|
|
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 =
|
|
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 ? "✅" : "✗"} ${
|
|
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 && !
|
|
7236
|
-
|
|
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 (!
|
|
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,
|
|
7279
|
-
|
|
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 (!
|
|
9020
|
+
if (!existsSync11(f)) {
|
|
7286
9021
|
console.error(`✗ not found: ${f}`);
|
|
7287
9022
|
return 1;
|
|
7288
9023
|
}
|
|
7289
|
-
parsed.push(parseUnityAnim2(
|
|
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 ${
|
|
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
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
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-" +
|
|
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(
|
|
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 (!
|
|
9125
|
+
if (!existsSync11(mapPath)) {
|
|
7346
9126
|
console.error(`✗ not found: ${mapPath}`);
|
|
7347
9127
|
return 1;
|
|
7348
9128
|
}
|
|
7349
|
-
spec = JSON.parse(
|
|
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 ${
|
|
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
|
-
|
|
7365
|
-
|
|
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
|
-
|
|
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 && !
|
|
7429
|
-
|
|
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 (!
|
|
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] ??
|
|
9363
|
+
const outDir = p.values.get("-o")?.[0] ?? join10(dirname7(pkg), basename7(pkg, extname5(pkg)));
|
|
7452
9364
|
const t0 = performance.now();
|
|
7453
|
-
const indexFile =
|
|
9365
|
+
const indexFile = join10(outDir, INDEX_NAME2);
|
|
7454
9366
|
let index;
|
|
7455
|
-
if (
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 && !
|
|
7557
|
-
|
|
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
|
|
7571
|
-
const p =
|
|
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 (
|
|
9501
|
+
if (existsSync11(dir))
|
|
7590
9502
|
walk(dir, recursive, toRegex(last));
|
|
7591
|
-
} else if (
|
|
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 (!
|
|
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 =
|
|
7677
|
-
const after =
|
|
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:
|