@melite/formats 0.0.0-stage → 0.1.4-alpha
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 +557 -0
- package/lib/b64.d.ts +2 -0
- package/lib/b64.js +7 -0
- package/lib/bodyPresets.d.ts +40 -0
- package/lib/bodyPresets.js +77 -0
- package/lib/capabilityCards.d.ts +54 -0
- package/lib/capabilityCards.js +127 -0
- package/lib/captionSplitter.d.ts +49 -0
- package/lib/captionSplitter.js +172 -0
- package/lib/cardMutations.d.ts +52 -0
- package/lib/cardMutations.js +67 -0
- package/lib/downloads.d.ts +18 -0
- package/lib/downloads.js +64 -0
- package/lib/extractVideoFrame.d.ts +23 -0
- package/lib/extractVideoFrame.js +114 -0
- package/lib/glbLoad.d.ts +54 -0
- package/lib/glbLoad.js +96 -0
- package/lib/glbSkin.d.ts +14 -0
- package/lib/glbSkin.js +124 -0
- package/lib/handPose.d.ts +46 -0
- package/lib/handPose.js +122 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +26 -0
- package/lib/keyframeExtract.d.ts +46 -0
- package/lib/keyframeExtract.js +89 -0
- package/lib/maskPrep.d.ts +88 -0
- package/lib/maskPrep.js +386 -0
- package/lib/motionConstraints.d.ts +237 -0
- package/lib/motionConstraints.js +369 -0
- package/lib/motionEnvelope.d.ts +56 -0
- package/lib/motionEnvelope.js +59 -0
- package/lib/motionNpz.d.ts +120 -0
- package/lib/motionNpz.js +497 -0
- package/lib/npz.d.ts +24 -0
- package/lib/npz.js +158 -0
- package/lib/openPoseCanvas.d.ts +17 -0
- package/lib/openPoseCanvas.js +90 -0
- package/lib/pngMeta.d.ts +42 -0
- package/lib/pngMeta.js +212 -0
- package/lib/pngMetaInfo.d.ts +14 -0
- package/lib/pngMetaInfo.js +127 -0
- package/lib/poseCodec.d.ts +20 -0
- package/lib/poseCodec.js +61 -0
- package/lib/poseTransforms.d.ts +73 -0
- package/lib/poseTransforms.js +237 -0
- package/lib/poserSkin.d.ts +75 -0
- package/lib/poserSkin.js +126 -0
- package/lib/prompt-tags.json +132 -0
- package/lib/promptTags.d.ts +42 -0
- package/lib/promptTags.js +116 -0
- package/lib/psdExport.d.ts +28 -0
- package/lib/psdExport.js +272 -0
- package/lib/readFile.d.ts +9 -0
- package/lib/readFile.js +16 -0
- package/lib/renderSnapshot.d.ts +34 -0
- package/lib/renderSnapshot.js +55 -0
- package/lib/somaSkin.d.ts +82 -0
- package/lib/somaSkin.js +267 -0
- package/lib/textToPose.d.ts +388 -0
- package/lib/textToPose.js +604 -0
- package/lib/uploadToEstate.d.ts +25 -0
- package/lib/uploadToEstate.js +97 -0
- package/package.json +148 -4
- package/README.md +0 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample a per-frame motion-amplitude envelope from overlapping audio
|
|
3
|
+
* cues. Returns a `number[]` of length `frames`, each value in [0, 1].
|
|
4
|
+
* Returns an empty array if `frames <= 0` or no cues have peaks.
|
|
5
|
+
*/
|
|
6
|
+
export function sampleMotionEnvelope(args) {
|
|
7
|
+
const { segmentStart, segmentDuration, frames, cues } = args;
|
|
8
|
+
if (frames <= 0 || segmentDuration <= 0)
|
|
9
|
+
return [];
|
|
10
|
+
// Pre-filter cues that have peaks AND overlap the segment's time range.
|
|
11
|
+
const usable = cues.filter((c) => {
|
|
12
|
+
if (!c.waveformPeaks || c.waveformPeaks.length === 0)
|
|
13
|
+
return false;
|
|
14
|
+
const cueEnd = c.start + c.duration;
|
|
15
|
+
return cueEnd > segmentStart && c.start < segmentStart + segmentDuration;
|
|
16
|
+
});
|
|
17
|
+
if (usable.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const envelope = new Array(frames);
|
|
20
|
+
for (let i = 0; i < frames; i++) {
|
|
21
|
+
// Sample at the frame midpoint to avoid edge bias.
|
|
22
|
+
const frameTime = segmentStart + ((i + 0.5) / frames) * segmentDuration;
|
|
23
|
+
let sum = 0;
|
|
24
|
+
for (const cue of usable) {
|
|
25
|
+
const relT = frameTime - cue.start;
|
|
26
|
+
if (relT < 0 || relT > cue.duration)
|
|
27
|
+
continue;
|
|
28
|
+
const peaks = cue.waveformPeaks;
|
|
29
|
+
// Map relative time → peak index. Clamp to valid range.
|
|
30
|
+
let idx = Math.floor((relT / cue.duration) * peaks.length);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if (idx < 0)
|
|
35
|
+
idx = 0;
|
|
36
|
+
if (idx >= peaks.length)
|
|
37
|
+
idx = peaks.length - 1;
|
|
38
|
+
const amp = peaks[idx];
|
|
39
|
+
if (typeof amp === 'number' && isFinite(amp)) {
|
|
40
|
+
sum += amp * cue.volume;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Clamp to [0, 1] — overlapping cues sum but must not overflow.
|
|
44
|
+
envelope[i] = sum > 1 ? 1 : sum < 0 ? 0 : sum;
|
|
45
|
+
}
|
|
46
|
+
return envelope;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Normalize an envelope so its peak is exactly 1.0 (if it has any
|
|
50
|
+
* non-zero values). Useful when the raw peaks aren't normalized to
|
|
51
|
+
* full-scale. Returns the input unchanged if all values are 0.
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeEnvelope(env) {
|
|
54
|
+
const peak = env.reduce((m, v) => (v > m ? v : m), 0);
|
|
55
|
+
if (peak <= 0)
|
|
56
|
+
return env;
|
|
57
|
+
return env.map((v) => v / peak);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=motionEnvelope.js.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type NpyArray } from "./npz.js";
|
|
2
|
+
import { loadSomaSkin, type SomaSkin } from "./somaSkin.js";
|
|
3
|
+
export interface MotionNpz {
|
|
4
|
+
/** (T, 77, 3) joint positions, meters, row-major flat. */
|
|
5
|
+
posedJoints: Float32Array;
|
|
6
|
+
/** (T, 77, 3, 3) per-joint rotation matrices, meters space, flat (T, 77, 9). */
|
|
7
|
+
globalRotMats: Float32Array;
|
|
8
|
+
fps: number;
|
|
9
|
+
T: number;
|
|
10
|
+
}
|
|
11
|
+
/** Fetch + parse a kimodo motion NPZ, validating the SOMA-77 contract. */
|
|
12
|
+
export declare function loadMotionNpz(url: string): Promise<MotionNpz>;
|
|
13
|
+
/**
|
|
14
|
+
* Run client-side LBS for every frame: allVerts (T, V, 3) meters.
|
|
15
|
+
* global_rot_mats is (T, 77, 9) per-joint row-major; posed_joints is
|
|
16
|
+
* (T, 77, 3) — both METERS (kimodo's native unit, no conversion).
|
|
17
|
+
*/
|
|
18
|
+
export declare function precomputeDeformedVerts(npz: MotionNpz, skin: SomaSkin): Float32Array;
|
|
19
|
+
/**
|
|
20
|
+
* Chunked async variant of precomputeDeformedVerts. Computes the SAME result
|
|
21
|
+
* but yields to the event loop every `chunkFrames` frames (default 3 frames
|
|
22
|
+
* ≈ 10-20ms per chunk for the 18k-vertex SOMA mesh). This keeps the main
|
|
23
|
+
* thread responsive — the browser can paint, service input, and run rAF
|
|
24
|
+
* between chunks instead of freezing for 1-3 seconds.
|
|
25
|
+
*
|
|
26
|
+
* Set `outputCm` to true to produce centimeters (PoseViewer3D's unit) — this
|
|
27
|
+
* folds the ×100 scale INTO the chunk loop so there's no separate scaling pass.
|
|
28
|
+
*/
|
|
29
|
+
export declare function precomputeDeformedVertsChunked(npz: MotionNpz, skin: SomaSkin, outputCm?: boolean, chunkFrames?: number): Promise<Float32Array>;
|
|
30
|
+
/** Scale a meters Float32Array by `factor` (×100 → cm) into a new array. */
|
|
31
|
+
export declare function scaledArray(src: Float32Array, factor: number): Float32Array;
|
|
32
|
+
/** Base64 of the raw float32 bytes (optionally scaled to cm first). */
|
|
33
|
+
export declare function vertsToB64(verts: Float32Array, toCm?: boolean): string;
|
|
34
|
+
export declare function jointsToCm(jointsM: Float32Array): Float32Array;
|
|
35
|
+
export declare function jointsFromCm(jointsCm: Float32Array): Float32Array;
|
|
36
|
+
export declare const AZIMUTH_PRESETS: readonly [{
|
|
37
|
+
readonly angle: 0;
|
|
38
|
+
readonly cameraView: "az-0";
|
|
39
|
+
readonly label: "Front";
|
|
40
|
+
}, {
|
|
41
|
+
readonly angle: 45;
|
|
42
|
+
readonly cameraView: "az-45";
|
|
43
|
+
readonly label: "F-R";
|
|
44
|
+
}, {
|
|
45
|
+
readonly angle: 90;
|
|
46
|
+
readonly cameraView: "az-90";
|
|
47
|
+
readonly label: "Right";
|
|
48
|
+
}, {
|
|
49
|
+
readonly angle: 135;
|
|
50
|
+
readonly cameraView: "az-135";
|
|
51
|
+
readonly label: "B-R";
|
|
52
|
+
}, {
|
|
53
|
+
readonly angle: 180;
|
|
54
|
+
readonly cameraView: "az-180";
|
|
55
|
+
readonly label: "Back";
|
|
56
|
+
}, {
|
|
57
|
+
readonly angle: 225;
|
|
58
|
+
readonly cameraView: "az-225";
|
|
59
|
+
readonly label: "B-L";
|
|
60
|
+
}, {
|
|
61
|
+
readonly angle: 270;
|
|
62
|
+
readonly cameraView: "az-270";
|
|
63
|
+
readonly label: "Left";
|
|
64
|
+
}, {
|
|
65
|
+
readonly angle: 315;
|
|
66
|
+
readonly cameraView: "az-315";
|
|
67
|
+
readonly label: "F-L";
|
|
68
|
+
}];
|
|
69
|
+
export type MotionAzimuth = (typeof AZIMUTH_PRESETS)[number]["cameraView"];
|
|
70
|
+
/** Camera position for an azimuth view — target is (0, 0.9, 0). */
|
|
71
|
+
export declare function azimuthCameraPos(view: MotionAzimuth): [number, number, number];
|
|
72
|
+
/** Step ±1 preset around the 8-angle ring (wrap-around). Used by the motion
|
|
73
|
+
* card's rotate arrows. All presets sit on the same ring (AZ_DIST/AZ_HEIGHT),
|
|
74
|
+
* so stepping is a hard camera snap — no zoom/height preservation needed. */
|
|
75
|
+
export declare function stepAzimuth(view: MotionAzimuth, dir: 1 | -1): MotionAzimuth;
|
|
76
|
+
export declare const COCO_TO_SOMA_IDX: number[];
|
|
77
|
+
/**
|
|
78
|
+
* Project SOMA-77 3D joints to COCO-18 3D joints (view-independent, meters).
|
|
79
|
+
* Input: (77, 3) meters; output: (18, 3) meters — [x,y,z] per COCO joint.
|
|
80
|
+
*/
|
|
81
|
+
export declare function soma77ToCoco18(jointsM: Float32Array): Float32Array;
|
|
82
|
+
export declare function crc32(bytes: Uint8Array): number;
|
|
83
|
+
export interface NpzEntry {
|
|
84
|
+
data: Float32Array;
|
|
85
|
+
/** The array's shape (e.g. [T, 77, 3]). Omitted → 1-D [length]. */
|
|
86
|
+
shape?: number[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Serialize a map of named float32 arrays to an NPZ (ZIP of NPY v1 entries).
|
|
90
|
+
* Matches np.savez_compressed's container: ZIP method 8 (deflate-raw),
|
|
91
|
+
* entries named "<key>.npy", real CRC32s. `compress: false` → stored (method
|
|
92
|
+
* 0), like np.savez.
|
|
93
|
+
*/
|
|
94
|
+
export declare function writeNpz(arrays: Record<string, NpzEntry>, compress?: boolean): Promise<Uint8Array<ArrayBuffer>>;
|
|
95
|
+
/** Data URL for the prev_motion_artifact slot (SlotSquare's file-drop shape). */
|
|
96
|
+
export declare function npzToDataUrl(bytes: Uint8Array): string;
|
|
97
|
+
/**
|
|
98
|
+
* Rebuild one frame's global_rot_mats from edited joint positions.
|
|
99
|
+
*
|
|
100
|
+
* Each joint's new rotation is the original rotation composed with the
|
|
101
|
+
* minimal rotation that aligns its longest child-offset direction from the
|
|
102
|
+
* original pose to the edited pose — the standard arc-align reconstruction.
|
|
103
|
+
* Leaf joints (finger/toe tips, no children) keep their original rotation
|
|
104
|
+
* (documented limitation: EndEffector hand/feet anchors are approximate for
|
|
105
|
+
* translate-gizmo edits).
|
|
106
|
+
*
|
|
107
|
+
* Returns patched COPIES of both arrays (full length, meters). The caller
|
|
108
|
+
* truncates to `frameIdx + 1` when the edited pose should become the chain
|
|
109
|
+
* end.
|
|
110
|
+
*/
|
|
111
|
+
export declare function patchFrame(posedJoints: Float32Array, // (T, 77, 3) meters
|
|
112
|
+
globalRotMats: Float32Array, // (T, 77, 9) meters
|
|
113
|
+
editedJointsM: Float32Array, // (77, 3) meters — the edited frame's positions
|
|
114
|
+
frameIdx: number): {
|
|
115
|
+
posedJoints: Float32Array;
|
|
116
|
+
globalRotMats: Float32Array;
|
|
117
|
+
};
|
|
118
|
+
export { loadSomaSkin, type SomaSkin };
|
|
119
|
+
export type { NpyArray };
|
|
120
|
+
//# sourceMappingURL=motionNpz.d.ts.map
|
package/lib/motionNpz.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motionNpz — the Motion Card's client-side motion engine.
|
|
3
|
+
*
|
|
4
|
+
* Everything the Motion Card needs beyond the raw NPZ bytes, in one place:
|
|
5
|
+
*
|
|
6
|
+
* • loadMotionNpz — fetch + parse a kimodo motion NPZ (meters)
|
|
7
|
+
* • precomputeDeformedVerts — LBS every frame onto the neutral SOMAX skin
|
|
8
|
+
* (extracted from MotionViewer — single source)
|
|
9
|
+
* • vertsToB64 / cm helpers — the PoseViewer3D boundary (cm + base64)
|
|
10
|
+
* • AZIMUTH_PRESETS — the 8 export angles (Blender convention,
|
|
11
|
+
* 0 = front / 90 = right, matching the vnccs
|
|
12
|
+
* flow's front_mesh…front_left_mesh names)
|
|
13
|
+
* • soma77ToCoco18 — TS port of the poser nodes' SOMA→COCO-18
|
|
14
|
+
* mapping + ear extrapolation (library/
|
|
15
|
+
* soma_to_coco.py — the DWPose / vnccs contract)
|
|
16
|
+
* • writeNpz — a browser-side NPZ WRITER (lib/npz.ts is
|
|
17
|
+
* reader-only): NPY v1 '<f4' + ZIP method 8 with
|
|
18
|
+
* real CRC32 (numpy validates). Feeds the
|
|
19
|
+
* prev_motion_artifact chain slot with an
|
|
20
|
+
* edited pose.
|
|
21
|
+
* • patchFrame — rotation reconstruction: given edited joint
|
|
22
|
+
* positions for one frame, rebuild global_rot_mats
|
|
23
|
+
* by arc-aligning each joint's child direction
|
|
24
|
+
* along the SOMA-77 hierarchy.
|
|
25
|
+
*
|
|
26
|
+
* Units: METERS everywhere (kimodo's native unit), except functions that
|
|
27
|
+
* cross the PoseViewer3D boundary, which is centimeters (its mesh, skeleton
|
|
28
|
+
* and gizmos all apply CM_TO_M internally). Callers convert explicitly.
|
|
29
|
+
*/
|
|
30
|
+
import * as THREE from '@melite/three-client/client';
|
|
31
|
+
import { parseNpz } from "./npz.js";
|
|
32
|
+
import { loadSomaSkin, deformFrame } from "./somaSkin.js";
|
|
33
|
+
import { encodeBufferToB64, SOMA77_BONE_CONNECTIONS } from "./poseCodec.js";
|
|
34
|
+
const J = 77; // SOMA-77 joints
|
|
35
|
+
/** Fetch + parse a kimodo motion NPZ, validating the SOMA-77 contract. */
|
|
36
|
+
export async function loadMotionNpz(url) {
|
|
37
|
+
const res = await fetch(url);
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
throw new Error(`Failed to fetch motion asset: ${res.status}`);
|
|
40
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
41
|
+
const arrays = await parseNpz(bytes);
|
|
42
|
+
const joints = arrays.get("posed_joints");
|
|
43
|
+
const rotmats = arrays.get("global_rot_mats");
|
|
44
|
+
if (!joints || joints.shape.length < 2) {
|
|
45
|
+
throw new Error(`NPZ missing 'posed_joints' (got: ${[...arrays.keys()].join(", ") || "empty"})`);
|
|
46
|
+
}
|
|
47
|
+
if (!rotmats || rotmats.shape.length < 3) {
|
|
48
|
+
// No `|| "empty"` fallback here (unlike the joints arm above): the
|
|
49
|
+
// joints gate just passed, so `posed_joints` is always among the keys
|
|
50
|
+
|
|
51
|
+
throw new Error(`NPZ missing 'global_rot_mats' (got: ${[...arrays.keys()].join(", ")})`);
|
|
52
|
+
}
|
|
53
|
+
const T = joints.shape[0];
|
|
54
|
+
if (joints.shape[1] !== 77 || joints.shape[2] !== 3) {
|
|
55
|
+
throw new Error(`Unexpected posed_joints shape ${joints.shape.join("×")} (expected T×77×3)`);
|
|
56
|
+
}
|
|
57
|
+
const fpsArr = arrays.get("fps");
|
|
58
|
+
const fps = fpsArr && fpsArr.data.length > 0 ? fpsArr.data[0] : 30;
|
|
59
|
+
return {
|
|
60
|
+
posedJoints: joints.data,
|
|
61
|
+
globalRotMats: rotmats.data,
|
|
62
|
+
fps,
|
|
63
|
+
T,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// ── LBS precompute (extracted verbatim from MotionViewer) ──────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Run client-side LBS for every frame: allVerts (T, V, 3) meters.
|
|
69
|
+
* global_rot_mats is (T, 77, 9) per-joint row-major; posed_joints is
|
|
70
|
+
* (T, 77, 3) — both METERS (kimodo's native unit, no conversion).
|
|
71
|
+
*/
|
|
72
|
+
export function precomputeDeformedVerts(npz, skin) {
|
|
73
|
+
const V = skin.vertexCount;
|
|
74
|
+
const allVerts = new Float32Array(npz.T * V * 3);
|
|
75
|
+
for (let t = 0; t < npz.T; t++) {
|
|
76
|
+
deformFrame(npz.globalRotMats.subarray(t * J * 9, (t + 1) * J * 9), npz.posedJoints.subarray(t * J * 3, (t + 1) * J * 3), skin, allVerts.subarray(t * V * 3, (t + 1) * V * 3));
|
|
77
|
+
}
|
|
78
|
+
return allVerts;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Chunked async variant of precomputeDeformedVerts. Computes the SAME result
|
|
82
|
+
* but yields to the event loop every `chunkFrames` frames (default 3 frames
|
|
83
|
+
* ≈ 10-20ms per chunk for the 18k-vertex SOMA mesh). This keeps the main
|
|
84
|
+
* thread responsive — the browser can paint, service input, and run rAF
|
|
85
|
+
* between chunks instead of freezing for 1-3 seconds.
|
|
86
|
+
*
|
|
87
|
+
* Set `outputCm` to true to produce centimeters (PoseViewer3D's unit) — this
|
|
88
|
+
* folds the ×100 scale INTO the chunk loop so there's no separate scaling pass.
|
|
89
|
+
*/
|
|
90
|
+
export async function precomputeDeformedVertsChunked(npz, skin, outputCm = false, chunkFrames = 10) {
|
|
91
|
+
const V = skin.vertexCount;
|
|
92
|
+
const stride = V * 3;
|
|
93
|
+
const allVerts = new Float32Array(npz.T * stride);
|
|
94
|
+
for (let t = 0; t < npz.T; t += chunkFrames) {
|
|
95
|
+
const end = Math.min(t + chunkFrames, npz.T);
|
|
96
|
+
for (let frame = t; frame < end; frame++) {
|
|
97
|
+
const off = frame * stride;
|
|
98
|
+
deformFrame(npz.globalRotMats.subarray(frame * J * 9, (frame + 1) * J * 9), npz.posedJoints.subarray(frame * J * 3, (frame + 1) * J * 3), skin, allVerts.subarray(off, off + stride));
|
|
99
|
+
if (outputCm) {
|
|
100
|
+
for (let i = off; i < off + stride; i++)
|
|
101
|
+
allVerts[i] *= 100;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Yield between chunks so the browser can paint + service input.
|
|
105
|
+
if (end < npz.T) {
|
|
106
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return allVerts;
|
|
110
|
+
}
|
|
111
|
+
// ── PoseViewer3D boundary (cm + base64) ────────────────────────────────────
|
|
112
|
+
/** Scale a meters Float32Array by `factor` (×100 → cm) into a new array. */
|
|
113
|
+
export function scaledArray(src, factor) {
|
|
114
|
+
const out = new Float32Array(src.length);
|
|
115
|
+
for (let i = 0; i < src.length; i++)
|
|
116
|
+
out[i] = src[i] * factor;
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/** Base64 of the raw float32 bytes (optionally scaled to cm first). */
|
|
120
|
+
export function vertsToB64(verts, toCm = false) {
|
|
121
|
+
const src = toCm ? scaledArray(verts, 100) : verts;
|
|
122
|
+
return encodeBufferToB64(new Uint8Array(src.buffer, src.byteOffset, src.byteLength));
|
|
123
|
+
}
|
|
124
|
+
export function jointsToCm(jointsM) {
|
|
125
|
+
return scaledArray(jointsM, 100);
|
|
126
|
+
}
|
|
127
|
+
export function jointsFromCm(jointsCm) {
|
|
128
|
+
return scaledArray(jointsCm, 0.01);
|
|
129
|
+
}
|
|
130
|
+
// ── Azimuth presets (Blender convention — matches character_vnccs.yaml) ────
|
|
131
|
+
// 0 = front (+Z camera), 90 = right (+X), 180 = back, 270 = left. The camera
|
|
132
|
+
// circles the figure at the same distance/height as the pose studio's
|
|
133
|
+
// "front" view so the 8 angles are the exact familiar ring.
|
|
134
|
+
export const AZIMUTH_PRESETS = [
|
|
135
|
+
{ angle: 0, cameraView: "az-0", label: "Front" },
|
|
136
|
+
{ angle: 45, cameraView: "az-45", label: "F-R" },
|
|
137
|
+
{ angle: 90, cameraView: "az-90", label: "Right" },
|
|
138
|
+
{ angle: 135, cameraView: "az-135", label: "B-R" },
|
|
139
|
+
{ angle: 180, cameraView: "az-180", label: "Back" },
|
|
140
|
+
{ angle: 225, cameraView: "az-225", label: "B-L" },
|
|
141
|
+
{ angle: 270, cameraView: "az-270", label: "Left" },
|
|
142
|
+
{ angle: 315, cameraView: "az-315", label: "F-L" },
|
|
143
|
+
];
|
|
144
|
+
const AZ_DIST = 3.4;
|
|
145
|
+
const AZ_HEIGHT = 1.2;
|
|
146
|
+
/** Camera position for an azimuth view — target is (0, 0.9, 0). */
|
|
147
|
+
export function azimuthCameraPos(view) {
|
|
148
|
+
const az = AZIMUTH_PRESETS.find((p) => p.cameraView === view) ?? AZIMUTH_PRESETS[0];
|
|
149
|
+
const rad = (az.angle * Math.PI) / 180;
|
|
150
|
+
return [Math.sin(rad) * AZ_DIST, AZ_HEIGHT, Math.cos(rad) * AZ_DIST];
|
|
151
|
+
}
|
|
152
|
+
/** Step ±1 preset around the 8-angle ring (wrap-around). Used by the motion
|
|
153
|
+
* card's rotate arrows. All presets sit on the same ring (AZ_DIST/AZ_HEIGHT),
|
|
154
|
+
* so stepping is a hard camera snap — no zoom/height preservation needed. */
|
|
155
|
+
export function stepAzimuth(view, dir) {
|
|
156
|
+
const i = AZIMUTH_PRESETS.findIndex((p) => p.cameraView === view);
|
|
157
|
+
const next = ((i === -1 ? 0 : i) + dir + AZIMUTH_PRESETS.length) % AZIMUTH_PRESETS.length;
|
|
158
|
+
return AZIMUTH_PRESETS[next].cameraView;
|
|
159
|
+
}
|
|
160
|
+
// ── SOMA-77 → COCO-18 (port of custom_nodes/melite-poser-nodes/library/soma_to_coco.py) ──
|
|
161
|
+
// COCO-18 ordering matches poser.py::_KP_ORDER (the DWPose / vnccs contract).
|
|
162
|
+
// COCO "shoulder" = the glenohumeral socket (SOMA LeftArm/RightArm 12/40),
|
|
163
|
+
// NOT the clavicle (LeftShoulder/RightShoulder 11/39).
|
|
164
|
+
export const COCO_TO_SOMA_IDX = [
|
|
165
|
+
6, // 0 nose → Head
|
|
166
|
+
4, // 1 neck → Neck1
|
|
167
|
+
40, // 2 r_shoulder → RightArm (socket)
|
|
168
|
+
41, // 3 r_elbow → RightForeArm
|
|
169
|
+
42, // 4 r_wrist → RightHand
|
|
170
|
+
12, // 5 l_shoulder → LeftArm (socket)
|
|
171
|
+
13, // 6 l_elbow → LeftForeArm
|
|
172
|
+
14, // 7 l_wrist → LeftHand
|
|
173
|
+
72, // 8 r_hip → RightLeg
|
|
174
|
+
73, // 9 r_knee → RightShin
|
|
175
|
+
74, // 10 r_ankle → RightFoot
|
|
176
|
+
67, // 11 l_hip → LeftLeg
|
|
177
|
+
68, // 12 l_knee → LeftShin
|
|
178
|
+
69, // 13 l_ankle → LeftFoot
|
|
179
|
+
10, // 14 r_eye → RightEye
|
|
180
|
+
9, // 15 l_eye → LeftEye
|
|
181
|
+
]; // 16/17 ears — extrapolated below (SOMA-77 has no ear joints)
|
|
182
|
+
// Ear extrapolation constants (soma_to_coco.py) — ears sit ~7cm behind and
|
|
183
|
+
// slightly below the eyes at ~half the interocular distance lateral to the
|
|
184
|
+
// eye center, scaled by interocular distance so it adapts to body size.
|
|
185
|
+
const EAR_LATERAL_RATIO = 0.65;
|
|
186
|
+
const EAR_VERTICAL_DROP_RATIO = 0.2;
|
|
187
|
+
/**
|
|
188
|
+
* Project SOMA-77 3D joints to COCO-18 3D joints (view-independent, meters).
|
|
189
|
+
* Input: (77, 3) meters; output: (18, 3) meters — [x,y,z] per COCO joint.
|
|
190
|
+
*/
|
|
191
|
+
export function soma77ToCoco18(jointsM) {
|
|
192
|
+
const out = new Float32Array(18 * 3);
|
|
193
|
+
for (let i = 0; i < 16; i++) {
|
|
194
|
+
const s = COCO_TO_SOMA_IDX[i] * 3;
|
|
195
|
+
out[i * 3] = jointsM[s];
|
|
196
|
+
out[i * 3 + 1] = jointsM[s + 1];
|
|
197
|
+
out[i * 3 + 2] = jointsM[s + 2];
|
|
198
|
+
}
|
|
199
|
+
// Ears: right_ear = right_eye + lateral + downward; left_ear = left_eye − lateral.
|
|
200
|
+
const le = 9 * 3; // LeftEye
|
|
201
|
+
const re = 10 * 3; // RightEye
|
|
202
|
+
const iox = jointsM[re] - jointsM[le];
|
|
203
|
+
const ioy = jointsM[re + 1] - jointsM[le + 1];
|
|
204
|
+
const ioz = jointsM[re + 2] - jointsM[le + 2];
|
|
205
|
+
const interocular = Math.max(Math.hypot(iox, ioy, ioz), 1e-6);
|
|
206
|
+
const ux = iox / interocular;
|
|
207
|
+
const uy = ioy / interocular;
|
|
208
|
+
const uz = ioz / interocular;
|
|
209
|
+
const hx = jointsM[6 * 3] - jointsM[4 * 3]; // Head − Neck1 (up direction)
|
|
210
|
+
const hy = jointsM[6 * 3 + 1] - jointsM[4 * 3 + 1];
|
|
211
|
+
const hz = jointsM[6 * 3 + 2] - jointsM[4 * 3 + 2];
|
|
212
|
+
const hLen = Math.max(Math.hypot(hx, hy, hz), 1e-6);
|
|
213
|
+
const upx = hx / hLen;
|
|
214
|
+
const upy = hy / hLen;
|
|
215
|
+
const upz = hz / hLen;
|
|
216
|
+
const lat = EAR_LATERAL_RATIO * interocular;
|
|
217
|
+
const drop = EAR_VERTICAL_DROP_RATIO * interocular;
|
|
218
|
+
// COCO 16 = r_ear, 17 = l_ear (subject's right = SOMA −X side... the eye
|
|
219
|
+
// vector points from left eye → right eye, so +lateral lands on the
|
|
220
|
+
// subject's right — matches soma_to_coco.py exactly).
|
|
221
|
+
out[16 * 3] = jointsM[re] + ux * lat - upx * drop;
|
|
222
|
+
out[16 * 3 + 1] = jointsM[re + 1] + uy * lat - upy * drop;
|
|
223
|
+
out[16 * 3 + 2] = jointsM[re + 2] + uz * lat - upz * drop;
|
|
224
|
+
out[17 * 3] = jointsM[le] - ux * lat - upx * drop;
|
|
225
|
+
out[17 * 3 + 1] = jointsM[le + 1] - uy * lat - upy * drop;
|
|
226
|
+
out[17 * 3 + 2] = jointsM[le + 2] - uz * lat - upz * drop;
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
// ── NPZ writer (lib/npz.ts is reader-only — this is the missing writer) ────
|
|
230
|
+
/** CRC-32 (IEEE 802.3, polynomial 0xEDB88320) — numpy validates NPZ CRCs. */
|
|
231
|
+
const CRC_TABLE = (() => {
|
|
232
|
+
const table = new Uint32Array(256);
|
|
233
|
+
for (let n = 0; n < 256; n++) {
|
|
234
|
+
let c = n;
|
|
235
|
+
for (let k = 0; k < 8; k++)
|
|
236
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
237
|
+
table[n] = c >>> 0;
|
|
238
|
+
}
|
|
239
|
+
return table;
|
|
240
|
+
})();
|
|
241
|
+
export function crc32(bytes) {
|
|
242
|
+
let c = 0xffffffff;
|
|
243
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
244
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
245
|
+
}
|
|
246
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
247
|
+
}
|
|
248
|
+
function npyHeader(shape) {
|
|
249
|
+
// numpy's own header string shape: {'descr': '<f4', 'fortran_order':
|
|
250
|
+
// False, 'shape': (T, 77, 3), } — 1-D arrays get the trailing comma.
|
|
251
|
+
const shapeStr = `(${shape.join(", ")}${shape.length === 1 ? "," : ""})`;
|
|
252
|
+
let dict = `{'descr': '<f4', 'fortran_order': False, 'shape': ${shapeStr}, }`;
|
|
253
|
+
let header = new TextEncoder().encode(dict);
|
|
254
|
+
// Pad so the full v1 header (6 magic + 2 version + 2 length + header) is
|
|
255
|
+
// 64-byte aligned — numpy requires this for the data offset. Trailing
|
|
256
|
+
// whitespace after the dict is ignored by the reader.
|
|
257
|
+
const align = (64 - ((10 + header.length) % 64)) % 64;
|
|
258
|
+
const padded = new Uint8Array(header.length + align + 1);
|
|
259
|
+
padded.set(header);
|
|
260
|
+
padded[padded.length - 1] = 0x0a; // trailing newline
|
|
261
|
+
dict = new TextDecoder().decode(padded);
|
|
262
|
+
header = new TextEncoder().encode(dict);
|
|
263
|
+
const out = new Uint8Array(10 + header.length);
|
|
264
|
+
out.set([0x93, 0x4e, 0x55, 0x4d, 0x50, 0x59, 0x01, 0x00]); // \x93NUMPY v1.0
|
|
265
|
+
out[8] = header.length & 0xff;
|
|
266
|
+
out[9] = (header.length >> 8) & 0xff;
|
|
267
|
+
out.set(header, 10);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
/** Build one NPY v1 float32 array blob. */
|
|
271
|
+
function npyFloat32(data, shape) {
|
|
272
|
+
const header = npyHeader(shape);
|
|
273
|
+
const out = new Uint8Array(header.length + data.byteLength);
|
|
274
|
+
out.set(header);
|
|
275
|
+
out.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), header.length);
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
const ZIP_LOCAL = 0x04034b50;
|
|
279
|
+
const ZIP_CENTRAL = 0x02014b50;
|
|
280
|
+
const ZIP_EOCD = 0x06054b50;
|
|
281
|
+
function writeU16(out, off, v) {
|
|
282
|
+
out[off] = v & 0xff;
|
|
283
|
+
out[off + 1] = (v >> 8) & 0xff;
|
|
284
|
+
}
|
|
285
|
+
function writeU32(out, off, v) {
|
|
286
|
+
out[off] = v & 0xff;
|
|
287
|
+
out[off + 1] = (v >> 8) & 0xff;
|
|
288
|
+
out[off + 2] = (v >> 16) & 0xff;
|
|
289
|
+
out[off + 3] = (v >>> 24) & 0xff;
|
|
290
|
+
}
|
|
291
|
+
async function deflateRaw(bytes) {
|
|
292
|
+
const cs = new CompressionStream("deflate-raw");
|
|
293
|
+
const stream = new Blob([bytes]).stream().pipeThrough(cs);
|
|
294
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Serialize a map of named float32 arrays to an NPZ (ZIP of NPY v1 entries).
|
|
298
|
+
* Matches np.savez_compressed's container: ZIP method 8 (deflate-raw),
|
|
299
|
+
* entries named "<key>.npy", real CRC32s. `compress: false` → stored (method
|
|
300
|
+
* 0), like np.savez.
|
|
301
|
+
*/
|
|
302
|
+
export async function writeNpz(arrays, compress = true) {
|
|
303
|
+
const entries = [];
|
|
304
|
+
for (const [key, entry] of Object.entries(arrays)) {
|
|
305
|
+
const shape = entry.shape ?? [entry.data.length];
|
|
306
|
+
const raw = npyFloat32(entry.data, shape);
|
|
307
|
+
const name = new TextEncoder().encode(`${key}.npy`);
|
|
308
|
+
const crc = crc32(raw);
|
|
309
|
+
let method = 0;
|
|
310
|
+
let payload = raw;
|
|
311
|
+
if (compress) {
|
|
312
|
+
const deflated = await deflateRaw(raw);
|
|
313
|
+
if (deflated.length < raw.length) {
|
|
314
|
+
method = 8;
|
|
315
|
+
payload = deflated;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
entries.push({ name, data: payload, crc, method, localOff: 0 });
|
|
319
|
+
}
|
|
320
|
+
// Layout: local headers + data, then central directory, then EOCD.
|
|
321
|
+
let offset = 0;
|
|
322
|
+
for (const e of entries) {
|
|
323
|
+
e.localOff = offset;
|
|
324
|
+
offset += 30 + e.name.length + e.data.length;
|
|
325
|
+
}
|
|
326
|
+
const cdSize = entries.reduce((acc, e) => acc + 46 + e.name.length, 0);
|
|
327
|
+
const total = offset + cdSize + 22;
|
|
328
|
+
const out = new Uint8Array(total);
|
|
329
|
+
let p = 0;
|
|
330
|
+
for (const e of entries) {
|
|
331
|
+
writeU32(out, p, ZIP_LOCAL);
|
|
332
|
+
writeU16(out, p + 4, 20); // version needed
|
|
333
|
+
writeU16(out, p + 6, 0); // flags
|
|
334
|
+
writeU16(out, p + 8, e.method);
|
|
335
|
+
writeU16(out, p + 10, 0); // mod time
|
|
336
|
+
writeU16(out, p + 12, 0); // mod date
|
|
337
|
+
writeU32(out, p + 14, e.crc);
|
|
338
|
+
writeU32(out, p + 18, e.data.length);
|
|
339
|
+
writeU32(out, p + 22, e.data.length);
|
|
340
|
+
writeU16(out, p + 26, e.name.length);
|
|
341
|
+
writeU16(out, p + 28, 0); // extra len
|
|
342
|
+
out.set(e.name, p + 30);
|
|
343
|
+
out.set(e.data, p + 30 + e.name.length);
|
|
344
|
+
p += 30 + e.name.length + e.data.length;
|
|
345
|
+
}
|
|
346
|
+
const cdStart = p;
|
|
347
|
+
for (const e of entries) {
|
|
348
|
+
writeU32(out, p, ZIP_CENTRAL);
|
|
349
|
+
writeU16(out, p + 4, 20); // version made by
|
|
350
|
+
writeU16(out, p + 6, 20); // version needed
|
|
351
|
+
writeU16(out, p + 8, 0); // flags
|
|
352
|
+
writeU16(out, p + 10, e.method);
|
|
353
|
+
writeU16(out, p + 12, 0); // mod time
|
|
354
|
+
writeU16(out, p + 14, 0); // mod date
|
|
355
|
+
writeU32(out, p + 16, e.crc);
|
|
356
|
+
writeU32(out, p + 20, e.data.length);
|
|
357
|
+
writeU32(out, p + 24, e.data.length);
|
|
358
|
+
writeU16(out, p + 28, e.name.length);
|
|
359
|
+
writeU16(out, p + 30, 0); // extra
|
|
360
|
+
writeU16(out, p + 32, 0); // comment
|
|
361
|
+
writeU16(out, p + 34, 0); // disk start
|
|
362
|
+
writeU16(out, p + 36, 0); // internal attrs
|
|
363
|
+
writeU32(out, p + 38, 0); // external attrs
|
|
364
|
+
writeU32(out, p + 42, e.localOff);
|
|
365
|
+
out.set(e.name, p + 46);
|
|
366
|
+
p += 46 + e.name.length;
|
|
367
|
+
}
|
|
368
|
+
const cdEnd = p;
|
|
369
|
+
writeU32(out, p, ZIP_EOCD);
|
|
370
|
+
writeU16(out, p + 4, 0); // disk
|
|
371
|
+
writeU16(out, p + 6, 0); // cd disk
|
|
372
|
+
writeU16(out, p + 8, entries.length);
|
|
373
|
+
writeU16(out, p + 10, entries.length);
|
|
374
|
+
writeU32(out, p + 12, cdEnd - cdStart);
|
|
375
|
+
writeU32(out, p + 16, cdStart);
|
|
376
|
+
writeU16(out, p + 20, 0); // comment len
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
/** Data URL for the prev_motion_artifact slot (SlotSquare's file-drop shape). */
|
|
380
|
+
export function npzToDataUrl(bytes) {
|
|
381
|
+
return `data:application/octet-stream;base64,${encodeBufferToB64(bytes)}`;
|
|
382
|
+
}
|
|
383
|
+
// ── Rotation reconstruction (Phase B — drag-refine) ────────────────────────
|
|
384
|
+
// SOMA-77 child tables, built from the canonical bone connections (same
|
|
385
|
+
// derivation as PoseEditMode's SOMA77_PARENT — text-to-pose is the source).
|
|
386
|
+
const SOMA77_CHILDREN = (() => {
|
|
387
|
+
const parent = new Array(J).fill(-1);
|
|
388
|
+
for (const [p, c] of SOMA77_BONE_CONNECTIONS)
|
|
389
|
+
parent[c] = p;
|
|
390
|
+
const children = Array.from({ length: J }, () => []);
|
|
391
|
+
for (let i = 0; i < J; i++) {
|
|
392
|
+
const p = parent[i];
|
|
393
|
+
if (p >= 0)
|
|
394
|
+
children[p].push(i);
|
|
395
|
+
}
|
|
396
|
+
return children;
|
|
397
|
+
})();
|
|
398
|
+
/**
|
|
399
|
+
* Rebuild one frame's global_rot_mats from edited joint positions.
|
|
400
|
+
*
|
|
401
|
+
* Each joint's new rotation is the original rotation composed with the
|
|
402
|
+
* minimal rotation that aligns its longest child-offset direction from the
|
|
403
|
+
* original pose to the edited pose — the standard arc-align reconstruction.
|
|
404
|
+
* Leaf joints (finger/toe tips, no children) keep their original rotation
|
|
405
|
+
* (documented limitation: EndEffector hand/feet anchors are approximate for
|
|
406
|
+
* translate-gizmo edits).
|
|
407
|
+
*
|
|
408
|
+
* Returns patched COPIES of both arrays (full length, meters). The caller
|
|
409
|
+
* truncates to `frameIdx + 1` when the edited pose should become the chain
|
|
410
|
+
* end.
|
|
411
|
+
*/
|
|
412
|
+
export function patchFrame(posedJoints, // (T, 77, 3) meters
|
|
413
|
+
globalRotMats, // (T, 77, 9) meters
|
|
414
|
+
editedJointsM, // (77, 3) meters — the edited frame's positions
|
|
415
|
+
frameIdx) {
|
|
416
|
+
const T = posedJoints.length / (J * 3);
|
|
417
|
+
const f = Math.max(0, Math.min(frameIdx, T - 1));
|
|
418
|
+
const jb = f * J * 3;
|
|
419
|
+
const rb = f * J * 9;
|
|
420
|
+
const joints = new Float32Array(posedJoints);
|
|
421
|
+
const rots = new Float32Array(globalRotMats);
|
|
422
|
+
// Original frame positions (meters) for the offset directions.
|
|
423
|
+
const origPos = new Float32Array(J * 3);
|
|
424
|
+
for (let j = 0; j < J; j++) {
|
|
425
|
+
origPos[j * 3] = posedJoints[jb + j * 3];
|
|
426
|
+
origPos[j * 3 + 1] = posedJoints[jb + j * 3 + 1];
|
|
427
|
+
origPos[j * 3 + 2] = posedJoints[jb + j * 3 + 2];
|
|
428
|
+
}
|
|
429
|
+
// Write the edited positions (meters) into the copy.
|
|
430
|
+
joints.set(editedJointsM, jb);
|
|
431
|
+
const q = new THREE.Quaternion();
|
|
432
|
+
const d0 = new THREE.Vector3();
|
|
433
|
+
const d1 = new THREE.Vector3();
|
|
434
|
+
const axis = new THREE.Vector3();
|
|
435
|
+
for (let j = 0; j < J; j++) {
|
|
436
|
+
// Reference child = the one with the longest ORIGINAL offset from j.
|
|
437
|
+
let bestChild = -1;
|
|
438
|
+
let bestLen = 1e-4;
|
|
439
|
+
for (const c of SOMA77_CHILDREN[j]) {
|
|
440
|
+
const cx = origPos[c * 3] - origPos[j * 3];
|
|
441
|
+
const cy = origPos[c * 3 + 1] - origPos[j * 3 + 1];
|
|
442
|
+
const cz = origPos[c * 3 + 2] - origPos[j * 3 + 2];
|
|
443
|
+
const len = Math.hypot(cx, cy, cz);
|
|
444
|
+
if (len > bestLen) {
|
|
445
|
+
bestLen = len;
|
|
446
|
+
bestChild = c;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (bestChild < 0)
|
|
450
|
+
continue; // leaf — keep original rotation
|
|
451
|
+
d0.set((origPos[bestChild * 3] - origPos[j * 3]) / bestLen, (origPos[bestChild * 3 + 1] - origPos[j * 3 + 1]) / bestLen, (origPos[bestChild * 3 + 2] - origPos[j * 3 + 2]) / bestLen);
|
|
452
|
+
const dx = editedJointsM[bestChild * 3] - editedJointsM[j * 3];
|
|
453
|
+
const dy = editedJointsM[bestChild * 3 + 1] - editedJointsM[j * 3 + 1];
|
|
454
|
+
const dz = editedJointsM[bestChild * 3 + 2] - editedJointsM[j * 3 + 2];
|
|
455
|
+
const len = Math.hypot(dx, dy, dz);
|
|
456
|
+
if (len < 1e-6)
|
|
457
|
+
continue; // edited child collapsed onto the joint — keep
|
|
458
|
+
d1.set(dx / len, dy / len, dz / len);
|
|
459
|
+
const dot = THREE.MathUtils.clamp(d0.dot(d1), -1, 1);
|
|
460
|
+
const angle = Math.acos(dot);
|
|
461
|
+
if (angle < 1e-4)
|
|
462
|
+
continue; // direction unchanged — keep original rotation
|
|
463
|
+
axis.crossVectors(d0, d1);
|
|
464
|
+
if (axis.lengthSq() < 1e-10) {
|
|
465
|
+
// 180° flip — pick an arbitrary perpendicular axis.
|
|
466
|
+
axis.set(1, 0, 0).cross(d0);
|
|
467
|
+
if (axis.lengthSq() < 1e-6)
|
|
468
|
+
axis.set(0, 1, 0).cross(d0);
|
|
469
|
+
}
|
|
470
|
+
axis.normalize();
|
|
471
|
+
q.setFromAxisAngle(axis, angle);
|
|
472
|
+
// R_new = Q_align · R_orig (global-space alignment composed with the
|
|
473
|
+
// original global rotation). The rotmat block is ROW-major and
|
|
474
|
+
// Matrix4.set takes its 16 args in row-major groups of 4 — place the
|
|
475
|
+
// block directly, no transpose (deformFrame consumes the same
|
|
476
|
+
// convention: v' = R·v).
|
|
477
|
+
const qRot = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().set(globalRotMats[rb + j * 9], globalRotMats[rb + j * 9 + 1], globalRotMats[rb + j * 9 + 2], 0, globalRotMats[rb + j * 9 + 3], globalRotMats[rb + j * 9 + 4], globalRotMats[rb + j * 9 + 5], 0, globalRotMats[rb + j * 9 + 6], globalRotMats[rb + j * 9 + 7], globalRotMats[rb + j * 9 + 8], 0, 0, 0, 0, 1));
|
|
478
|
+
q.multiply(qRot);
|
|
479
|
+
const mOut = new THREE.Matrix4().makeRotationFromQuaternion(q);
|
|
480
|
+
const e = mOut.elements;
|
|
481
|
+
// THREE stores column-major; our contract is row-major.
|
|
482
|
+
rots[rb + j * 9] = e[0];
|
|
483
|
+
rots[rb + j * 9 + 1] = e[4];
|
|
484
|
+
rots[rb + j * 9 + 2] = e[8];
|
|
485
|
+
rots[rb + j * 9 + 3] = e[1];
|
|
486
|
+
rots[rb + j * 9 + 4] = e[5];
|
|
487
|
+
rots[rb + j * 9 + 5] = e[9];
|
|
488
|
+
rots[rb + j * 9 + 6] = e[2];
|
|
489
|
+
rots[rb + j * 9 + 7] = e[6];
|
|
490
|
+
rots[rb + j * 9 + 8] = e[10];
|
|
491
|
+
}
|
|
492
|
+
return { posedJoints: joints, globalRotMats: rots };
|
|
493
|
+
}
|
|
494
|
+
// Re-export for the skin loader's callers (motion card builds its own
|
|
495
|
+
// SkinMeshData-shaped bind mesh from the neutral skin).
|
|
496
|
+
export { loadSomaSkin };
|
|
497
|
+
//# sourceMappingURL=motionNpz.js.map
|