cozyclay 1.0.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/LICENSE +674 -0
- package/README.md +109 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/bin/cozyclay.mjs +194 -0
- package/dist/ardy/cskel27-rest.json +557 -0
- package/dist/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/dist/assets/basis_transcoder-o4Hde_L7.js +19 -0
- package/dist/assets/draco_decoder-C32yEggz.wasm +0 -0
- package/dist/assets/draco_decoder-Z1_iN-Ht.wasm +0 -0
- package/dist/assets/draco_decoder-fzg4nYZr.js +34 -0
- package/dist/assets/draco_wasm_wrapper-DxJM36Ib.js +117 -0
- package/dist/assets/draco_wasm_wrapper-fZCQGLGb.js +116 -0
- package/dist/assets/index-BW_S1YRy.js +4395 -0
- package/dist/assets/index-nRmmJgap.css +1 -0
- package/dist/demo/walk-then-stop.npz +0 -0
- package/dist/fonts/instrument-serif-italic-latin.woff2 +0 -0
- package/dist/fonts/instrument-serif-latin.woff2 +0 -0
- package/dist/fonts/inter-latin.woff2 +0 -0
- package/dist/index.html +13 -0
- package/dist/models/x-bot-tpose.fbx +0 -0
- package/dist/models/y-bot-tpose.fbx +0 -0
- package/package.json +80 -0
- package/src/App.jsx +3840 -0
- package/src/ardy/client.js +140 -0
- package/src/ardy/convert.js +313 -0
- package/src/ardy/cskel27-neutral.js +39 -0
- package/src/ardy/cskel27.js +68 -0
- package/src/ardy/export.js +157 -0
- package/src/ardy/ik.js +610 -0
- package/src/ardy/npz.js +520 -0
- package/src/ardy/playback.js +414 -0
- package/src/ardy/prompt-clips.js +16 -0
- package/src/ardy/timeline-coordinates.js +17 -0
- package/src/ardy/timeline-resize.js +11 -0
- package/src/ardy/timeline.jsx +723 -0
- package/src/ardy/to-cskel27.js +205 -0
- package/src/ardy/waypoints.js +364 -0
- package/src/camera-follow.js +366 -0
- package/src/camera-move.js +286 -0
- package/src/controls.jsx +245 -0
- package/src/dualview.jsx +305 -0
- package/src/hierarchy-model.js +77 -0
- package/src/hierarchy-panel.jsx +365 -0
- package/src/history.js +90 -0
- package/src/main.jsx +10 -0
- package/src/object-catalog.jsx +90 -0
- package/src/object-gizmo.jsx +755 -0
- package/src/planview.jsx +653 -0
- package/src/poses.js +424 -0
- package/src/posestudio.jsx +811 -0
- package/src/props.jsx +302 -0
- package/src/room.jsx +64 -0
- package/src/scene-history.js +125 -0
- package/src/scene-objects.js +420 -0
- package/src/shot-authoring.js +113 -0
- package/src/shot.js +239 -0
- package/src/styles.css +5639 -0
- package/src/ui.jsx +391 -0
- package/src/use-render-activity.js +96 -0
- package/tools/ardy/BRIDGE.md +255 -0
- package/tools/ardy/README.md +136 -0
- package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +1427 -0
- package/tools/ardy/cclay_motion_edit.py +445 -0
- package/tools/ardy/cclay_sequence_generate.py +595 -0
- package/tools/ardy/dump-npz.py +205 -0
- package/tools/ardy/extract-rest.mjs +299 -0
- package/tools/ardy/npz.mjs +335 -0
- package/tools/ardy/out/gen-1786443326924-c6019e-generated.npz +0 -0
- package/tools/ardy/out/gen-1786443609325-0053db-generated.npz +0 -0
- package/tools/ardy/out/gen-1786443835628-749ed1-generated.npz +0 -0
- package/tools/ardy/out/gen-1786462605247-03cb19-generated.npz +0 -0
- package/tools/ardy/pose-to-npz.mjs +106 -0
- package/tools/ardy/run-edit-on-box.sh +73 -0
- package/tools/ardy/run-on-box.sh +568 -0
- package/tools/ardy/run-sequence-on-box.sh +162 -0
- package/tools/ardy/visual-qa.mjs +188 -0
- package/tools/ardy/vq-car.mjs +52 -0
- package/tools/dev-full.mjs +29 -0
- package/tools/process-supervisor.mjs +63 -0
- package/tools/qa-browser.mjs +84 -0
- package/tools/qa-crop3.mjs +22 -0
- package/tools/qa-playview.mjs +49 -0
- package/tools/qa-screenshot.mjs +24 -0
- package/tools/qa-visual.mjs +51 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side client for the ARDY dev sidecar (tools/ardy/bridge.mjs).
|
|
3
|
+
*
|
|
4
|
+
* The sidecar is an OPTIONAL dev companion: it listens on 127.0.0.1:5181 and
|
|
5
|
+
* Vite proxies /ardy to it during development only. The production build is
|
|
6
|
+
* a static SPA with no server at all, so every function here treats a missing
|
|
7
|
+
* sidecar as an expected state and reports it as data — never as a thrown
|
|
8
|
+
* error — so the UI can degrade to a hint instead of crashing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
12
|
+
|
|
13
|
+
// Bounded wait so a hung proxy or a slow-but-alive bridge cannot wedge the
|
|
14
|
+
// panel behind a spinner forever. Long enough to not mislabel a healthy
|
|
15
|
+
// bridge that answers in a couple of seconds, short enough to keep the
|
|
16
|
+
// no-sidecar state snappy.
|
|
17
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Prefer the sidecar's {"reason": "..."} error body; fall back to `fallback`
|
|
21
|
+
* when the body is not JSON (e.g. the dev proxy's own error page).
|
|
22
|
+
*/
|
|
23
|
+
async function reasonOf(res, fallback) {
|
|
24
|
+
try {
|
|
25
|
+
const payload = await res.json();
|
|
26
|
+
if (payload && typeof payload.reason === "string" && payload.reason) return payload.reason;
|
|
27
|
+
} catch {
|
|
28
|
+
// non-JSON error body — keep the caller's fallback
|
|
29
|
+
}
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Probe the sidecar once. Resolves to { ok:true, host, encoder, device } on a
|
|
35
|
+
* healthy 200, otherwise { ok:false, reason }. Never rejects: the most common
|
|
36
|
+
* failure (no sidecar running) is a UI state, not a crash.
|
|
37
|
+
*/
|
|
38
|
+
export async function checkBridge() {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch("/ardy/health", { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
return { ok: false, reason: await reasonOf(res, `bridge unhealthy (HTTP ${res.status})`) };
|
|
43
|
+
}
|
|
44
|
+
const payload = await res.json();
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
host: payload.host,
|
|
48
|
+
encoder: payload.encoder,
|
|
49
|
+
device: payload.device,
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return { ok: false, reason: err?.message || "bridge unreachable" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* List base motions the box reported. Returns [] when the sidecar is down —
|
|
58
|
+
* the caller renders an empty state, it does not crash.
|
|
59
|
+
*/
|
|
60
|
+
export async function listBases() {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch("/ardy/bases", { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
63
|
+
if (!res.ok) return [];
|
|
64
|
+
const payload = await res.json();
|
|
65
|
+
return payload && Array.isArray(payload.bases) ? payload.bases : [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* POST a generation and stream the ndjson answer.
|
|
73
|
+
*
|
|
74
|
+
* The response is read incrementally with a streaming reader and every parsed
|
|
75
|
+
* line is handed to `onEvent(event)`. Lines may be split at any byte — text is
|
|
76
|
+
* buffered until the trailing newline arrives, so a JSON object broken across
|
|
77
|
+
* two chunks is still delivered as one event.
|
|
78
|
+
*
|
|
79
|
+
* Resolves with the terminal `done` event ({event, output, bytes}); rejects on
|
|
80
|
+
* a terminal `error` event, a malformed line, a non-200 response, or an
|
|
81
|
+
* aborted signal (the AbortError propagates so callers can tell cancellation
|
|
82
|
+
* apart from failure). `signal` is optional.
|
|
83
|
+
*/
|
|
84
|
+
export async function generate(body, onEvent, { signal } = {}) {
|
|
85
|
+
const res = await fetch("/ardy/generate", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: JSON_HEADERS,
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
signal,
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw new Error(await reasonOf(res, `generate failed (HTTP ${res.status})`));
|
|
93
|
+
}
|
|
94
|
+
if (!res.body) throw new Error("generate: response has no body stream");
|
|
95
|
+
|
|
96
|
+
const reader = res.body.getReader();
|
|
97
|
+
const decoder = new TextDecoder();
|
|
98
|
+
let buffer = "";
|
|
99
|
+
for (;;) {
|
|
100
|
+
const { done, value } = await reader.read();
|
|
101
|
+
if (done) break;
|
|
102
|
+
buffer += decoder.decode(value, { stream: true });
|
|
103
|
+
// Drain complete lines only; a chunk may end mid-line, in which case
|
|
104
|
+
// the remainder stays in the buffer until its newline arrives.
|
|
105
|
+
let nl = buffer.indexOf("\n");
|
|
106
|
+
while (nl !== -1) {
|
|
107
|
+
const terminal = applyLine(buffer.slice(0, nl).trim());
|
|
108
|
+
buffer = buffer.slice(nl + 1);
|
|
109
|
+
if (terminal) return terminal;
|
|
110
|
+
nl = buffer.indexOf("\n");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Flush the decoder so a multi-byte char split by the final chunk boundary
|
|
114
|
+
// is complete, then treat a non-empty tail as one last line: the contract
|
|
115
|
+
// does not promise a trailing newline on the terminal event.
|
|
116
|
+
buffer += decoder.decode();
|
|
117
|
+
if (buffer.trim()) {
|
|
118
|
+
const terminal = applyLine(buffer.trim());
|
|
119
|
+
if (terminal) return terminal;
|
|
120
|
+
}
|
|
121
|
+
throw new Error("generate: stream ended without a done or error event");
|
|
122
|
+
|
|
123
|
+
/** Parse one ndjson line; returns the done event, throws on error/bad line. */
|
|
124
|
+
function applyLine(line) {
|
|
125
|
+
let event;
|
|
126
|
+
try {
|
|
127
|
+
event = JSON.parse(line);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new Error(`generate: bad ndjson line: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
onEvent(event);
|
|
132
|
+
if (event.event === "error") {
|
|
133
|
+
throw new Error(
|
|
134
|
+
typeof event.message === "string" && event.message ? event.message : "generate: generator reported an error",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (event.event === "done") return event;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, framework-free math for the CozyClay -> ARDY pose bridge.
|
|
3
|
+
* JS twin of /Users/yun/CozyClay/blender-addon/cclay/motion_constraints.py,
|
|
4
|
+
* with the small matrix/quaternion helpers mirrored from
|
|
5
|
+
* /Users/yun/CozyClay/blender-addon/cclay/motion_retarget.py so the math is
|
|
6
|
+
* not re-derived (keep the two files in lockstep when the Python changes).
|
|
7
|
+
*
|
|
8
|
+
* Matrices are row-major 3x3 nested arrays, quaternions are w-first
|
|
9
|
+
* [w, x, y, z] (Blender/motion_constraints order, NOT three.js [x, y, z, w]).
|
|
10
|
+
* The math contract, from PoseTrackBuilder.step in motion_retarget.py:
|
|
11
|
+
*
|
|
12
|
+
* basis = Rb^T @ L @ Rb (ARDY local rotation L -> Blender basis)
|
|
13
|
+
* L = Rb @ basis @ Rb^T (inverse, this module)
|
|
14
|
+
*
|
|
15
|
+
* where Rb is the target bone's armature-space rest rotation (3x3). Every
|
|
16
|
+
* function is total: shape mismatches raise a clear Error instead of leaking
|
|
17
|
+
* NaN. No file IO, no three.js, no UI — pure functions over plain arrays.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { CSKEL27_PARENTS } from "./cskel27.js";
|
|
21
|
+
|
|
22
|
+
// Quaternion norm below which a rotation matrix cannot be built. Mirrors
|
|
23
|
+
// motion_retarget._QUATERNION_NORM_EPSILON so the inverse path rejects the
|
|
24
|
+
// same degenerate inputs the forward path would have produced.
|
|
25
|
+
const QUATERNION_NORM_EPSILON = 1e-12;
|
|
26
|
+
|
|
27
|
+
function isFiniteNumber(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requireQuat(q, label) {
|
|
32
|
+
if (!Array.isArray(q) || q.length !== 4 || !q.every(isFiniteNumber)) {
|
|
33
|
+
throw new Error(`${label} must be a [w, x, y, z] quaternion of four finite numbers`);
|
|
34
|
+
}
|
|
35
|
+
return q;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireMat3(m, label) {
|
|
39
|
+
if (
|
|
40
|
+
!Array.isArray(m) ||
|
|
41
|
+
m.length !== 3 ||
|
|
42
|
+
!m.every(
|
|
43
|
+
(row) => Array.isArray(row) && row.length === 3 && row.every(isFiniteNumber)
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(`${label} must be a row-major 3x3 matrix of finite numbers`);
|
|
47
|
+
}
|
|
48
|
+
return m;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requireVec3(v, label) {
|
|
52
|
+
if (!Array.isArray(v) || v.length !== 3 || !v.every(isFiniteNumber)) {
|
|
53
|
+
throw new Error(`${label} must be an [x, y, z] vector of three finite numbers`);
|
|
54
|
+
}
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireJointMatrices(values, label) {
|
|
59
|
+
if (!Array.isArray(values) || values.length !== CSKEL27_PARENTS.length) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${label} must hold ${CSKEL27_PARENTS.length} entries (one per cskel27 joint)`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
values.forEach((m, index) => requireMat3(m, `${label}[${index}]`));
|
|
65
|
+
return values;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requireJointVectors(values, label) {
|
|
69
|
+
if (!Array.isArray(values) || values.length !== CSKEL27_PARENTS.length) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${label} must hold ${CSKEL27_PARENTS.length} entries (one per cskel27 joint)`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
values.forEach((v, index) => requireVec3(v, `${label}[${index}]`));
|
|
75
|
+
return values;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Joint indices ordered so every parent precedes its children. Mirrors
|
|
79
|
+
// motion_constraints._topological_order.
|
|
80
|
+
function topologicalOrder() {
|
|
81
|
+
const placed = new Set();
|
|
82
|
+
const order = [];
|
|
83
|
+
CSKEL27_PARENTS.forEach((parent, index) => {
|
|
84
|
+
if (parent === null) {
|
|
85
|
+
placed.add(index);
|
|
86
|
+
order.push(index);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
order.sort((a, b) => a - b);
|
|
90
|
+
while (order.length < CSKEL27_PARENTS.length) {
|
|
91
|
+
let progressed = false;
|
|
92
|
+
for (let index = 0; index < CSKEL27_PARENTS.length; index += 1) {
|
|
93
|
+
if (!placed.has(index) && placed.has(CSKEL27_PARENTS[index])) {
|
|
94
|
+
order.push(index);
|
|
95
|
+
placed.add(index);
|
|
96
|
+
progressed = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!progressed) {
|
|
100
|
+
throw new Error("cskel27 parent table is not a tree");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return order;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const CSKEL27_TOPOLOGICAL_ORDER = topologicalOrder();
|
|
107
|
+
|
|
108
|
+
/** Row-major 3x3 matrix product a @ b (motion_retarget._mat_mul). */
|
|
109
|
+
export function matMul(a, b) {
|
|
110
|
+
requireMat3(a, "matMul(a, b): a");
|
|
111
|
+
requireMat3(b, "matMul(a, b): b");
|
|
112
|
+
return [
|
|
113
|
+
[
|
|
114
|
+
a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0],
|
|
115
|
+
a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1],
|
|
116
|
+
a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2],
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0],
|
|
120
|
+
a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1],
|
|
121
|
+
a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2],
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0],
|
|
125
|
+
a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1],
|
|
126
|
+
a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2],
|
|
127
|
+
],
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Row-major 3x3 transpose (motion_retarget._mat_transpose). */
|
|
132
|
+
export function matTranspose(m) {
|
|
133
|
+
requireMat3(m, "matTranspose(m): m");
|
|
134
|
+
return [
|
|
135
|
+
[m[0][0], m[1][0], m[2][0]],
|
|
136
|
+
[m[0][1], m[1][1], m[2][1]],
|
|
137
|
+
[m[0][2], m[1][2], m[2][2]],
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Row-major 3x3 matrix times column vector (motion_retarget._mat_vec).
|
|
142
|
+
function matVec(m, v) {
|
|
143
|
+
return [
|
|
144
|
+
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
|
|
145
|
+
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
|
|
146
|
+
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Normalized [w, x, y, z] -> row-major 3x3 rotation matrix.
|
|
152
|
+
* Twin of motion_constraints._quat_to_mat (which is the inverse of
|
|
153
|
+
* motion_retarget._mat_to_quat). A zero-length or non-finite quaternion is
|
|
154
|
+
* rejected explicitly rather than producing a garbage matrix.
|
|
155
|
+
*/
|
|
156
|
+
export function quatToMat(q) {
|
|
157
|
+
const [wRaw, xRaw, yRaw, zRaw] = requireQuat(q, "quatToMat(q): q");
|
|
158
|
+
const normSq = wRaw * wRaw + xRaw * xRaw + yRaw * yRaw + zRaw * zRaw;
|
|
159
|
+
if (!Number.isFinite(normSq) || normSq <= QUATERNION_NORM_EPSILON) {
|
|
160
|
+
throw new Error("quatToMat(q): quaternion is zero-length and cannot represent a rotation");
|
|
161
|
+
}
|
|
162
|
+
const inverseNorm = 1 / Math.sqrt(normSq);
|
|
163
|
+
const w = wRaw * inverseNorm;
|
|
164
|
+
const x = xRaw * inverseNorm;
|
|
165
|
+
const y = yRaw * inverseNorm;
|
|
166
|
+
const z = zRaw * inverseNorm;
|
|
167
|
+
// Standard quaternion -> rotation matrix (right-handed, row-major).
|
|
168
|
+
return [
|
|
169
|
+
[1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)],
|
|
170
|
+
[2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)],
|
|
171
|
+
[2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)],
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Row-major 3x3 rotation matrix -> normalized [w, x, y, z].
|
|
177
|
+
* Twin of motion_retarget._mat_to_quat (Shepperd's method). A non-finite or
|
|
178
|
+
* degenerate matrix raises instead of returning a garbage quaternion.
|
|
179
|
+
*/
|
|
180
|
+
export function matToQuat(m) {
|
|
181
|
+
requireMat3(m, "matToQuat(m): m");
|
|
182
|
+
let quat;
|
|
183
|
+
const trace = m[0][0] + m[1][1] + m[2][2];
|
|
184
|
+
if (trace > 0) {
|
|
185
|
+
const s = Math.sqrt(trace + 1) * 2;
|
|
186
|
+
quat = [0.25 * s, (m[2][1] - m[1][2]) / s, (m[0][2] - m[2][0]) / s, (m[1][0] - m[0][1]) / s];
|
|
187
|
+
} else if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {
|
|
188
|
+
const s = Math.sqrt(1 + m[0][0] - m[1][1] - m[2][2]) * 2;
|
|
189
|
+
quat = [(m[2][1] - m[1][2]) / s, 0.25 * s, (m[0][1] + m[1][0]) / s, (m[0][2] + m[2][0]) / s];
|
|
190
|
+
} else if (m[1][1] > m[2][2]) {
|
|
191
|
+
const s = Math.sqrt(1 + m[1][1] - m[0][0] - m[2][2]) * 2;
|
|
192
|
+
quat = [(m[0][2] - m[2][0]) / s, (m[0][1] + m[1][0]) / s, 0.25 * s, (m[1][2] + m[2][1]) / s];
|
|
193
|
+
} else {
|
|
194
|
+
const s = Math.sqrt(1 + m[2][2] - m[0][0] - m[1][1]) * 2;
|
|
195
|
+
quat = [(m[1][0] - m[0][1]) / s, (m[0][2] + m[2][0]) / s, (m[1][2] + m[2][1]) / s, 0.25 * s];
|
|
196
|
+
}
|
|
197
|
+
const squaredNorm = quat.reduce((sum, component) => sum + component * component, 0);
|
|
198
|
+
if (!Number.isFinite(squaredNorm) || squaredNorm <= QUATERNION_NORM_EPSILON) {
|
|
199
|
+
throw new Error("matToQuat(m): matrix is degenerate and cannot represent a rotation");
|
|
200
|
+
}
|
|
201
|
+
const inverseNorm = 1 / Math.sqrt(squaredNorm);
|
|
202
|
+
const normalized = quat.map((component) => component * inverseNorm);
|
|
203
|
+
if (!normalized.every(isFiniteNumber)) {
|
|
204
|
+
throw new Error("matToQuat(m): matrix is degenerate and cannot represent a rotation");
|
|
205
|
+
}
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* ARDY local rotation L -> Blender pose basis: basis = Rb^T @ L @ Rb.
|
|
211
|
+
* (the forward direction of the PoseTrackBuilder.step contract).
|
|
212
|
+
*/
|
|
213
|
+
export function localToBasis(L, Rb) {
|
|
214
|
+
const local = requireMat3(L, "localToBasis(L, Rb): L");
|
|
215
|
+
const rest = requireMat3(Rb, "localToBasis(L, Rb): Rb");
|
|
216
|
+
const restT = matTranspose(rest);
|
|
217
|
+
return matMul(restT, matMul(local, rest));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Blender pose basis -> ARDY local rotation L: L = Rb @ basis @ Rb^T.
|
|
222
|
+
* (the inverse of the PoseTrackBuilder.step contract).
|
|
223
|
+
*/
|
|
224
|
+
export function basisToLocal(basis, Rb) {
|
|
225
|
+
const b = requireMat3(basis, "basisToLocal(basis, Rb): basis");
|
|
226
|
+
const rest = requireMat3(Rb, "basisToLocal(basis, Rb): Rb");
|
|
227
|
+
const restT = matTranspose(rest);
|
|
228
|
+
return matMul(rest, matMul(b, restT));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Blender pose-bone quaternion (normalized [w, x, y, z]) -> ARDY local
|
|
233
|
+
* rotation L, given the bone's armature-space rest rotation Rb.
|
|
234
|
+
* Twin of motion_constraints.basis_quaternion_to_local_rotation:
|
|
235
|
+
* basis = quatToMat(quat); L = Rb @ basis @ Rb^T.
|
|
236
|
+
*/
|
|
237
|
+
export function basisQuaternionToLocalRotation(quatWFirst, restRotation) {
|
|
238
|
+
const basis = quatToMat(quatWFirst);
|
|
239
|
+
return basisToLocal(basis, restRotation);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Accumulate per-joint local rotations into global ones down the tree.
|
|
244
|
+
* Twin of motion_constraints.global_rotations. Returns 27 fresh matrices in
|
|
245
|
+
* cskel27 index order.
|
|
246
|
+
*/
|
|
247
|
+
export function globalRotations(localRotations) {
|
|
248
|
+
const locals = requireJointMatrices(localRotations, "globalRotations(localRotations)");
|
|
249
|
+
const out = new Array(CSKEL27_PARENTS.length);
|
|
250
|
+
for (const index of CSKEL27_TOPOLOGICAL_ORDER) {
|
|
251
|
+
const parent = CSKEL27_PARENTS[index];
|
|
252
|
+
out[index] =
|
|
253
|
+
parent === null
|
|
254
|
+
? locals[index].map((row) => row.slice())
|
|
255
|
+
: matMul(out[parent], locals[index]);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Constant bone vectors, each expressed in its parent's global frame, derived
|
|
262
|
+
* from one posed frame: offset = R_parent^T @ (child - parent).
|
|
263
|
+
* Twin of motion_constraints.derive_bone_offsets (argument order swapped: JS
|
|
264
|
+
* call sites pass the source positions first). Deriving them from the clip
|
|
265
|
+
* rather than hardcoding a rest skeleton keeps this correct if ARDY ever
|
|
266
|
+
* reproportions cskel27.
|
|
267
|
+
*/
|
|
268
|
+
export function deriveBoneOffsets(jointPositions, localRotations) {
|
|
269
|
+
const positions = requireJointVectors(jointPositions, "deriveBoneOffsets(jointPositions, ...)");
|
|
270
|
+
const rotations = globalRotations(localRotations);
|
|
271
|
+
const offsets = new Array(CSKEL27_PARENTS.length);
|
|
272
|
+
for (let index = 0; index < offsets.length; index += 1) {
|
|
273
|
+
offsets[index] = [0, 0, 0];
|
|
274
|
+
}
|
|
275
|
+
for (const index of CSKEL27_TOPOLOGICAL_ORDER) {
|
|
276
|
+
const parent = CSKEL27_PARENTS[index];
|
|
277
|
+
if (parent === null) continue;
|
|
278
|
+
const delta = [
|
|
279
|
+
positions[index][0] - positions[parent][0],
|
|
280
|
+
positions[index][1] - positions[parent][1],
|
|
281
|
+
positions[index][2] - positions[parent][2],
|
|
282
|
+
];
|
|
283
|
+
offsets[index] = matVec(matTranspose(rotations[parent]), delta);
|
|
284
|
+
}
|
|
285
|
+
return offsets;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Joint positions in npz space for one posed frame: the mapping that answers
|
|
290
|
+
* "where does the ARDY skeleton put this joint when posed like these
|
|
291
|
+
* rotations". Twin of motion_constraints.forward_kinematics. `rootPosition`
|
|
292
|
+
* defaults to the npz origin when the caller only needs relative pose.
|
|
293
|
+
*/
|
|
294
|
+
export function forwardKinematics(localRotations, boneOffsets, rootPosition = [0, 0, 0]) {
|
|
295
|
+
const rotations = globalRotations(localRotations);
|
|
296
|
+
requireJointVectors(boneOffsets, "forwardKinematics(...): boneOffsets");
|
|
297
|
+
const root = requireVec3(rootPosition, "forwardKinematics(...): rootPosition");
|
|
298
|
+
const positions = new Array(CSKEL27_PARENTS.length);
|
|
299
|
+
for (const index of CSKEL27_TOPOLOGICAL_ORDER) {
|
|
300
|
+
const parent = CSKEL27_PARENTS[index];
|
|
301
|
+
if (parent === null) {
|
|
302
|
+
positions[index] = root.slice();
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const moved = matVec(rotations[parent], boneOffsets[index]);
|
|
306
|
+
positions[index] = [
|
|
307
|
+
positions[parent][0] + moved[0],
|
|
308
|
+
positions[parent][1] + moved[1],
|
|
309
|
+
positions[parent][2] + moved[2],
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
return positions;
|
|
313
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoreSkeleton27 neutral-pose joint positions (metres, Y-up), exported
|
|
3
|
+
* verbatim from the box (`CoreSkeleton27().neutral_joints`, ~/ardy) — the
|
|
4
|
+
* same reference the viser demo's Mixamo avatar uses as ardy_bind_positions
|
|
5
|
+
* in compute_bone_transforms. Index order == CSKEL27_JOINTS. The hips sit
|
|
6
|
+
* at the origin and the lowest joint (toes) at y = -0.9544128.
|
|
7
|
+
*
|
|
8
|
+
* Positional skinning needs this: a bone's bind offset is measured
|
|
9
|
+
* against the ARDY neutral joint, not against the rig's own rest pose.
|
|
10
|
+
*/
|
|
11
|
+
export const CSKEL27_NEUTRAL = [
|
|
12
|
+
[0.0, 0.0, 0.0], // Hips
|
|
13
|
+
[0.0, 0.0709891, -0.0473261], // Spine
|
|
14
|
+
[0.0, 0.1642033, -0.0637623], // Spine1
|
|
15
|
+
[0.0, 0.2584953, -0.0720118], // Spine2
|
|
16
|
+
[0.0, 0.3531475, -0.0720119], // Spine3
|
|
17
|
+
[0.0, 0.6016096, -0.0365176], // Neck
|
|
18
|
+
[0.0, 0.7297793, -0.0139179], // Head
|
|
19
|
+
[-0.0319949, 0.5259196, -0.0186873], // RightShoulder
|
|
20
|
+
[-0.1909029, 0.5259195, -0.0186873], // RightArm
|
|
21
|
+
[-0.4863389, 0.5259194, -0.0186873], // RightForeArm
|
|
22
|
+
[-0.7189909, 0.5259193, -0.0186873], // RightHand
|
|
23
|
+
[-0.7886024, 0.5259193, -0.0186873], // RightHandEnd
|
|
24
|
+
[-0.7468355, 0.5073563, 0.0277204], // RightHandThumb1
|
|
25
|
+
[0.0319949, 0.5259196, -0.0186873], // LeftShoulder
|
|
26
|
+
[0.1909029, 0.5259196, -0.0186873], // LeftArm
|
|
27
|
+
[0.4863389, 0.5259196, -0.0186873], // LeftForeArm
|
|
28
|
+
[0.7189909, 0.5259196, -0.0186873], // LeftHand
|
|
29
|
+
[0.7886024, 0.5259196, -0.0186873], // LeftHandEnd
|
|
30
|
+
[0.7468355, 0.5073565, 0.0277204], // LeftHandThumb1
|
|
31
|
+
[-0.0949182, -0.0277289, 0.0], // RightUpLeg
|
|
32
|
+
[-0.0949182, -0.4398469, 0.0], // RightLeg
|
|
33
|
+
[-0.0949182, -0.8959379, 0.0], // RightFoot
|
|
34
|
+
[-0.0949182, -0.9544128, 0.1606583], // RightToeBase
|
|
35
|
+
[0.0949182, -0.0277289, 0.0], // LeftUpLeg
|
|
36
|
+
[0.0949182, -0.4398469, 0.0], // LeftLeg
|
|
37
|
+
[0.0949182, -0.8959379, 0.0], // LeftFoot
|
|
38
|
+
[0.0949182, -0.9544128, 0.1606583], // LeftToeBase
|
|
39
|
+
];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cskel27 joint order and parent hierarchy, transcribed verbatim from the
|
|
3
|
+
* ARDY reference implementation:
|
|
4
|
+
* - CSKEL27_JOINTS mirrors `motion_retarget.CSKEL27_JOINTS`
|
|
5
|
+
* (/Users/yun/CozyClay/blender-addon/cclay/motion_retarget.py)
|
|
6
|
+
* - CSKEL27_PARENTS mirrors `motion_constraints._PARENT_BY_NAME` /
|
|
7
|
+
* `CSKEL27_PARENTS` (/Users/yun/CozyClay/blender-addon/cclay/motion_constraints.py)
|
|
8
|
+
*
|
|
9
|
+
* Index in CSKEL27_JOINTS == joint index in the npz arrays
|
|
10
|
+
* (local_rot_mats[:, i], posed_joints[:, i]). motion_retarget deliberately
|
|
11
|
+
* carries no hierarchy (it is rotation-only); the parent table was verified
|
|
12
|
+
* numerically in the Python repo (deriving offsets from one frame and running
|
|
13
|
+
* FK across every other frame reproduces the npz's own posed_joints to
|
|
14
|
+
* ~2.8e-07, float32 serialization noise) — a wrong parent moves that error to
|
|
15
|
+
* whole npz units immediately, so this table must stay exact.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The 27 ARDY core-skeleton joints in index order. */
|
|
19
|
+
export const CSKEL27_JOINTS = [
|
|
20
|
+
"Hips", "Spine", "Spine1", "Spine2", "Spine3", "Neck", "Head",
|
|
21
|
+
"RightShoulder", "RightArm", "RightForeArm", "RightHand",
|
|
22
|
+
"RightHandEnd", "RightHandThumb1",
|
|
23
|
+
"LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand",
|
|
24
|
+
"LeftHandEnd", "LeftHandThumb1",
|
|
25
|
+
"RightUpLeg", "RightLeg", "RightFoot", "RightToeBase",
|
|
26
|
+
"LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const JOINT_INDEX = Object.fromEntries(
|
|
30
|
+
CSKEL27_JOINTS.map((name, index) => [name, index])
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Parent per joint by name, transcribed verbatim from
|
|
34
|
+
// motion_constraints._PARENT_BY_NAME. The root (Hips) has no parent.
|
|
35
|
+
const PARENT_BY_NAME = {
|
|
36
|
+
"Hips": null,
|
|
37
|
+
"Spine": "Hips", "Spine1": "Spine", "Spine2": "Spine1", "Spine3": "Spine2",
|
|
38
|
+
"Neck": "Spine3", "Head": "Neck",
|
|
39
|
+
"RightShoulder": "Spine3", "RightArm": "RightShoulder",
|
|
40
|
+
"RightForeArm": "RightArm", "RightHand": "RightForeArm",
|
|
41
|
+
"RightHandEnd": "RightHand", "RightHandThumb1": "RightHand",
|
|
42
|
+
"LeftShoulder": "Spine3", "LeftArm": "LeftShoulder",
|
|
43
|
+
"LeftForeArm": "LeftArm", "LeftHand": "LeftForeArm",
|
|
44
|
+
"LeftHandEnd": "LeftHand", "LeftHandThumb1": "LeftHand",
|
|
45
|
+
"RightUpLeg": "Hips", "RightLeg": "RightUpLeg",
|
|
46
|
+
"RightFoot": "RightLeg", "RightToeBase": "RightFoot",
|
|
47
|
+
"LeftUpLeg": "Hips", "LeftLeg": "LeftUpLeg",
|
|
48
|
+
"LeftFoot": "LeftLeg", "LeftToeBase": "LeftFoot",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Parent joint index per cskel27 index; the root (Hips) is null. */
|
|
52
|
+
export const CSKEL27_PARENTS = CSKEL27_JOINTS.map(
|
|
53
|
+
(name) => (PARENT_BY_NAME[name] === null ? null : JOINT_INDEX[PARENT_BY_NAME[name]])
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/** The 19 joints CozyClay edits (src/poses.js POSE_BONES), as cskel27 names. */
|
|
57
|
+
export const COZYCLAY_BONES = [
|
|
58
|
+
"Hips", "Spine", "Spine1", "Neck", "Head",
|
|
59
|
+
"LeftShoulder", "RightShoulder", "LeftArm", "RightArm",
|
|
60
|
+
"LeftForeArm", "RightForeArm", "LeftHand", "RightHand",
|
|
61
|
+
"LeftUpLeg", "RightUpLeg", "LeftLeg", "RightLeg",
|
|
62
|
+
"LeftFoot", "RightFoot",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** CozyClay bone name -> cskel27 joint index. */
|
|
66
|
+
export const COZYCLAY_TO_CSKEL27 = Object.fromEntries(
|
|
67
|
+
COZYCLAY_BONES.map((name) => [name, JOINT_INDEX[name]])
|
|
68
|
+
);
|