@woosh/meep-engine 3.14.4 → 3.14.5

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.
Files changed (89) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
  3. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
  4. package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
  5. package/src/engine/network/orchestrator/NetworkPeer.d.ts +56 -8
  6. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  7. package/src/engine/network/orchestrator/NetworkPeer.js +263 -67
  8. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
  9. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  10. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
  11. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +13 -7
  12. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  13. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +103 -138
  14. package/src/engine/network/replication/Replicator.d.ts +29 -6
  15. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  16. package/src/engine/network/replication/Replicator.js +265 -108
  17. package/src/engine/network/sim/ActionLog.d.ts +2 -0
  18. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  19. package/src/engine/network/sim/ActionLog.js +2 -0
  20. package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
  21. package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
  22. package/src/engine/network/sim/ActionRecordCursor.js +120 -0
  23. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  24. package/src/engine/network/sim/RewindEngine.js +13 -29
  25. package/src/engine/network/sim/SimActionExecutor.d.ts +2 -3
  26. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  27. package/src/engine/network/sim/SimActionExecutor.js +10 -11
  28. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  29. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
  30. package/src/shade/playground/particle_system/particle_scene.js +38 -37
  31. package/src/shade/playground/skinned_mesh_soup/README.md +39 -18
  32. package/src/shade/playground/skinned_mesh_soup/index.html +9 -6
  33. package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
  34. package/src/shade/playground/skinned_mesh_soup/main.js +16 -13
  35. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +8 -2
  36. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  37. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +23 -4
  38. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  39. package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
  40. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
  41. package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
  42. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
  44. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
  45. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
  46. package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
  47. package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
  48. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
  49. package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
  50. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
  51. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
  52. package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
  53. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
  54. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
  55. package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
  56. package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
  57. package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
  58. package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
  59. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
  60. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
  61. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
  62. package/src/shade/renderer/particles/DESIGN.md +5 -5
  63. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
  64. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
  65. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
  66. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
  67. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  68. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
  69. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
  70. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  71. package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
  72. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
  73. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
  74. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
  75. package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
  76. package/src/shade/renderer/particles/particle_prototype.js +181 -180
  77. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
  78. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  79. package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
  80. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
  81. package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
  82. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
  83. package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
  84. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
  85. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
  86. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
  87. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
  88. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
  89. package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
