@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.
- package/package.json +1 -1
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +56 -8
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +263 -67
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +13 -7
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +103 -138
- package/src/engine/network/replication/Replicator.d.ts +29 -6
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +265 -108
- package/src/engine/network/sim/ActionLog.d.ts +2 -0
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +2 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
- package/src/engine/network/sim/ActionRecordCursor.js +120 -0
- package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
- package/src/engine/network/sim/RewindEngine.js +13 -29
- package/src/engine/network/sim/SimActionExecutor.d.ts +2 -3
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +10 -11
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
- package/src/shade/playground/particle_system/particle_scene.js +38 -37
- package/src/shade/playground/skinned_mesh_soup/README.md +39 -18
- package/src/shade/playground/skinned_mesh_soup/index.html +9 -6
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
- package/src/shade/playground/skinned_mesh_soup/main.js +16 -13
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +8 -2
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +23 -4
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
- package/src/shade/renderer/particles/DESIGN.md +5 -5
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
- package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
- package/src/shade/renderer/particles/particle_prototype.js +181 -180
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
- package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
- package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
- 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"}
|