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