@flighthq/skeleton3d 0.2.1-next.410.a23f189

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.
@@ -0,0 +1,4 @@
1
+ import type { AabbLike, MeshSkinBindPose, Skeleton3D } from '@flighthq/types';
2
+ export declare function getMeshSkinConservativeBounds(out: AabbLike, bindPose: Readonly<MeshSkinBindPose>, skeleton: Readonly<Skeleton3D>): void;
3
+ export declare function getMeshSkinExactBounds(out: AabbLike, bindPose: Readonly<MeshSkinBindPose>, skeleton: Readonly<Skeleton3D>): void;
4
+ //# sourceMappingURL=getMeshSkinBounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getMeshSkinBounds.d.ts","sourceRoot":"","sources":["../src/getMeshSkinBounds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAoB9E,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,QAAQ,EACb,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EACpC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAC7B,IAAI,CAgFN;AASD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,QAAQ,EACb,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EACpC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAC7B,IAAI,CAwCN"}
@@ -0,0 +1,138 @@
1
+ import { skinVertices } from './skinVertices';
2
+ // Skinned-mesh local-space bounds. Two functions, same signature, trading tightness for cost:
3
+ // getMeshSkinConservativeBounds is the cheap culling default (no per-vertex skinning — expands the
4
+ // rest-pose box by the palette's per-joint motion), getMeshSkinExactBounds is the opt-in accurate
5
+ // path (skins every vertex on the CPU). Both operate on the captured bind pose and the skeleton's
6
+ // already-computed palette (call computeSkeleton3DJointMatrices first), and both write into `out`.
7
+ // Writes a CONSERVATIVE axis-aligned bound of the skinned mesh into `out` without skinning any
8
+ // vertex — the cheap default for culling. Each skinned vertex position is a convex combination
9
+ // (non-negative weights summing to ≤ 1) of its influencing joints' palette-transformed rest
10
+ // positions, so it lies inside the union of {Mⱼ · restBounds} over the referenced joints j; the
11
+ // AABB of that union (this function's result) therefore encloses every skinned vertex. Loose but
12
+ // never wrong: getMeshSkinExactBounds's result is always contained in this one. Cost is the
13
+ // rest-pose box (one pass over bindPose.positions, computed once conceptually) plus one box-transform
14
+ // per referenced joint — no matrix multiply per vertex. Reads the rest bounds into locals before
15
+ // writing, so it is safe when `out` aliases nothing it reads (it holds no input reference to `out`).
16
+ // An empty mesh (no rest positions) yields the empty box (min = +Infinity, max = -Infinity).
17
+ export function getMeshSkinConservativeBounds(out, bindPose, skeleton) {
18
+ const positions = bindPose.positions;
19
+ const restVertexCount = (positions.length / 3) | 0;
20
+ let restMinX = Number.POSITIVE_INFINITY, restMinY = Number.POSITIVE_INFINITY, restMinZ = Number.POSITIVE_INFINITY;
21
+ let restMaxX = Number.NEGATIVE_INFINITY, restMaxY = Number.NEGATIVE_INFINITY, restMaxZ = Number.NEGATIVE_INFINITY;
22
+ for (let v = 0; v < restVertexCount; v++) {
23
+ const p = v * 3;
24
+ const px = positions[p], py = positions[p + 1], pz = positions[p + 2];
25
+ if (px < restMinX)
26
+ restMinX = px;
27
+ if (py < restMinY)
28
+ restMinY = py;
29
+ if (pz < restMinZ)
30
+ restMinZ = pz;
31
+ if (px > restMaxX)
32
+ restMaxX = px;
33
+ if (py > restMaxY)
34
+ restMaxY = py;
35
+ if (pz > restMaxZ)
36
+ restMaxZ = pz;
37
+ }
38
+ // Empty rest pose → empty box, no joints to sweep.
39
+ if (restVertexCount === 0) {
40
+ out.min.x = Number.POSITIVE_INFINITY;
41
+ out.min.y = Number.POSITIVE_INFINITY;
42
+ out.min.z = Number.POSITIVE_INFINITY;
43
+ out.max.x = Number.NEGATIVE_INFINITY;
44
+ out.max.y = Number.NEGATIVE_INFINITY;
45
+ out.max.z = Number.NEGATIVE_INFINITY;
46
+ return;
47
+ }
48
+ // Rest-box center + half-extents, transformed per joint via center + |M|·extent (the same
49
+ // affine-box-transform used by transformAabbByMatrix4), then unioned.
50
+ const cx = (restMinX + restMaxX) * 0.5, cy = (restMinY + restMaxY) * 0.5, cz = (restMinZ + restMaxZ) * 0.5;
51
+ const ex = (restMaxX - restMinX) * 0.5, ey = (restMaxY - restMinY) * 0.5, ez = (restMaxZ - restMinZ) * 0.5;
52
+ const palette = skeleton.jointMatrices;
53
+ const jointCount = (palette.length / 16) | 0;
54
+ const referenced = getReferencedJoints(bindPose.joints, bindPose.weights, jointCount);
55
+ let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, minZ = Number.POSITIVE_INFINITY;
56
+ let maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY, maxZ = Number.NEGATIVE_INFINITY;
57
+ for (let j = 0; j < jointCount; j++) {
58
+ if (!referenced[j])
59
+ continue;
60
+ const m = j * 16;
61
+ const tcx = palette[m] * cx + palette[m + 4] * cy + palette[m + 8] * cz + palette[m + 12];
62
+ const tcy = palette[m + 1] * cx + palette[m + 5] * cy + palette[m + 9] * cz + palette[m + 13];
63
+ const tcz = palette[m + 2] * cx + palette[m + 6] * cy + palette[m + 10] * cz + palette[m + 14];
64
+ const tex = Math.abs(palette[m]) * ex + Math.abs(palette[m + 4]) * ey + Math.abs(palette[m + 8]) * ez;
65
+ const tey = Math.abs(palette[m + 1]) * ex + Math.abs(palette[m + 5]) * ey + Math.abs(palette[m + 9]) * ez;
66
+ const tez = Math.abs(palette[m + 2]) * ex + Math.abs(palette[m + 6]) * ey + Math.abs(palette[m + 10]) * ez;
67
+ if (tcx - tex < minX)
68
+ minX = tcx - tex;
69
+ if (tcy - tey < minY)
70
+ minY = tcy - tey;
71
+ if (tcz - tez < minZ)
72
+ minZ = tcz - tez;
73
+ if (tcx + tex > maxX)
74
+ maxX = tcx + tex;
75
+ if (tcy + tey > maxY)
76
+ maxY = tcy + tey;
77
+ if (tcz + tez > maxZ)
78
+ maxZ = tcz + tez;
79
+ }
80
+ out.min.x = minX;
81
+ out.min.y = minY;
82
+ out.min.z = minZ;
83
+ out.max.x = maxX;
84
+ out.max.y = maxY;
85
+ out.max.z = maxZ;
86
+ }
87
+ // Writes the EXACT (tight) axis-aligned bound of the skinned mesh into `out` — the opt-in accurate
88
+ // path. Linear-blend-skins every vertex on the CPU via the same kernel skinMeshGeometry uses
89
+ // (writing through bindPose.skinnedPositions scratch, so it allocates nothing), then takes the tight
90
+ // AABB of the skinned positions. Use for precise picking or a leaf-level bound where the conservative
91
+ // sweep is too loose; prefer getMeshSkinConservativeBounds for per-frame culling. An empty mesh
92
+ // yields the empty box (min = +Infinity, max = -Infinity). Overwrites bindPose.skinnedPositions /
93
+ // skinnedNormals as scratch (the same buffers a subsequent skinMeshGeometry would rewrite).
94
+ export function getMeshSkinExactBounds(out, bindPose, skeleton) {
95
+ skinVertices(bindPose.skinnedPositions, bindPose.skinnedNormals, bindPose.positions, bindPose.normals, bindPose.joints, bindPose.weights, skeleton.jointMatrices);
96
+ const skinned = bindPose.skinnedPositions;
97
+ const vertexCount = (skinned.length / 3) | 0;
98
+ let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, minZ = Number.POSITIVE_INFINITY;
99
+ let maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY, maxZ = Number.NEGATIVE_INFINITY;
100
+ for (let v = 0; v < vertexCount; v++) {
101
+ const p = v * 3;
102
+ const px = skinned[p], py = skinned[p + 1], pz = skinned[p + 2];
103
+ if (px < minX)
104
+ minX = px;
105
+ if (py < minY)
106
+ minY = py;
107
+ if (pz < minZ)
108
+ minZ = pz;
109
+ if (px > maxX)
110
+ maxX = px;
111
+ if (py > maxY)
112
+ maxY = py;
113
+ if (pz > maxZ)
114
+ maxZ = pz;
115
+ }
116
+ out.min.x = minX;
117
+ out.min.y = minY;
118
+ out.min.z = minZ;
119
+ out.max.x = maxX;
120
+ out.max.y = maxY;
121
+ out.max.z = maxZ;
122
+ }
123
+ // Marks which palette joints an actual influence references (nonzero weight), so the conservative
124
+ // sweep unions only the joints that can move a vertex — not every joint in the palette. One pass over
125
+ // the static 4-influence binding; out-of-range or zero-weight influences are skipped.
126
+ function getReferencedJoints(joints, weights, jointCount) {
127
+ const referenced = new Uint8Array(jointCount);
128
+ const influenceCount = joints.length;
129
+ for (let k = 0; k < influenceCount; k++) {
130
+ if (weights[k] === 0)
131
+ continue;
132
+ const j = joints[k] | 0;
133
+ if (j >= 0 && j < jointCount)
134
+ referenced[j] = 1;
135
+ }
136
+ return referenced;
137
+ }
138
+ //# sourceMappingURL=getMeshSkinBounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getMeshSkinBounds.js","sourceRoot":"","sources":["../src/getMeshSkinBounds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export * from './getMeshSkinBounds';
2
+ export * from './skeleton3d';
3
+ export * from './skinMeshGeometry';
4
+ export * from './skinVertices';
5
+ export * from './updateMeshSkin';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './getMeshSkinBounds';
2
+ export * from './skeleton3d';
3
+ export * from './skinMeshGeometry';
4
+ export * from './skinVertices';
5
+ export * from './updateMeshSkin';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Matrix4Like, SceneNode, Skeleton3D, Skeleton3DValidationDiagnostic } from '@flighthq/types';
2
+ export declare function cloneSkeleton3D(skeleton: Readonly<Skeleton3D>): Skeleton3D;
3
+ export declare function computeSkeleton3DJointMatrices(skeleton: Readonly<Skeleton3D>): void;
4
+ export declare function createSkeleton3D(joints: SceneNode[], inverseBindMatrices?: Float32Array, names?: readonly string[] | null): Skeleton3D;
5
+ export declare function disposeSkeleton3D(skeleton: Skeleton3D): void;
6
+ export declare function equalsSkeleton3D(a: Readonly<Skeleton3D>, b: Readonly<Skeleton3D>): boolean;
7
+ export declare function getSkeleton3DJointIndexByName(skeleton: Readonly<Skeleton3D>, name: string): number;
8
+ export declare function getSkeleton3DJointWorldMatrix(out: Matrix4Like, skeleton: Readonly<Skeleton3D>, jointIndex: number): boolean;
9
+ export declare function getSkeleton3DJointWorldMatrixByName(out: Matrix4Like, skeleton: Readonly<Skeleton3D>, name: string): boolean;
10
+ export declare function setSkeleton3DBindPose(skeleton: Readonly<Skeleton3D>): void;
11
+ export declare function validateSkeleton3D(skeleton: Readonly<Skeleton3D>): Skeleton3DValidationDiagnostic | null;
12
+ //# sourceMappingURL=skeleton3d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skeleton3d.d.ts","sourceRoot":"","sources":["../src/skeleton3d.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAC;AAE1G,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAS1E;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAQnF;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EAAE,EACnB,mBAAmB,CAAC,EAAE,YAAY,EAClC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAC/B,UAAU,CAUZ;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAG5D;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAe1F;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIlG;AAED,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,UAAU,EAAE,MAAM,GACjB,OAAO,CAKT;AAED,wBAAgB,mCAAmC,CACjD,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,IAAI,EAAE,MAAM,GACX,OAAO,CAET;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAM1E;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,8BAA8B,GAAG,IAAI,CAWxG"}
@@ -0,0 +1,102 @@
1
+ import { copyMatrix4, createMatrix4, inverseMatrix4, multiplyMatrix4 } from '@flighthq/geometry';
2
+ import { getNodeWorldMatrix4 } from '@flighthq/node';
3
+ export function cloneSkeleton3D(skeleton) {
4
+ const clone = {
5
+ inverseBindMatrices: new Float32Array(skeleton.inverseBindMatrices),
6
+ jointMatrices: new Float32Array(skeleton.jointMatrices),
7
+ joints: skeleton.joints.slice(),
8
+ };
9
+ if (skeleton.names != null)
10
+ clone.names = skeleton.names.slice();
11
+ else if (skeleton.names === null)
12
+ clone.names = null;
13
+ return clone;
14
+ }
15
+ export function computeSkeleton3DJointMatrices(skeleton) {
16
+ const { inverseBindMatrices, jointMatrices, joints } = skeleton;
17
+ for (let j = 0; j < joints.length; j++) {
18
+ const base = j * 16;
19
+ for (let i = 0; i < 16; i++)
20
+ _invBind.m[i] = inverseBindMatrices[base + i];
21
+ multiplyMatrix4(_result, getNodeWorldMatrix4(joints[j]), _invBind);
22
+ jointMatrices.set(_result.m, base);
23
+ }
24
+ }
25
+ export function createSkeleton3D(joints, inverseBindMatrices, names) {
26
+ const count = joints.length;
27
+ const skeleton = {
28
+ inverseBindMatrices: inverseBindMatrices ?? new Float32Array(count * 16),
29
+ jointMatrices: new Float32Array(count * 16),
30
+ joints,
31
+ names: names ?? null,
32
+ };
33
+ if (inverseBindMatrices === undefined)
34
+ setSkeleton3DBindPose(skeleton);
35
+ return skeleton;
36
+ }
37
+ export function disposeSkeleton3D(skeleton) {
38
+ skeleton.joints.length = 0;
39
+ skeleton.names = null;
40
+ }
41
+ export function equalsSkeleton3D(a, b) {
42
+ if (a === b)
43
+ return true;
44
+ if (a.joints.length !== b.joints.length)
45
+ return false;
46
+ if (a.inverseBindMatrices.length !== b.inverseBindMatrices.length)
47
+ return false;
48
+ for (let i = 0; i < a.inverseBindMatrices.length; i++) {
49
+ if (a.inverseBindMatrices[i] !== b.inverseBindMatrices[i])
50
+ return false;
51
+ }
52
+ const aNames = a.names ?? null;
53
+ const bNames = b.names ?? null;
54
+ if (aNames === null || bNames === null)
55
+ return aNames === bNames;
56
+ if (aNames.length !== bNames.length)
57
+ return false;
58
+ for (let i = 0; i < aNames.length; i++) {
59
+ if (aNames[i] !== bNames[i])
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ export function getSkeleton3DJointIndexByName(skeleton, name) {
65
+ const { names } = skeleton;
66
+ if (names == null)
67
+ return -1;
68
+ return names.indexOf(name);
69
+ }
70
+ export function getSkeleton3DJointWorldMatrix(out, skeleton, jointIndex) {
71
+ const { joints } = skeleton;
72
+ if (jointIndex < 0 || jointIndex >= joints.length)
73
+ return false;
74
+ copyMatrix4(out, getNodeWorldMatrix4(joints[jointIndex]));
75
+ return true;
76
+ }
77
+ export function getSkeleton3DJointWorldMatrixByName(out, skeleton, name) {
78
+ return getSkeleton3DJointWorldMatrix(out, skeleton, getSkeleton3DJointIndexByName(skeleton, name));
79
+ }
80
+ export function setSkeleton3DBindPose(skeleton) {
81
+ const { inverseBindMatrices, joints } = skeleton;
82
+ for (let j = 0; j < joints.length; j++) {
83
+ inverseMatrix4(_result, getNodeWorldMatrix4(joints[j]));
84
+ inverseBindMatrices.set(_result.m, j * 16);
85
+ }
86
+ }
87
+ export function validateSkeleton3D(skeleton) {
88
+ const jointCount = skeleton.joints.length;
89
+ const expectedInverseBindMatricesLength = jointCount * 16;
90
+ const inverseBindMatricesLength = skeleton.inverseBindMatrices.length;
91
+ if (inverseBindMatricesLength === expectedInverseBindMatricesLength)
92
+ return null;
93
+ return {
94
+ expectedInverseBindMatricesLength,
95
+ inverseBindMatricesLength,
96
+ jointCount,
97
+ message: `Skeleton3D inverseBindMatrices length ${inverseBindMatricesLength} does not match jointCount ${jointCount} * 16 = ${expectedInverseBindMatricesLength}.`,
98
+ };
99
+ }
100
+ const _invBind = createMatrix4();
101
+ const _result = createMatrix4();
102
+ //# sourceMappingURL=skeleton3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skeleton3d.js","sourceRoot":"","sources":["../src/skeleton3d.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { MeshGeometry, MeshSkinBindPose, Skeleton3D } from '@flighthq/types';
2
+ export declare function captureMeshSkinBindPose(geometry: Readonly<MeshGeometry>): MeshSkinBindPose;
3
+ export declare function skinMeshGeometry(geometry: MeshGeometry, skeleton: Readonly<Skeleton3D>, bindPose: Readonly<MeshSkinBindPose>): void;
4
+ //# sourceMappingURL=skinMeshGeometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skinMeshGeometry.d.ts","sourceRoot":"","sources":["../src/skinMeshGeometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAyB,MAAM,iBAAiB,CAAC;AAWzG,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,gBAAgB,CAmD1F;AASD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACnC,IAAI,CAkCN"}
@@ -0,0 +1,98 @@
1
+ import { skinVertices } from './skinVertices';
2
+ // Captures the de-interleaved CPU-skinning inputs for one skinned MeshGeometry: the bind-pose
3
+ // positions/normals and the static joints0/weights0 influences, read out of the interleaved
4
+ // `geometry.vertices` through `geometry.layout` (any offsets, any layout carrying the four
5
+ // semantics). Allocates the SoA arrays plus the reusable skinned-output scratch skinMeshGeometry
6
+ // writes each frame, so the per-frame deform allocates nothing. Call once, before the first deform,
7
+ // and store the result on the geometry's runtime (MeshGeometryRuntime.skinBindPose). Missing
8
+ // joints0/weights0 default those influences to zero (an unskinned vertex stays at its bind pose).
9
+ export function captureMeshSkinBindPose(geometry) {
10
+ const { layout, vertices } = geometry;
11
+ const floatsPerVertex = layout.stride / 4;
12
+ const vertexCount = floatsPerVertex > 0 ? (vertices.length / floatsPerVertex) | 0 : 0;
13
+ const positionOffset = floatOffsetForSemantic(layout, 'position');
14
+ const normalOffset = floatOffsetForSemantic(layout, 'normal');
15
+ const jointsOffset = floatOffsetForSemantic(layout, 'joints0');
16
+ const weightsOffset = floatOffsetForSemantic(layout, 'weights0');
17
+ const positions = new Float32Array(vertexCount * 3);
18
+ const normals = new Float32Array(vertexCount * 3);
19
+ const joints = new Float32Array(vertexCount * 4);
20
+ const weights = new Float32Array(vertexCount * 4);
21
+ for (let v = 0; v < vertexCount; v++) {
22
+ const base = v * floatsPerVertex;
23
+ const p = v * 3;
24
+ const w = v * 4;
25
+ if (positionOffset >= 0) {
26
+ positions[p] = vertices[base + positionOffset];
27
+ positions[p + 1] = vertices[base + positionOffset + 1];
28
+ positions[p + 2] = vertices[base + positionOffset + 2];
29
+ }
30
+ if (normalOffset >= 0) {
31
+ normals[p] = vertices[base + normalOffset];
32
+ normals[p + 1] = vertices[base + normalOffset + 1];
33
+ normals[p + 2] = vertices[base + normalOffset + 2];
34
+ }
35
+ if (jointsOffset >= 0) {
36
+ joints[w] = vertices[base + jointsOffset];
37
+ joints[w + 1] = vertices[base + jointsOffset + 1];
38
+ joints[w + 2] = vertices[base + jointsOffset + 2];
39
+ joints[w + 3] = vertices[base + jointsOffset + 3];
40
+ }
41
+ if (weightsOffset >= 0) {
42
+ weights[w] = vertices[base + weightsOffset];
43
+ weights[w + 1] = vertices[base + weightsOffset + 1];
44
+ weights[w + 2] = vertices[base + weightsOffset + 2];
45
+ weights[w + 3] = vertices[base + weightsOffset + 3];
46
+ }
47
+ }
48
+ return {
49
+ joints,
50
+ normals,
51
+ positions,
52
+ skinnedNormals: new Float32Array(vertexCount * 3),
53
+ skinnedPositions: new Float32Array(vertexCount * 3),
54
+ weights,
55
+ };
56
+ }
57
+ // Deforms a skinned MeshGeometry in place for the current pose: linear-blend-skins the bind pose by
58
+ // the skeleton's already-computed palette (skeleton.jointMatrices — call computeSkeleton3DJointMatrices
59
+ // first) into the bind pose's scratch, writes the skinned positions/normals back into the interleaved
60
+ // `geometry.vertices`, and bumps `geometry.version` so the backends re-upload. `bindPose` is the
61
+ // capture the caller holds on the geometry runtime; only the position/normal channels are rewritten,
62
+ // so tangent/uv0/joints0/weights0 stay intact. Scene-free by design — the mesh-level glue that owns
63
+ // the runtime slot and drives the palette lives in @flighthq/scene (updateMeshSkin).
64
+ export function skinMeshGeometry(geometry, skeleton, bindPose) {
65
+ skinVertices(bindPose.skinnedPositions, bindPose.skinnedNormals, bindPose.positions, bindPose.normals, bindPose.joints, bindPose.weights, skeleton.jointMatrices);
66
+ const { layout, vertices } = geometry;
67
+ const floatsPerVertex = layout.stride / 4;
68
+ const positionOffset = floatOffsetForSemantic(layout, 'position');
69
+ const normalOffset = floatOffsetForSemantic(layout, 'normal');
70
+ const { skinnedNormals, skinnedPositions } = bindPose;
71
+ const vertexCount = (skinnedPositions.length / 3) | 0;
72
+ for (let v = 0; v < vertexCount; v++) {
73
+ const base = v * floatsPerVertex;
74
+ const s = v * 3;
75
+ if (positionOffset >= 0) {
76
+ vertices[base + positionOffset] = skinnedPositions[s];
77
+ vertices[base + positionOffset + 1] = skinnedPositions[s + 1];
78
+ vertices[base + positionOffset + 2] = skinnedPositions[s + 2];
79
+ }
80
+ if (normalOffset >= 0) {
81
+ vertices[base + normalOffset] = skinnedNormals[s];
82
+ vertices[base + normalOffset + 1] = skinnedNormals[s + 1];
83
+ vertices[base + normalOffset + 2] = skinnedNormals[s + 2];
84
+ }
85
+ }
86
+ geometry.version++;
87
+ }
88
+ // The float (not byte) offset of a semantic within an interleaved vertex record, or -1 when the
89
+ // layout does not carry it. byteOffset is a multiple of 4 for the float32 channels skinning reads.
90
+ function floatOffsetForSemantic(layout, semantic) {
91
+ const attributes = layout.attributes;
92
+ for (let i = 0; i < attributes.length; i++) {
93
+ if (attributes[i].semantic === semantic)
94
+ return attributes[i].byteOffset / 4;
95
+ }
96
+ return -1;
97
+ }
98
+ //# sourceMappingURL=skinMeshGeometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skinMeshGeometry.js","sourceRoot":"","sources":["../src/skinMeshGeometry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function skinVertices(outPositions: Float32Array, outNormals: Float32Array, positions: Readonly<Float32Array>, normals: Readonly<Float32Array>, joints: Readonly<ArrayLike<number>>, weights: Readonly<Float32Array>, jointMatrices: Readonly<Float32Array>): void;
2
+ //# sourceMappingURL=skinVertices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skinVertices.d.ts","sourceRoot":"","sources":["../src/skinVertices.ts"],"names":[],"mappings":"AAeA,wBAAgB,YAAY,CAC1B,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,YAAY,EACxB,SAAS,EAAE,QAAQ,CAAC,YAAY,CAAC,EACjC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC/B,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EACnC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC/B,aAAa,EAAE,QAAQ,CAAC,YAAY,CAAC,GACpC,IAAI,CAkDN"}
@@ -0,0 +1,62 @@
1
+ // CPU linear-blend skinning (a.k.a. smooth/matrix-palette skinning). Deforms each vertex on the CPU by the
2
+ // weighted sum of its influencing joints' palette matrices — the same math a GPU vertex shader runs, kept
3
+ // here as a deterministic, GPU-free path for offscreen bakes, physics proxies, and jsdom tests.
4
+ //
5
+ // Buffer layout (all flat typed arrays, tightly packed, vertex-major):
6
+ // positions / outPositions : 3 floats per vertex (x, y, z)
7
+ // normals / outNormals : 3 floats per vertex (x, y, z)
8
+ // joints : 4 joint indices per vertex (the standard 4-influence LBS)
9
+ // weights : 4 blend weights per vertex, aligned with `joints`
10
+ // jointMatrices : the skin palette — 16 column-major floats per joint
11
+ //
12
+ // Positions are transformed as affine points (including translation); normals are transformed by the
13
+ // palette matrix's upper 3×3 only (no translation) and are NOT renormalized — callers that need unit
14
+ // normals (e.g. non-uniform scale in the palette) renormalize afterward. `out*` may alias `positions` /
15
+ // `normals`: each vertex's inputs are read into locals before any output is written.
16
+ export function skinVertices(outPositions, outNormals, positions, normals, joints, weights, jointMatrices) {
17
+ const vertexCount = (positions.length / 3) | 0;
18
+ for (let v = 0; v < vertexCount; v++) {
19
+ const p = v * 3;
20
+ const px = positions[p];
21
+ const py = positions[p + 1];
22
+ const pz = positions[p + 2];
23
+ const nx = normals[p];
24
+ const ny = normals[p + 1];
25
+ const nz = normals[p + 2];
26
+ let opx = 0;
27
+ let opy = 0;
28
+ let opz = 0;
29
+ let onx = 0;
30
+ let ony = 0;
31
+ let onz = 0;
32
+ const w = v * 4;
33
+ for (let k = 0; k < 4; k++) {
34
+ const weight = weights[w + k];
35
+ if (weight === 0)
36
+ continue;
37
+ const m = joints[w + k] * 16;
38
+ const m0 = jointMatrices[m];
39
+ const m1 = jointMatrices[m + 1];
40
+ const m2 = jointMatrices[m + 2];
41
+ const m4 = jointMatrices[m + 4];
42
+ const m5 = jointMatrices[m + 5];
43
+ const m6 = jointMatrices[m + 6];
44
+ const m8 = jointMatrices[m + 8];
45
+ const m9 = jointMatrices[m + 9];
46
+ const m10 = jointMatrices[m + 10];
47
+ opx += weight * (m0 * px + m4 * py + m8 * pz + jointMatrices[m + 12]);
48
+ opy += weight * (m1 * px + m5 * py + m9 * pz + jointMatrices[m + 13]);
49
+ opz += weight * (m2 * px + m6 * py + m10 * pz + jointMatrices[m + 14]);
50
+ onx += weight * (m0 * nx + m4 * ny + m8 * nz);
51
+ ony += weight * (m1 * nx + m5 * ny + m9 * nz);
52
+ onz += weight * (m2 * nx + m6 * ny + m10 * nz);
53
+ }
54
+ outPositions[p] = opx;
55
+ outPositions[p + 1] = opy;
56
+ outPositions[p + 2] = opz;
57
+ outNormals[p] = onx;
58
+ outNormals[p + 1] = ony;
59
+ outNormals[p + 2] = onz;
60
+ }
61
+ }
62
+ //# sourceMappingURL=skinVertices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skinVertices.js","sourceRoot":"","sources":["../src/skinVertices.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Mesh } from '@flighthq/types';
2
+ export declare function updateMeshSkin(mesh: Readonly<Mesh>): void;
3
+ //# sourceMappingURL=updateMeshSkin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateMeshSkin.d.ts","sourceRoot":"","sources":["../src/updateMeshSkin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAiB5C,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAczD"}
@@ -0,0 +1,29 @@
1
+ import { getMeshGeometrySkinBindPose, setMeshGeometrySkinBindPose } from '@flighthq/mesh';
2
+ import { computeSkeleton3DJointMatrices } from './skeleton3d';
3
+ import { captureMeshSkinBindPose, skinMeshGeometry } from './skinMeshGeometry';
4
+ // Deforms a skinned mesh into its geometry for the current joint pose — the explicit per-frame
5
+ // skinning call. Run it after the pose is set (an animation clip applied to the joint nodes, or
6
+ // manual joint transforms) and before rendering: it recomputes the skeleton's palette from the
7
+ // joints' current world transforms, then linear-blend-skins the geometry's bind pose into
8
+ // geometry.vertices and bumps the version so the backends re-upload. A mesh with no skin is a
9
+ // no-op, so calling it over a whole scene's meshes is safe. The bind pose is captured once (lazily)
10
+ // onto the geometry runtime and reused every frame, so steady-state skinning allocates nothing. The
11
+ // version bump alone drives the backend re-upload — no per-frame destroy* of GPU data is needed.
12
+ //
13
+ // Lives here (with a mesh dep) rather than in @flighthq/scene: its only dependencies are the skinning
14
+ // primitives above and mesh's runtime-slot accessors — it never touches the scene graph or animation,
15
+ // so pairing it with the skinning it drives keeps skeleton3d below scene with no cycle.
16
+ export function updateMeshSkin(mesh) {
17
+ const skin = mesh.skin;
18
+ if (skin == null)
19
+ return;
20
+ computeSkeleton3DJointMatrices(skin.skeleton);
21
+ const geometry = mesh.geometry;
22
+ let bindPose = getMeshGeometrySkinBindPose(geometry);
23
+ if (bindPose === null) {
24
+ bindPose = captureMeshSkinBindPose(geometry);
25
+ setMeshGeometrySkinBindPose(geometry, bindPose);
26
+ }
27
+ skinMeshGeometry(geometry, skin.skeleton, bindPose);
28
+ }
29
+ //# sourceMappingURL=updateMeshSkin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateMeshSkin.js","sourceRoot":"","sources":["../src/updateMeshSkin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAG1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE/E,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,0FAA0F;AAC1F,8FAA8F;AAC9F,oGAAoG;AACpG,oGAAoG;AACpG,iGAAiG;AACjG,EAAE;AACF,sGAAsG;AACtG,sGAAsG;AACtG,wFAAwF;AACxF,MAAM,UAAU,cAAc,CAAC,IAAoB;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACvB,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO;IAEzB,8BAA8B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,IAAI,QAAQ,GAAG,2BAA2B,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,QAAQ,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;QAC7C,2BAA2B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@flighthq/skeleton3d",
3
+ "version": "0.2.1-next.410.a23f189",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/skeleton3d"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src/**/*.test.ts",
21
+ "!dist/**/*.test.js",
22
+ "!dist/**/*.test.d.ts",
23
+ "!dist/**/*.test.js.map",
24
+ "!dist/**/*.test.d.ts.map"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -b",
28
+ "clean": "tsc -b --clean",
29
+ "test": "vitest run --config vitest.config.ts",
30
+ "test:watch": "vitest --watch --config vitest.config.ts",
31
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
32
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
+ },
34
+ "dependencies": {
35
+ "@flighthq/geometry": "0.2.1-next.410.a23f189",
36
+ "@flighthq/mesh": "0.2.1-next.410.a23f189",
37
+ "@flighthq/node": "0.2.1-next.410.a23f189",
38
+ "@flighthq/types": "0.2.1-next.410.a23f189"
39
+ },
40
+ "devDependencies": {
41
+ "@flighthq/scene": "*",
42
+ "typescript": "^5.3.0"
43
+ },
44
+ "description": "3D skeleton: joint bind poses and skin-palette (jointWorld * inverseBind) computation over animated SceneNode bones",
45
+ "sideEffects": false
46
+ }
@@ -0,0 +1,145 @@
1
+ import { createAabb } from '@flighthq/geometry';
2
+ import type { MeshSkinBindPose, Skeleton3D } from '@flighthq/types';
3
+
4
+ import { getMeshSkinConservativeBounds, getMeshSkinExactBounds } from './getMeshSkinBounds';
5
+
6
+ // A column-major 4x4 identity / translation, laid out as a 16-float palette entry.
7
+ function identity(): number[] {
8
+ // prettier-ignore
9
+ return [
10
+ 1, 0, 0, 0,
11
+ 0, 1, 0, 0,
12
+ 0, 0, 1, 0,
13
+ 0, 0, 0, 1,
14
+ ];
15
+ }
16
+
17
+ function translation(tx: number, ty: number, tz: number): number[] {
18
+ const m = identity();
19
+ m[12] = tx;
20
+ m[13] = ty;
21
+ m[14] = tz;
22
+ return m;
23
+ }
24
+
25
+ // A minimal bind pose: `count` vertices with the given flat rest positions, each fully weighted to
26
+ // the joint indices in `jointIndices` (aligned by vertex, one influence slot filled). Normals are
27
+ // zero; skinned scratch is allocated to match.
28
+ function bindPoseFor(positions: number[], jointIndices: number[]): MeshSkinBindPose {
29
+ const count = (positions.length / 3) | 0;
30
+ const joints = new Float32Array(count * 4);
31
+ const weights = new Float32Array(count * 4);
32
+ for (let v = 0; v < count; v++) {
33
+ joints[v * 4] = jointIndices[v];
34
+ weights[v * 4] = 1;
35
+ }
36
+ return {
37
+ joints,
38
+ normals: new Float32Array(count * 3),
39
+ positions: new Float32Array(positions),
40
+ skinnedNormals: new Float32Array(count * 3),
41
+ skinnedPositions: new Float32Array(count * 3),
42
+ weights,
43
+ };
44
+ }
45
+
46
+ function skeletonFor(palette: number[]): Skeleton3D {
47
+ return {
48
+ inverseBindMatrices: new Float32Array(palette.length),
49
+ jointMatrices: new Float32Array(palette),
50
+ joints: [],
51
+ };
52
+ }
53
+
54
+ describe('getMeshSkinConservativeBounds', () => {
55
+ it('sweeps the rest box by each referenced joint and unions the results', () => {
56
+ // Two vertices at the origin, one bound to joint 0 (+x by 10), one to joint 1 (+y by 20).
57
+ // The rest box is a single point at the origin, so each joint-transformed box is a point; the
58
+ // union of the two joint translations is the box spanning (0,0,0)..(10,20,0).
59
+ const bindPose = bindPoseFor([0, 0, 0, 0, 0, 0], [0, 1]);
60
+ const skeleton = skeletonFor([...translation(10, 0, 0), ...translation(0, 20, 0)]);
61
+ const out = createAabb();
62
+
63
+ getMeshSkinConservativeBounds(out, bindPose, skeleton);
64
+
65
+ expect(out.min.x).toBeCloseTo(0);
66
+ expect(out.min.y).toBeCloseTo(0);
67
+ expect(out.min.z).toBeCloseTo(0);
68
+ expect(out.max.x).toBeCloseTo(10);
69
+ expect(out.max.y).toBeCloseTo(20);
70
+ expect(out.max.z).toBeCloseTo(0);
71
+ });
72
+
73
+ it('ignores unreferenced palette joints (a far joint no vertex weights does not inflate the box)', () => {
74
+ // Only joint 0 is referenced; joint 1 sits far away but is never weighted.
75
+ const bindPose = bindPoseFor([1, 0, 0], [0]);
76
+ const skeleton = skeletonFor([...translation(0, 0, 0), ...translation(1000, 1000, 1000)]);
77
+ const out = createAabb();
78
+
79
+ getMeshSkinConservativeBounds(out, bindPose, skeleton);
80
+
81
+ expect(out.max.x).toBeCloseTo(1);
82
+ expect(out.max.y).toBeCloseTo(0);
83
+ expect(out.max.z).toBeCloseTo(0);
84
+ });
85
+
86
+ it('yields the empty box for an empty rest pose', () => {
87
+ const bindPose = bindPoseFor([], []);
88
+ const skeleton = skeletonFor(identity());
89
+ const out = createAabb();
90
+
91
+ getMeshSkinConservativeBounds(out, bindPose, skeleton);
92
+
93
+ expect(out.min.x).toBe(Number.POSITIVE_INFINITY);
94
+ expect(out.max.x).toBe(Number.NEGATIVE_INFINITY);
95
+ });
96
+ });
97
+
98
+ describe('getMeshSkinExactBounds', () => {
99
+ it('takes the tight AABB of every CPU-skinned vertex (hand-computed)', () => {
100
+ // A rest box from (0,0,0) to (2,2,0), each corner bound to a different joint.
101
+ // Joint 0 translates +(0,0,0) (identity), joint 1 translates +(10,0,0). The skinned corners are
102
+ // (0,0,0),(2,2,0) under joint 0 and (10,0,0),(12,2,0) under joint 1 → tight box (0,0,0)..(12,2,0).
103
+ const bindPose = bindPoseFor([0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0], [0, 0, 1, 1]);
104
+ const skeleton = skeletonFor([...identity(), ...translation(10, 0, 0)]);
105
+ const out = createAabb();
106
+
107
+ getMeshSkinExactBounds(out, bindPose, skeleton);
108
+
109
+ expect(out.min.x).toBeCloseTo(0);
110
+ expect(out.min.y).toBeCloseTo(0);
111
+ expect(out.min.z).toBeCloseTo(0);
112
+ expect(out.max.x).toBeCloseTo(12);
113
+ expect(out.max.y).toBeCloseTo(2);
114
+ expect(out.max.z).toBeCloseTo(0);
115
+ });
116
+
117
+ it('is contained within the conservative bound (conservative >= exact containment invariant)', () => {
118
+ // A spread of vertices across two rotating/translating joints; the exact box must sit inside the
119
+ // conservative sweep for every referenced joint.
120
+ const positions = [-1, -1, -1, 1, 1, 1, 0.5, -0.5, 0.25, -0.75, 0.6, -0.4];
121
+ const bindPose = bindPoseFor(positions, [0, 1, 0, 1]);
122
+ // Joint 0: scale 2 + translate (3,0,0); joint 1: translate (0,-5,2). Non-trivial so the boxes differ.
123
+ // prettier-ignore
124
+ const palette = [
125
+ 2, 0, 0, 0,
126
+ 0, 2, 0, 0,
127
+ 0, 0, 2, 0,
128
+ 3, 0, 0, 1,
129
+ ...translation(0, -5, 2),
130
+ ];
131
+ const skeleton = skeletonFor(palette);
132
+
133
+ const exact = createAabb();
134
+ const conservative = createAabb();
135
+ getMeshSkinExactBounds(exact, bindPose, skeleton);
136
+ getMeshSkinConservativeBounds(conservative, bindPose, skeleton);
137
+
138
+ expect(conservative.min.x).toBeLessThanOrEqual(exact.min.x + 1e-5);
139
+ expect(conservative.min.y).toBeLessThanOrEqual(exact.min.y + 1e-5);
140
+ expect(conservative.min.z).toBeLessThanOrEqual(exact.min.z + 1e-5);
141
+ expect(conservative.max.x).toBeGreaterThanOrEqual(exact.max.x - 1e-5);
142
+ expect(conservative.max.y).toBeGreaterThanOrEqual(exact.max.y - 1e-5);
143
+ expect(conservative.max.z).toBeGreaterThanOrEqual(exact.max.z - 1e-5);
144
+ });
145
+ });
@@ -0,0 +1,254 @@
1
+ import { createMatrix4, getMatrix4Element, setVector3 } from '@flighthq/geometry';
2
+ import { invalidateNodeLocalTransform } from '@flighthq/node';
3
+ import { createSceneNode } from '@flighthq/scene';
4
+ import type { Skeleton3D } from '@flighthq/types';
5
+
6
+ import {
7
+ cloneSkeleton3D,
8
+ computeSkeleton3DJointMatrices,
9
+ createSkeleton3D,
10
+ disposeSkeleton3D,
11
+ equalsSkeleton3D,
12
+ getSkeleton3DJointIndexByName,
13
+ getSkeleton3DJointWorldMatrix,
14
+ getSkeleton3DJointWorldMatrixByName,
15
+ setSkeleton3DBindPose,
16
+ validateSkeleton3D,
17
+ } from './skeleton3d';
18
+
19
+ describe('cloneSkeleton3D', () => {
20
+ it('copies buffers and arrays into a new identity while sharing joint nodes', () => {
21
+ const joint = createSceneNode();
22
+ setVector3(joint.position, 5, 0, 0);
23
+ invalidateNodeLocalTransform(joint);
24
+ const skeleton = createSkeleton3D([joint], undefined, ['root']);
25
+
26
+ const clone = cloneSkeleton3D(skeleton);
27
+
28
+ expect(clone).not.toBe(skeleton);
29
+ expect(clone.inverseBindMatrices).not.toBe(skeleton.inverseBindMatrices);
30
+ expect(clone.jointMatrices).not.toBe(skeleton.jointMatrices);
31
+ expect(clone.joints).not.toBe(skeleton.joints);
32
+ expect(clone.joints[0]).toBe(joint); // node shared by reference
33
+ expect(Array.from(clone.inverseBindMatrices)).toEqual(Array.from(skeleton.inverseBindMatrices));
34
+ expect(clone.names).toEqual(['root']);
35
+ expect(clone.names).not.toBe(skeleton.names);
36
+ });
37
+
38
+ it('preserves a null names field', () => {
39
+ const clone = cloneSkeleton3D(createSkeleton3D([createSceneNode()]));
40
+ expect(clone.names).toBeNull();
41
+ });
42
+ });
43
+
44
+ describe('computeSkeleton3DJointMatrices', () => {
45
+ it('encodes the joint delta from its bind pose', () => {
46
+ const joint = createSceneNode();
47
+ setVector3(joint.position, 5, 0, 0);
48
+ invalidateNodeLocalTransform(joint);
49
+ const skeleton = createSkeleton3D([joint]); // binds at (5, 0, 0)
50
+ setVector3(joint.position, 5, 3, 0); // move +3 in y
51
+ invalidateNodeLocalTransform(joint);
52
+
53
+ computeSkeleton3DJointMatrices(skeleton);
54
+
55
+ expect(skeleton.jointMatrices[12]).toBeCloseTo(0); // x delta
56
+ expect(skeleton.jointMatrices[13]).toBeCloseTo(3); // y delta
57
+ });
58
+
59
+ it('is alias-safe when jointMatrices and inverseBindMatrices share the same buffer', () => {
60
+ const joint0 = createSceneNode();
61
+ const joint1 = createSceneNode();
62
+ setVector3(joint0.position, 0, 0, 0);
63
+ invalidateNodeLocalTransform(joint0);
64
+ setVector3(joint1.position, 3, 0, 0);
65
+ invalidateNodeLocalTransform(joint1);
66
+
67
+ const temp = createSkeleton3D([joint0, joint1]);
68
+ const shared = new Float32Array(32);
69
+ shared.set(temp.inverseBindMatrices);
70
+
71
+ const aliasSkeleton: Skeleton3D = {
72
+ inverseBindMatrices: shared,
73
+ jointMatrices: shared,
74
+ joints: [joint0, joint1],
75
+ };
76
+
77
+ computeSkeleton3DJointMatrices(aliasSkeleton);
78
+
79
+ expect(shared[12]).toBeCloseTo(0); // joint0 tx
80
+ expect(shared[13]).toBeCloseTo(0); // joint0 ty
81
+ expect(shared[28]).toBeCloseTo(0); // joint1 tx
82
+ expect(shared[29]).toBeCloseTo(0); // joint1 ty
83
+ });
84
+
85
+ it('propagates correctly through a 3-joint chain', () => {
86
+ const joint0 = createSceneNode();
87
+ const joint1 = createSceneNode();
88
+ const joint2 = createSceneNode();
89
+ setVector3(joint0.position, 1, 0, 0);
90
+ invalidateNodeLocalTransform(joint0);
91
+ setVector3(joint1.position, 0, 1, 0);
92
+ invalidateNodeLocalTransform(joint1);
93
+ setVector3(joint2.position, 0, 0, 1);
94
+ invalidateNodeLocalTransform(joint2);
95
+ const skeleton = createSkeleton3D([joint0, joint1, joint2]);
96
+
97
+ setVector3(joint1.position, 0, 2, 0);
98
+ invalidateNodeLocalTransform(joint1);
99
+
100
+ computeSkeleton3DJointMatrices(skeleton);
101
+
102
+ expect(skeleton.jointMatrices[12]).toBeCloseTo(0);
103
+ expect(skeleton.jointMatrices[13]).toBeCloseTo(0);
104
+ expect(skeleton.jointMatrices[14]).toBeCloseTo(0);
105
+
106
+ expect(skeleton.jointMatrices[28]).toBeCloseTo(0);
107
+ expect(skeleton.jointMatrices[29]).toBeCloseTo(1);
108
+ expect(skeleton.jointMatrices[30]).toBeCloseTo(0);
109
+
110
+ expect(skeleton.jointMatrices[44]).toBeCloseTo(0);
111
+ expect(skeleton.jointMatrices[45]).toBeCloseTo(0);
112
+ expect(skeleton.jointMatrices[46]).toBeCloseTo(0);
113
+ });
114
+
115
+ it('yields identity when a joint is at its bind pose', () => {
116
+ const joint = createSceneNode();
117
+ setVector3(joint.position, 5, 0, 0);
118
+ invalidateNodeLocalTransform(joint);
119
+ const skeleton = createSkeleton3D([joint]);
120
+
121
+ computeSkeleton3DJointMatrices(skeleton);
122
+
123
+ expect(skeleton.jointMatrices[0]).toBeCloseTo(1);
124
+ expect(skeleton.jointMatrices[12]).toBeCloseTo(0);
125
+ expect(skeleton.jointMatrices[13]).toBeCloseTo(0);
126
+ });
127
+ });
128
+
129
+ describe('createSkeleton3D', () => {
130
+ it('allocates a palette sized to the joint count when no inverse-bind is given', () => {
131
+ const skeleton = createSkeleton3D([createSceneNode(), createSceneNode()]);
132
+ expect(skeleton.jointMatrices.length).toBe(32);
133
+ expect(skeleton.inverseBindMatrices.length).toBe(32);
134
+ });
135
+ });
136
+
137
+ describe('disposeSkeleton3D', () => {
138
+ it('drops joint references and clears names', () => {
139
+ const skeleton = createSkeleton3D([createSceneNode(), createSceneNode()], undefined, ['a', 'b']);
140
+ disposeSkeleton3D(skeleton);
141
+ expect(skeleton.joints.length).toBe(0);
142
+ expect(skeleton.names).toBeNull();
143
+ });
144
+ });
145
+
146
+ describe('equalsSkeleton3D', () => {
147
+ it('is reflexive and compares clones as equal', () => {
148
+ const joint = createSceneNode();
149
+ setVector3(joint.position, 2, 0, 0);
150
+ invalidateNodeLocalTransform(joint);
151
+ const skeleton = createSkeleton3D([joint], undefined, ['root']);
152
+ expect(equalsSkeleton3D(skeleton, skeleton)).toBe(true);
153
+ expect(equalsSkeleton3D(skeleton, cloneSkeleton3D(skeleton))).toBe(true);
154
+ });
155
+
156
+ it('differs on joint count, inverse-bind contents, and names', () => {
157
+ const j0 = createSceneNode();
158
+ setVector3(j0.position, 1, 0, 0);
159
+ invalidateNodeLocalTransform(j0);
160
+ const a = createSkeleton3D([j0]);
161
+
162
+ const j1 = createSceneNode();
163
+ setVector3(j1.position, 9, 0, 0);
164
+ invalidateNodeLocalTransform(j1);
165
+ const differentBind = createSkeleton3D([j1]);
166
+ expect(equalsSkeleton3D(a, differentBind)).toBe(false);
167
+
168
+ const extra = createSkeleton3D([createSceneNode(), createSceneNode()]);
169
+ expect(equalsSkeleton3D(a, extra)).toBe(false);
170
+
171
+ const named = createSkeleton3D([j0], a.inverseBindMatrices, ['root']);
172
+ const unnamed = createSkeleton3D([j0], a.inverseBindMatrices);
173
+ expect(equalsSkeleton3D(named, unnamed)).toBe(false);
174
+ expect(equalsSkeleton3D(named, createSkeleton3D([j0], a.inverseBindMatrices, ['other']))).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe('getSkeleton3DJointIndexByName', () => {
179
+ it('returns the joint index or -1 when unnamed or missing', () => {
180
+ const skeleton = createSkeleton3D([createSceneNode(), createSceneNode()], undefined, ['hip', 'spine']);
181
+ expect(getSkeleton3DJointIndexByName(skeleton, 'spine')).toBe(1);
182
+ expect(getSkeleton3DJointIndexByName(skeleton, 'missing')).toBe(-1);
183
+ expect(getSkeleton3DJointIndexByName(createSkeleton3D([createSceneNode()]), 'hip')).toBe(-1);
184
+ });
185
+ });
186
+
187
+ describe('getSkeleton3DJointWorldMatrix', () => {
188
+ it('reads the joint world matrix into out and returns true', () => {
189
+ const joint = createSceneNode();
190
+ setVector3(joint.position, 5, 3, 0);
191
+ invalidateNodeLocalTransform(joint);
192
+ const skeleton = createSkeleton3D([joint]);
193
+ const out = createMatrix4();
194
+
195
+ expect(getSkeleton3DJointWorldMatrix(out, skeleton, 0)).toBe(true);
196
+ expect(getMatrix4Element(out, 0, 3)).toBeCloseTo(5);
197
+ expect(getMatrix4Element(out, 1, 3)).toBeCloseTo(3);
198
+ });
199
+
200
+ it('returns false and leaves out untouched for an out-of-range index', () => {
201
+ const skeleton = createSkeleton3D([createSceneNode()]);
202
+ const out = createMatrix4();
203
+ expect(getSkeleton3DJointWorldMatrix(out, skeleton, 5)).toBe(false);
204
+ expect(getSkeleton3DJointWorldMatrix(out, skeleton, -1)).toBe(false);
205
+ expect(getMatrix4Element(out, 0, 3)).toBeCloseTo(0);
206
+ });
207
+ });
208
+
209
+ describe('getSkeleton3DJointWorldMatrixByName', () => {
210
+ it('resolves the joint by name and reads its world matrix', () => {
211
+ const hip = createSceneNode();
212
+ const hand = createSceneNode();
213
+ setVector3(hand.position, 7, 0, 0);
214
+ invalidateNodeLocalTransform(hand);
215
+ const skeleton = createSkeleton3D([hip, hand], undefined, ['hip', 'hand']);
216
+ const out = createMatrix4();
217
+
218
+ expect(getSkeleton3DJointWorldMatrixByName(out, skeleton, 'hand')).toBe(true);
219
+ expect(getMatrix4Element(out, 0, 3)).toBeCloseTo(7);
220
+ expect(getSkeleton3DJointWorldMatrixByName(out, skeleton, 'missing')).toBe(false);
221
+ });
222
+ });
223
+
224
+ describe('setSkeleton3DBindPose', () => {
225
+ it('rebinds so the current pose becomes the rest pose', () => {
226
+ const joint = createSceneNode();
227
+ const skeleton = createSkeleton3D([joint]);
228
+ setVector3(joint.position, 2, 0, 0);
229
+ invalidateNodeLocalTransform(joint);
230
+ setSkeleton3DBindPose(skeleton);
231
+
232
+ computeSkeleton3DJointMatrices(skeleton);
233
+
234
+ expect(skeleton.jointMatrices[12]).toBeCloseTo(0);
235
+ });
236
+ });
237
+
238
+ describe('validateSkeleton3D', () => {
239
+ it('returns null for a well-formed skeleton', () => {
240
+ expect(validateSkeleton3D(createSkeleton3D([createSceneNode(), createSceneNode()]))).toBeNull();
241
+ });
242
+
243
+ it('returns a diagnostic when inverseBindMatrices length does not match jointCount * 16', () => {
244
+ const skeleton = createSkeleton3D([createSceneNode(), createSceneNode()]);
245
+ skeleton.inverseBindMatrices = new Float32Array(16);
246
+
247
+ const diagnostic = validateSkeleton3D(skeleton);
248
+ expect(diagnostic).not.toBeNull();
249
+ expect(diagnostic!.jointCount).toBe(2);
250
+ expect(diagnostic!.expectedInverseBindMatricesLength).toBe(32);
251
+ expect(diagnostic!.inverseBindMatricesLength).toBe(16);
252
+ expect(diagnostic!.message).toContain('does not match');
253
+ });
254
+ });
@@ -0,0 +1,82 @@
1
+ import { setVector3 } from '@flighthq/geometry';
2
+ import { CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT, createMeshGeometry } from '@flighthq/mesh';
3
+ import { invalidateNodeLocalTransform } from '@flighthq/node';
4
+ import { createSceneNode } from '@flighthq/scene';
5
+ import type { MeshGeometry } from '@flighthq/types';
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ import { computeSkeleton3DJointMatrices, createSkeleton3D } from './skeleton3d';
9
+ import { captureMeshSkinBindPose, skinMeshGeometry } from './skinMeshGeometry';
10
+
11
+ // Builds a single-vertex geometry in the canonical skinned layout (20 floats): position, normal,
12
+ // tangent(0), uv0(0), joints0, weights0.
13
+ function createOneVertexSkinnedGeometry(
14
+ position: readonly [number, number, number],
15
+ normal: readonly [number, number, number],
16
+ joints: readonly [number, number, number, number],
17
+ weights: readonly [number, number, number, number],
18
+ ): MeshGeometry {
19
+ const vertices = new Float32Array(20);
20
+ vertices[0] = position[0];
21
+ vertices[1] = position[1];
22
+ vertices[2] = position[2];
23
+ vertices[3] = normal[0];
24
+ vertices[4] = normal[1];
25
+ vertices[5] = normal[2];
26
+ vertices.set(joints, 12);
27
+ vertices.set(weights, 16);
28
+ return createMeshGeometry({ layout: CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT, vertices });
29
+ }
30
+
31
+ describe('captureMeshSkinBindPose', () => {
32
+ it('de-interleaves positions, normals, joints and weights from the skinned layout', () => {
33
+ const geometry = createOneVertexSkinnedGeometry([1, 2, 3], [0, 1, 0], [2, 0, 0, 0], [1, 0, 0, 0]);
34
+
35
+ const bindPose = captureMeshSkinBindPose(geometry);
36
+
37
+ expect(Array.from(bindPose.positions)).toEqual([1, 2, 3]);
38
+ expect(Array.from(bindPose.normals)).toEqual([0, 1, 0]);
39
+ expect(bindPose.joints[0]).toBe(2);
40
+ expect(bindPose.weights[0]).toBe(1);
41
+ expect(bindPose.skinnedPositions.length).toBe(3);
42
+ expect(bindPose.skinnedNormals.length).toBe(3);
43
+ });
44
+ });
45
+
46
+ describe('skinMeshGeometry', () => {
47
+ it('leaves the vertex at its bind position when the palette is identity', () => {
48
+ const geometry = createOneVertexSkinnedGeometry([1, 0, 0], [0, 1, 0], [0, 0, 0, 0], [1, 0, 0, 0]);
49
+ const joint = createSceneNode();
50
+ const skeleton = createSkeleton3D([joint]);
51
+ const bindPose = captureMeshSkinBindPose(geometry);
52
+ const versionBefore = geometry.version;
53
+
54
+ computeSkeleton3DJointMatrices(skeleton);
55
+ skinMeshGeometry(geometry, skeleton, bindPose);
56
+
57
+ expect(geometry.vertices[0]).toBeCloseTo(1);
58
+ expect(geometry.vertices[1]).toBeCloseTo(0);
59
+ expect(geometry.vertices[2]).toBeCloseTo(0);
60
+ expect(geometry.version).toBe(versionBefore + 1);
61
+ });
62
+
63
+ it('translates the vertex by a translated joint and preserves the untouched channels', () => {
64
+ const geometry = createOneVertexSkinnedGeometry([1, 0, 0], [0, 1, 0], [0, 0, 0, 0], [1, 0, 0, 0]);
65
+ const joint = createSceneNode();
66
+ // Bind pose is captured with the joint at the origin (inverse-bind = identity), then the joint moves.
67
+ const skeleton = createSkeleton3D([joint]);
68
+ const bindPose = captureMeshSkinBindPose(geometry);
69
+ setVector3(joint.position, 0, 5, 0);
70
+ invalidateNodeLocalTransform(joint);
71
+
72
+ computeSkeleton3DJointMatrices(skeleton);
73
+ skinMeshGeometry(geometry, skeleton, bindPose);
74
+
75
+ expect(geometry.vertices[0]).toBeCloseTo(1);
76
+ expect(geometry.vertices[1]).toBeCloseTo(5);
77
+ expect(geometry.vertices[2]).toBeCloseTo(0);
78
+ // Normal (translation leaves it), and the static joints0/weights0 channels are untouched.
79
+ expect(geometry.vertices[4]).toBeCloseTo(1);
80
+ expect(geometry.vertices[16]).toBe(1);
81
+ });
82
+ });
@@ -0,0 +1,102 @@
1
+ import { skinVertices } from './skinVertices';
2
+
3
+ // A column-major 4x4 identity, then variants with translation/scale, laid out as the 16-float palette entry.
4
+ function identity(): number[] {
5
+ // prettier-ignore
6
+ return [
7
+ 1, 0, 0, 0,
8
+ 0, 1, 0, 0,
9
+ 0, 0, 1, 0,
10
+ 0, 0, 0, 1,
11
+ ];
12
+ }
13
+
14
+ function translation(tx: number, ty: number, tz: number): number[] {
15
+ const m = identity();
16
+ m[12] = tx;
17
+ m[13] = ty;
18
+ m[14] = tz;
19
+ return m;
20
+ }
21
+
22
+ describe('skinVertices', () => {
23
+ it('passes vertices through unchanged when the only weighted joint is identity', () => {
24
+ const positions = new Float32Array([2, 3, 4]);
25
+ const normals = new Float32Array([0, 1, 0]);
26
+ const joints = new Uint16Array([0, 0, 0, 0]);
27
+ const weights = new Float32Array([1, 0, 0, 0]);
28
+ const palette = new Float32Array(identity());
29
+ const outPositions = new Float32Array(3);
30
+ const outNormals = new Float32Array(3);
31
+
32
+ skinVertices(outPositions, outNormals, positions, normals, joints, weights, palette);
33
+
34
+ expect(Array.from(outPositions)).toEqual([2, 3, 4]);
35
+ expect(Array.from(outNormals)).toEqual([0, 1, 0]);
36
+ });
37
+
38
+ it('applies a single-joint translation to the position but not the normal', () => {
39
+ const positions = new Float32Array([1, 0, 0]);
40
+ const normals = new Float32Array([0, 0, 1]);
41
+ const joints = new Uint16Array([0, 0, 0, 0]);
42
+ const weights = new Float32Array([1, 0, 0, 0]);
43
+ const palette = new Float32Array(translation(10, 20, 30));
44
+ const outPositions = new Float32Array(3);
45
+ const outNormals = new Float32Array(3);
46
+
47
+ skinVertices(outPositions, outNormals, positions, normals, joints, weights, palette);
48
+
49
+ expect(outPositions[0]).toBeCloseTo(11);
50
+ expect(outPositions[1]).toBeCloseTo(20);
51
+ expect(outPositions[2]).toBeCloseTo(30);
52
+ // Normals ignore the translation column.
53
+ expect(outPositions).not.toBe(outNormals);
54
+ expect(outNormals[0]).toBeCloseTo(0);
55
+ expect(outNormals[1]).toBeCloseTo(0);
56
+ expect(outNormals[2]).toBeCloseTo(1);
57
+ });
58
+
59
+ it('blends two joints by their weights', () => {
60
+ const positions = new Float32Array([0, 0, 0]);
61
+ const normals = new Float32Array([0, 0, 0]);
62
+ // Joint 0 at palette base 0 translates +(10,0,0); joint 1 at base 16 translates +(0,10,0).
63
+ const palette = new Float32Array([...translation(10, 0, 0), ...translation(0, 10, 0)]);
64
+ const joints = new Uint16Array([0, 1, 0, 0]);
65
+ const weights = new Float32Array([0.25, 0.75, 0, 0]);
66
+ const outPositions = new Float32Array(3);
67
+ const outNormals = new Float32Array(3);
68
+
69
+ skinVertices(outPositions, outNormals, positions, normals, joints, weights, palette);
70
+
71
+ // 0.25 * (10,0,0) + 0.75 * (0,10,0) = (2.5, 7.5, 0)
72
+ expect(outPositions[0]).toBeCloseTo(2.5);
73
+ expect(outPositions[1]).toBeCloseTo(7.5);
74
+ expect(outPositions[2]).toBeCloseTo(0);
75
+ });
76
+
77
+ it('skins every vertex in a multi-vertex buffer', () => {
78
+ const positions = new Float32Array([1, 0, 0, 0, 1, 0]);
79
+ const normals = new Float32Array([0, 0, 1, 0, 0, 1]);
80
+ const joints = new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0]);
81
+ const weights = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0]);
82
+ const palette = new Float32Array(translation(5, 0, 0));
83
+ const outPositions = new Float32Array(6);
84
+ const outNormals = new Float32Array(6);
85
+
86
+ skinVertices(outPositions, outNormals, positions, normals, joints, weights, palette);
87
+
88
+ expect(Array.from(outPositions)).toEqual([6, 0, 0, 5, 1, 0]);
89
+ });
90
+
91
+ it('is safe when out aliases the input positions', () => {
92
+ const positions = new Float32Array([1, 2, 3]);
93
+ const normals = new Float32Array([1, 0, 0]);
94
+ const joints = new Uint16Array([0, 0, 0, 0]);
95
+ const weights = new Float32Array([1, 0, 0, 0]);
96
+ const palette = new Float32Array(translation(1, 1, 1));
97
+
98
+ skinVertices(positions, normals, positions, normals, joints, weights, palette);
99
+
100
+ expect(Array.from(positions)).toEqual([2, 3, 4]);
101
+ });
102
+ });
@@ -0,0 +1,62 @@
1
+ import { setVector3 } from '@flighthq/geometry';
2
+ import {
3
+ CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT,
4
+ createMeshGeometry,
5
+ getMeshGeometrySkinBindPose,
6
+ } from '@flighthq/mesh';
7
+ import { invalidateNodeLocalTransform } from '@flighthq/node';
8
+ import { createMesh, createSceneNode } from '@flighthq/scene';
9
+ import { describe, expect, it } from 'vitest';
10
+
11
+ import { createSkeleton3D } from './skeleton3d';
12
+ import { updateMeshSkin } from './updateMeshSkin';
13
+
14
+ function createOneVertexSkinnedMesh() {
15
+ const vertices = new Float32Array(20);
16
+ vertices[0] = 1; // position x
17
+ vertices[4] = 1; // normal y
18
+ vertices[16] = 1; // weights0[0]
19
+ const geometry = createMeshGeometry({ layout: CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT, vertices });
20
+ return createMesh(geometry, []);
21
+ }
22
+
23
+ describe('updateMeshSkin', () => {
24
+ it('deforms the mesh, bumps the version, and lazily captures the bind pose onto the runtime', () => {
25
+ const mesh = createOneVertexSkinnedMesh();
26
+ const joint = createSceneNode();
27
+ mesh.skin = { skeleton: createSkeleton3D([joint]) };
28
+ setVector3(joint.position, 0, 5, 0);
29
+ invalidateNodeLocalTransform(joint);
30
+ const versionBefore = mesh.geometry.version;
31
+
32
+ expect(getMeshGeometrySkinBindPose(mesh.geometry)).toBeNull();
33
+
34
+ updateMeshSkin(mesh);
35
+
36
+ expect(mesh.geometry.vertices[1]).toBeCloseTo(5);
37
+ expect(mesh.geometry.version).toBe(versionBefore + 1);
38
+ expect(getMeshGeometrySkinBindPose(mesh.geometry)).not.toBeNull();
39
+ });
40
+
41
+ it('reuses the captured bind pose across frames rather than recapturing', () => {
42
+ const mesh = createOneVertexSkinnedMesh();
43
+ const joint = createSceneNode();
44
+ mesh.skin = { skeleton: createSkeleton3D([joint]) };
45
+
46
+ updateMeshSkin(mesh);
47
+ const bindPose = getMeshGeometrySkinBindPose(mesh.geometry);
48
+ updateMeshSkin(mesh);
49
+
50
+ expect(getMeshGeometrySkinBindPose(mesh.geometry)).toBe(bindPose);
51
+ });
52
+
53
+ it('is a no-op for a mesh with no skin', () => {
54
+ const mesh = createOneVertexSkinnedMesh();
55
+ const versionBefore = mesh.geometry.version;
56
+
57
+ updateMeshSkin(mesh);
58
+
59
+ expect(mesh.geometry.version).toBe(versionBefore);
60
+ expect(getMeshGeometrySkinBindPose(mesh.geometry)).toBeNull();
61
+ });
62
+ });