@@ -0,0 +1,199 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import AABB2 from "../../../core/geom/2d/aabb/AABB2.js";
3
+ import {
4
+ animation_curve_compute_aabb
5
+ } from "../../../engine/animation/curve/animation_curve_compute_aabb.js";
6
+ import { Node3DProperty } from "../object_property/Node3DProperty.js";
7
+
8
+ /**
9
+ * How far from 1 a scale curve may run and still count as not scaling.
10
+ *
11
+ * The same order as the `IDENTITY_EPSILON` {@link ShadeAnimationChannel#optimize} drops an identity
12
+ * channel by, and for the same reason: a scale curve authored as the constant 1 arrives with f32
13
+ * round-trip noise on it and is not a scale. Anything a person authored is orders above this.
14
+ *
15
+ * @type {number}
16
+ */
17
+ const UNIT_EPSILON = 1e-5;
18
+
19
+ /**
20
+ * Scratch for one curve's value range. Nothing here re-enters.
21
+ *
22
+ * @type {AABB2}
23
+ */
24
+ const _bounds = new AABB2();
25
+
26
+ /**
27
+ * The channels of a clip that would scale this skin's joints out from under the frame it is
28
+ * deformed in.
29
+ *
30
+ * ## Why this is worth asking
31
+ *
32
+ * {@link skin_normalize_mesh_frame} makes `inverse(mesh.global) × joint.global × inverse_bind`
33
+ * rigid, and rigid is what a dual quaternion can carry. It does that with a **constant** correction,
34
+ * which is sound because the frame and the joints share their ancestors and a placement multiplies
35
+ * both on the left. A clip that scales a joint breaks that: the scale enters `joint.global` and not
36
+ * the frame, the fold stops being rigid, and no choice of frame fixes it —
37
+ * `SKINNING_FRAME_PLAN.md` §4 is the ladder out and neither of its rungs is built.
38
+ *
39
+ * The failure is per-frame and on the GPU, where nothing can see it. This is the same question asked
40
+ * **statically**, off the clip's own curves, at registration: exact, cheap, and it needs no device.
41
+ *
42
+ * ## What counts
43
+ *
44
+ * Not "the channel targets a joint" — that understates it. What matters is whether the scale is
45
+ * *shared* between the frame and the joints, and a scale is shared exactly when the node carrying it
46
+ * is an ancestor of the mesh **and** of every joint. So:
47
+ *
48
+ * - a scale on the character's own root reaches the mesh and all the joints, cancels out of the
49
+ * fold, and is fine — this is how a model is made to grow;
50
+ * - a scale on a joint, or on any node above only *some* of them, reaches the joints and not the
51
+ * mesh, and is a conflict;
52
+ * - a scale on a node that reaches neither is somebody else's business.
53
+ *
54
+ * A channel whose curves are all the constant 1 is not a scale at all, whatever it targets. The
55
+ * range is taken with {@link animation_curve_compute_aabb}, which follows the Hermite spline between
56
+ * keys rather than only reading the keys — a curve that leaves 1 and returns to it is a scale.
57
+ *
58
+ * @param {Skin} skin
59
+ * @param {ShadeAnimationClip} clip
60
+ * @returns {Array<{
61
+ * channel: ShadeAnimationChannel,
62
+ * node: Node3D,
63
+ * uniform: boolean,
64
+ * joints_reached: number,
65
+ * reaches_mesh: boolean,
66
+ * }>} one entry per conflicting channel, empty when the clip and the skin can coexist. `uniform`
67
+ * says which rung of §4's ladder would be needed: a uniform scale is one a dual quaternion could
68
+ * be taught to carry alongside, a non-uniform one is not. It is `true` only when all three
69
+ * components are animated by equal curves, so a channel that is uniform by coincidence of the
70
+ * node's own scale reads as non-uniform — the safe direction for a report
71
+ */
72
+ export function skin_clip_scale_conflicts(skin, clip) {
73
+ assert.defined(skin, 'skin');
74
+ assert.equal(skin.isSkin, true, 'skin.isSkin !== true');
75
+ assert.defined(clip, 'clip');
76
+
77
+ const scaling = clip_scale_channels(clip);
78
+
79
+ const conflicts = [];
80
+
81
+ if (scaling.length === 0) {
82
+ return conflicts;
83
+ }
84
+
85
+ const joints = new Set(skin.joints);
86
+ const meshes = new Set(skin.meshes);
87
+
88
+ for (const channel of scaling) {
89
+ let joints_reached = 0;
90
+ let meshes_reached = 0;
91
+
92
+ channel.target.traverse(node => {
93
+ if (joints.has(node)) {
94
+ joints_reached++;
95
+ }
96
+
97
+ if (meshes.has(node)) {
98
+ meshes_reached++;
99
+ }
100
+ });
101
+
102
+ const reaches_everything = joints_reached === joints.size && meshes_reached === meshes.size;
103
+ const reaches_nothing = joints_reached === 0 && meshes_reached === 0;
104
+
105
+ if (reaches_everything || reaches_nothing) {
106
+ continue;
107
+ }
108
+
109
+ conflicts.push({
110
+ channel,
111
+ node: channel.target,
112
+ uniform: is_uniform(channel),
113
+ joints_reached,
114
+ reaches_mesh: meshes_reached > 0,
115
+ });
116
+ }
117
+
118
+ return conflicts;
119
+ }
120
+
121
+ /**
122
+ * The channels of a clip that scale something, anywhere — before any question of *whose* joints.
123
+ *
124
+ * Separated out because it is the cheap half and almost always the whole answer. A character's clips
125
+ * rotate and translate; a clip with no scale channel at all can be dismissed in one pass over its
126
+ * channel list, which is what keeps the per-`(skin, clip)` pair check from costing anything at a
127
+ * few hundred dancers. {@link GPUAnimationManager} asks this first and only remembers a clip that
128
+ * says yes.
129
+ *
130
+ * @param {ShadeAnimationClip} clip
131
+ * @returns {ShadeAnimationChannel[]}
132
+ */
133
+ export function clip_scale_channels(clip) {
134
+ assert.defined(clip, 'clip');
135
+
136
+ const scaling = [];
137
+
138
+ for (const channel of clip.channels) {
139
+ if (channel.property !== Node3DProperty.Scale) {
140
+ // translation and rotation keep the fold rigid, which is the whole of what it needs
141
+ continue;
142
+ }
143
+
144
+ if (channel.target === null || channel.target === undefined) {
145
+ continue;
146
+ }
147
+
148
+ if (scales(channel)) {
149
+ scaling.push(channel);
150
+ }
151
+ }
152
+
153
+ return scaling;
154
+ }
155
+
156
+ /**
157
+ * Whether a scale channel moves anything — false when every component it animates stays at 1.
158
+ *
159
+ * @param {ShadeAnimationChannel} channel
160
+ * @returns {boolean}
161
+ */
162
+ function scales(channel) {
163
+ const curves = channel.curves;
164
+
165
+ for (const axis of ['x', 'y', 'z']) {
166
+ const curve = curves[axis];
167
+
168
+ if (curve === undefined || curve.length === 0) {
169
+ continue;
170
+ }
171
+
172
+ animation_curve_compute_aabb(_bounds, curve);
173
+
174
+ if (Math.abs(_bounds.y0 - 1) > UNIT_EPSILON || Math.abs(_bounds.y1 - 1) > UNIT_EPSILON) {
175
+ return true;
176
+ }
177
+ }
178
+
179
+ return false;
180
+ }
181
+
182
+ /**
183
+ * Whether a scale channel drives all three axes by the same curve — the shape
184
+ * {@link Node3DProperty.Scale} names as how a uniform pulsate is authored.
185
+ *
186
+ * @param {ShadeAnimationChannel} channel
187
+ * @returns {boolean}
188
+ */
189
+ function is_uniform(channel) {
190
+ const { x, y, z } = channel.curves;
191
+
192
+ if (x === undefined || y === undefined || z === undefined) {
193
+ // an axis left unanimated keeps whatever the node's own scale holds while the others move,
194
+ // which is a non-uniform scale however uniform the curves that are there
195
+ return false;
196
+ }
197
+
198
+ return (x === y || x.equals(y)) && (x === z || x.equals(z));
199
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Whether a frame is one this skin can be deformed in — and if not, by how much it is not.
3
+ *
4
+ * The skinning pass converts `frame⁻¹ × joint.global × inverse_bind` to a dual quaternion, and a
5
+ * dual quaternion carries a rotation and a translation and nothing else. So that product must be
6
+ * rigid for every joint. This is that condition as a number: zero when the frame works, and growing
7
+ * with whatever the conversion would have to throw away. `SKINNING_FRAME_PLAN.md` §1 is what
8
+ * happens when it does.
9
+ *
10
+ * **This, and not "do the joints agree", is the precondition.** A file whose rest pose is not its
11
+ * bind pose has joints whose skin matrices differ — by *rigid* motions, which cost nothing here.
12
+ * What cannot be absorbed is a scale or a shear, and this sees exactly that.
13
+ *
14
+ * Reads the joints' `transform_global`, so it answers for whatever pose the skeleton is standing in.
15
+ * At rest that is the registration-time question. Handed a posed skeleton it answers the runtime one
16
+ * — a clip that scales a joint, which no frame can fix (§4).
17
+ *
18
+ * @param {Skin} skin
19
+ * @param {ArrayLike<number>} frame 16 elements, column-major — the transform the renderer re-applies
20
+ * @returns {number} the largest {@link m4_rigidity_defect} over the skin's joints; `Infinity` if
21
+ * `frame` has no inverse, or if any joint's fold comes out a reflection — which scores zero
22
+ * on the defect and is unrepresentable all the same
23
+ */
24
+ export function skin_frame_defect(skin: Skin, frame: ArrayLike<number>): number;
25
+ /**
26
+ * The same question of one joint, freshly allocated — for a caller that wants to say *which* joint
27
+ * and by how much rather than only the worst of them.
28
+ *
29
+ * @param {Skin} skin
30
+ * @param {ArrayLike<number>} frame
31
+ * @returns {number[]} one defect per joint, in joint order
32
+ */
33
+ export function skin_frame_defect_by_joint(skin: Skin, frame: ArrayLike<number>): number[];
34
+ //# sourceMappingURL=skin_frame_defect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skin_frame_defect.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/animation/skin_frame_defect.js"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qDALW,UAAU,MAAM,CAAC,GACf,MAAM,CA6ClB;AAiBD;;;;;;;GAOG;AACH,8DAHW,UAAU,MAAM,CAAC,GACf,MAAM,EAAE,CA8BpB"}
@@ -0,0 +1,132 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import { m4_invert } from "../../../core/geom/3d/mat4/m4_invert.js";
3
+ import { m4_multiply } from "../../../core/geom/3d/mat4/m4_multiply.js";
4
+ import { m4_rigidity_defect } from "../../../core/geom/3d/mat4/m4_rigidity_defect.js";
5
+ import { skin_joint_matrix } from "./skin_joint_matrix.js";
6
+
7
+ /**
8
+ * Scratch, so that walking a sixty-five-joint skeleton does not allocate per joint. Nothing here
9
+ * re-enters, and none of it outlives the call.
10
+ */
11
+ const _inverse_frame = new Float64Array(16);
12
+ const _joint_matrix = new Float64Array(16);
13
+ const _folded = new Float64Array(16);
14
+
15
+ /**
16
+ * Whether a frame is one this skin can be deformed in — and if not, by how much it is not.
17
+ *
18
+ * The skinning pass converts `frame⁻¹ × joint.global × inverse_bind` to a dual quaternion, and a
19
+ * dual quaternion carries a rotation and a translation and nothing else. So that product must be
20
+ * rigid for every joint. This is that condition as a number: zero when the frame works, and growing
21
+ * with whatever the conversion would have to throw away. `SKINNING_FRAME_PLAN.md` §1 is what
22
+ * happens when it does.
23
+ *
24
+ * **This, and not "do the joints agree", is the precondition.** A file whose rest pose is not its
25
+ * bind pose has joints whose skin matrices differ — by *rigid* motions, which cost nothing here.
26
+ * What cannot be absorbed is a scale or a shear, and this sees exactly that.
27
+ *
28
+ * Reads the joints' `transform_global`, so it answers for whatever pose the skeleton is standing in.
29
+ * At rest that is the registration-time question. Handed a posed skeleton it answers the runtime one
30
+ * — a clip that scales a joint, which no frame can fix (§4).
31
+ *
32
+ * @param {Skin} skin
33
+ * @param {ArrayLike<number>} frame 16 elements, column-major — the transform the renderer re-applies
34
+ * @returns {number} the largest {@link m4_rigidity_defect} over the skin's joints; `Infinity` if
35
+ * `frame` has no inverse, or if any joint's fold comes out a reflection — which scores zero
36
+ * on the defect and is unrepresentable all the same
37
+ */
38
+ export function skin_frame_defect(skin, frame) {
39
+ assert.defined(skin, 'skin');
40
+ assert.equal(skin.isSkin, true, 'skin.isSkin !== true');
41
+ assert.isArrayLike(frame, 'frame');
42
+
43
+ if (!m4_invert(_inverse_frame, frame)) {
44
+ // a frame with no inverse is not a frame; the skinning pass would divide by that determinant
45
+ return Infinity;
46
+ }
47
+
48
+ const joint_count = skin.joints.length;
49
+
50
+ let worst = 0;
51
+
52
+ for (let j = 0; j < joint_count; j++) {
53
+ skin_joint_matrix(_joint_matrix, skin, j);
54
+
55
+ m4_multiply(_folded, _inverse_frame, _joint_matrix);
56
+
57
+ if (m4_linear_determinant(_folded) < 0) {
58
+ /*
59
+ A reflection. {@link m4_rigidity_defect} reads zero for one — it measures orthonormality,
60
+ and a reflection is orthonormal — so this is the one way a fold can be unrepresentable
61
+ and still score perfectly. `quat_from_m3` would answer with a *rotation*, silently, which
62
+ is precisely the class of failure this check exists to end.
63
+
64
+ Not fixable by a frame either: a reflection shared by the frame and every joint cancels
65
+ and never reaches here, so one that does reach here is a skeleton whose joints disagree
66
+ about handedness.
67
+ */
68
+ return Infinity;
69
+ }
70
+
71
+ const defect = m4_rigidity_defect(_folded);
72
+
73
+ if (defect > worst) {
74
+ worst = defect;
75
+ }
76
+ }
77
+
78
+ return worst;
79
+ }
80
+
81
+ /**
82
+ * The determinant of a matrix's upper 3×3 — negative exactly when the transform turns a
83
+ * right-handed basis left-handed.
84
+ *
85
+ * @param {ArrayLike<number>} mat4 column-major
86
+ * @returns {number}
87
+ */
88
+ function m4_linear_determinant(mat4) {
89
+ const a = mat4[0], b = mat4[4], c = mat4[8];
90
+ const d = mat4[1], e = mat4[5], f = mat4[9];
91
+ const g = mat4[2], h = mat4[6], i = mat4[10];
92
+
93
+ return a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
94
+ }
95
+
96
+ /**
97
+ * The same question of one joint, freshly allocated — for a caller that wants to say *which* joint
98
+ * and by how much rather than only the worst of them.
99
+ *
100
+ * @param {Skin} skin
101
+ * @param {ArrayLike<number>} frame
102
+ * @returns {number[]} one defect per joint, in joint order
103
+ */
104
+ export function skin_frame_defect_by_joint(skin, frame) {
105
+ assert.defined(skin, 'skin');
106
+ assert.equal(skin.isSkin, true, 'skin.isSkin !== true');
107
+
108
+ // f64 throughout: a 100× frame inverted in f32 reports a defect of its own rounding, and this
109
+ // number is read against a tolerance small enough to see one
110
+ const inverse_frame = new Float64Array(16);
111
+
112
+ if (!m4_invert(inverse_frame, frame)) {
113
+ return skin.joints.map(() => Infinity);
114
+ }
115
+
116
+ const joint_matrix = new Float64Array(16);
117
+ const folded = new Float64Array(16);
118
+
119
+ return skin.joints.map((_joint, j) => {
120
+ skin_joint_matrix(joint_matrix, skin, j);
121
+
122
+ m4_multiply(folded, inverse_frame, joint_matrix);
123
+
124
+ // the same refusal {@link skin_frame_defect} makes, so that the gate and the diagnostic
125
+ // that explains it cannot disagree about which joints are the problem
126
+ if (m4_linear_determinant(folded) < 0) {
127
+ return Infinity;
128
+ }
129
+
130
+ return m4_rigidity_defect(folded);
131
+ });
132
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One joint's skin matrix, in the pose its node currently holds: `joint.global × inverse_bind`.
3
+ *
4
+ * The same quantity `shader_skin_matrix_prep` writes to the GPU's `skin_matrices` buffer, and the
5
+ * same one `compute_skin_world_bounds` composes its box out of — kept here so the CPU-side callers
6
+ * that reason about a skin's geometry all read it from one place rather than three.
7
+ *
8
+ * It maps a vertex from the skin's mesh space into **world** space. The mesh node's own transform
9
+ * does not appear and must not: glTF says a skinned mesh's node transform is ignored, and this
10
+ * engine's arrangement is that the skinning pass folds its inverse in and the rasterizer re-applies
11
+ * it, so the two cancel. See `SKINNING_FRAME_PLAN.md` for what that fold requires of an asset.
12
+ *
13
+ * Not a per-frame path. Joints the pose accumulator drives are {@link TransformAuthority.GPU} and
14
+ * their `transform_global` holds the rest pose the CPU seeded, so what this answers for an animated
15
+ * character is the rest-pose matrix — which is exactly what the frame arithmetic wants, and is not
16
+ * what a renderer wants.
17
+ *
18
+ * @param {Float64Array|Float32Array|number[]} out 16 elements, column-major
19
+ * @param {Skin} skin
20
+ * @param {number} joint_index
21
+ * @returns {Float64Array|Float32Array|number[]} `out`
22
+ */
23
+ export function skin_joint_matrix(out: Float64Array | Float32Array | number[], skin: Skin, joint_index: number): Float64Array | Float32Array | number[];
24
+ //# sourceMappingURL=skin_joint_matrix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skin_joint_matrix.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/animation/skin_joint_matrix.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,uCALW,YAAY,GAAC,YAAY,GAAC,MAAM,EAAE,2BAElC,MAAM,GACJ,YAAY,GAAC,YAAY,GAAC,MAAM,EAAE,CAsB9C"}
@@ -0,0 +1,46 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import { m4_multiply } from "../../../core/geom/3d/mat4/m4_multiply.js";
3
+
4
+ /**
5
+ * One joint's skin matrix, in the pose its node currently holds: `joint.global × inverse_bind`.
6
+ *
7
+ * The same quantity `shader_skin_matrix_prep` writes to the GPU's `skin_matrices` buffer, and the
8
+ * same one `compute_skin_world_bounds` composes its box out of — kept here so the CPU-side callers
9
+ * that reason about a skin's geometry all read it from one place rather than three.
10
+ *
11
+ * It maps a vertex from the skin's mesh space into **world** space. The mesh node's own transform
12
+ * does not appear and must not: glTF says a skinned mesh's node transform is ignored, and this
13
+ * engine's arrangement is that the skinning pass folds its inverse in and the rasterizer re-applies
14
+ * it, so the two cancel. See `SKINNING_FRAME_PLAN.md` for what that fold requires of an asset.
15
+ *
16
+ * Not a per-frame path. Joints the pose accumulator drives are {@link TransformAuthority.GPU} and
17
+ * their `transform_global` holds the rest pose the CPU seeded, so what this answers for an animated
18
+ * character is the rest-pose matrix — which is exactly what the frame arithmetic wants, and is not
19
+ * what a renderer wants.
20
+ *
21
+ * @param {Float64Array|Float32Array|number[]} out 16 elements, column-major
22
+ * @param {Skin} skin
23
+ * @param {number} joint_index
24
+ * @returns {Float64Array|Float32Array|number[]} `out`
25
+ */
26
+ export function skin_joint_matrix(out, skin, joint_index) {
27
+ assert.isArrayLike(out, 'out');
28
+ assert.defined(skin, 'skin');
29
+ assert.equal(skin.isSkin, true, 'skin.isSkin !== true');
30
+ assert.isNonNegativeInteger(joint_index, 'joint_index');
31
+ assert.lessThan(joint_index, skin.joints.length, 'joint_index');
32
+
33
+ const offset = joint_index * 16;
34
+
35
+ assert.greaterThanOrEqual(
36
+ skin.inverse_bind_matrices.length, offset + 16,
37
+ 'skin.inverse_bind_matrices.length'
38
+ );
39
+
40
+ // a Transform64's matrix starts at element 0, so the transform is its own mat4 argument
41
+ return m4_multiply(
42
+ out,
43
+ skin.joints[joint_index].transform_global,
44
+ skin.inverse_bind_matrices.subarray(offset, offset + 16)
45
+ );
46
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Put a skin's meshes into the frame the skin actually deforms in.
3
+ *
4
+ * ## What it does and why
5
+ *
6
+ * The skinning pass writes each clone's vertices in `inverse(mesh.global)` space and the rasterizer
7
+ * re-applies `mesh.global`, so `mesh.global` is the *frame* a skinned mesh's vertices live in. The
8
+ * pass gets there by converting `inverse(mesh.global) × joint.global × inverse_bind` to a dual
9
+ * quaternion, which carries a rotation and a translation and **nothing else** — so that product has
10
+ * to be rigid, and whether it is depends entirely on `mesh.global`.
11
+ *
12
+ * glTF says a skinned mesh's node transform is ignored, so it is not the asset's to choose. This
13
+ * makes it the engine's, and sets it to the one frame that is guaranteed to work: the skin's own
14
+ * bind transform, `joint.global × inverse_bind` at rest — see {@link skin_bind_transform}. After
15
+ * this, `inverse(mesh.global) × joint.global × inverse_bind` is the joint's **displacement from
16
+ * bind**, which is rigid whenever the clip rotates and translates and does not scale. That is the
17
+ * honest precondition of dual-quaternion skinning, and it is a property of the animation rather than
18
+ * of the exporter's unit conventions.
19
+ *
20
+ * `SKINNING_FRAME_PLAN.md` is the whole argument, including what a violation looks like on screen.
21
+ *
22
+ * ## Properties worth knowing
23
+ *
24
+ * - **Idempotent.** Afterwards `mesh.global` *is* the bind transform, so a second call computes the
25
+ * identity and changes nothing. Re-registering a skin is free.
26
+ * - **Placement-invariant.** The correction is `mesh.global⁻¹ × bind`, and anything applied above
27
+ * both the mesh node and the joints — a placement, a walk cycle moving the character — multiplies
28
+ * both on the left and cancels. Computed once, correct forever.
29
+ * - **A no-op for an asset that already agrees.** A file whose inverse-bind matrices are authored
30
+ * before its root's unit conversion has `bind == mesh.global` already and is not touched. That is
31
+ * the shape this engine's creature models are said to have, and it is what keeps this from being
32
+ * a change to them.
33
+ *
34
+ * ## What it cannot fix
35
+ *
36
+ * A skin whose *own* bind frame does not work — a rest pose carrying a non-uniform scale or a shear,
37
+ * or joints that disagree about more than a rigid motion. No choice of frame fixes those; a dual
38
+ * quaternion cannot represent them at all. `frame_defect` in the report is that case, and when it
39
+ * fires nothing is written: the mesh is left as the asset had it rather than moved somewhere equally
40
+ * wrong. See `SKINNING_FRAME_PLAN.md` §4 for the ladder out.
41
+ *
42
+ * @param {Skin} skin its joints and meshes must have been walked — this reads `transform_global`,
43
+ * which is derived. A skin that has never been walked at all is refused rather than
44
+ * measured; one walked and then moved is the caller's to walk again, and the residual
45
+ * below is what notices if the mesh half of that was missed
46
+ * @returns {{
47
+ * frame_defect: number,
48
+ * absorbed: number,
49
+ * corrected: number,
50
+ * unchanged: number,
51
+ * residual: number,
52
+ * obstructed: Node3D[],
53
+ * unwalked: Node3D[],
54
+ * }} `frame_defect` — how far the skin is from being deformable in *any* frame, zero when it is
55
+ * fine and `NaN` when `unwalked` stopped it being asked; `absorbed` — how far from rigid the largest correction was, which for a violation is the
56
+ * number the soup was made of; `corrected` and `unchanged` — meshes moved and meshes already
57
+ * right; `residual` — the worst `|mesh.global − bind|` left afterwards; `obstructed` — corrected
58
+ * mesh nodes that had children, which have been scaled along with them; `unwalked` — joints or
59
+ * meshes whose `transform_global` has never been derived, which stops everything else
60
+ */
61
+ export function skin_normalize_mesh_frame(skin: Skin): {
62
+ frame_defect: number;
63
+ absorbed: number;
64
+ corrected: number;
65
+ unchanged: number;
66
+ residual: number;
67
+ obstructed: Node3D[];
68
+ unwalked: Node3D[];
69
+ };
70
+ //# sourceMappingURL=skin_normalize_mesh_frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skin_normalize_mesh_frame.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/animation/skin_normalize_mesh_frame.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH;kBAdqB,MAAM;cACV,MAAM;eACL,MAAM;eACN,MAAM;cACP,MAAM;gBACJ,QAAQ;cACV,QAAQ;EA4GxB"}