@driftengine/animation 3.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +74 -0
- package/dist/blend.d.ts +39 -0
- package/dist/blend.js +158 -0
- package/dist/blendTree.d.ts +115 -0
- package/dist/blendTree.js +171 -0
- package/dist/clip.d.ts +50 -0
- package/dist/clip.js +171 -0
- package/dist/ik.d.ts +29 -0
- package/dist/ik.js +329 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +12 -0
- package/dist/pose.d.ts +39 -0
- package/dist/pose.js +49 -0
- package/dist/retarget.d.ts +40 -0
- package/dist/retarget.js +72 -0
- package/dist/rigid.d.ts +56 -0
- package/dist/rigid.js +92 -0
- package/dist/rootMotion.d.ts +74 -0
- package/dist/rootMotion.js +182 -0
- package/dist/skeleton.d.ts +81 -0
- package/dist/skeleton.js +187 -0
- package/dist/spring.d.ts +108 -0
- package/dist/spring.js +386 -0
- package/dist/stateMachine.d.ts +75 -0
- package/dist/stateMachine.js +142 -0
- package/package.json +61 -0
- package/src/blend.ts +191 -0
- package/src/blendTree.ts +253 -0
- package/src/clip.ts +223 -0
- package/src/ik.ts +401 -0
- package/src/index.ts +38 -0
- package/src/pose.ts +60 -0
- package/src/retarget.ts +105 -0
- package/src/rigid.ts +99 -0
- package/src/rootMotion.ts +253 -0
- package/src/skeleton.ts +231 -0
- package/src/spring.ts +520 -0
- package/src/stateMachine.ts +181 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 Drift Technologies
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
DriftEngine
|
|
2
|
+
Copyright 2026 Drift Technologies
|
|
3
|
+
|
|
4
|
+
This product includes software developed at Drift Technologies
|
|
5
|
+
(https://github.com/drftrun/driftengine).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0. Section 4(d) of that licence
|
|
8
|
+
requires this NOTICE to be reproduced in any derivative work you distribute,
|
|
9
|
+
in the source, the documentation, or a display generated by the work.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @driftengine/animation
|
|
2
|
+
|
|
3
|
+
Skeletons, clips, poses, and the graphs over them.
|
|
4
|
+
|
|
5
|
+
**Cost: 6.1 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
|
|
6
|
+
drifts more than 3% — the number is derived from the same floors that gate asserts, so a README
|
|
7
|
+
quoting a stale one is a red suite rather than a thing somebody notices.
|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
|
|
11
|
+
A `Skeleton` resolves a `Pose` into a skinning palette. A clip is **sampled as a pure function of a
|
|
12
|
+
time you supply** — `sampleClip(clip, seconds, out)` reads no clock of its own, which is what lets an
|
|
13
|
+
animation be part of a replay rather than something layered on top of one. Above that: `blendPoses`
|
|
14
|
+
and additive layers, a `BlendTree`, a crossfading state machine over parameters you name, two-bone
|
|
15
|
+
IK, morph targets, retargeting by joint name, root motion, and **secondary motion** —
|
|
16
|
+
`sampleSpring(settings, anchorAt, seconds, out)` for one damped spring and `sampleSpringChain` for a
|
|
17
|
+
chain of them, which is hair, a coat, an antenna or a chain reacting to what the subject is doing.
|
|
18
|
+
|
|
19
|
+
Skinning and morphing run on both backends as vertex-shader permutations, so **a mesh with no rig
|
|
20
|
+
carries none of their instructions**. glTF's skins, animations and morph targets import, and `.drft`
|
|
21
|
+
carries them.
|
|
22
|
+
|
|
23
|
+
## Things that cost time before they were written down
|
|
24
|
+
|
|
25
|
+
**A blend needs a bind pose and does not take one.** `blendPoses(a, b, t, out)` has no bind-pose
|
|
26
|
+
parameter — the bind pose belongs to `BlendTree`, which allocates its own scratch poses. Reaching
|
|
27
|
+
for one on `blendPoses` is reading a paraphrase rather than the signature.
|
|
28
|
+
|
|
29
|
+
**Nothing here allocates per frame, and `out` is why.** Every sampler and every blend fills a
|
|
30
|
+
caller-owned target. A version returning a fresh `Pose` reads better and allocates one per joint per
|
|
31
|
+
frame, which is the shape `AGENTS.md` forbids in a hot path.
|
|
32
|
+
|
|
33
|
+
**Root motion is two calls and it is a mistake to make only one of them.**
|
|
34
|
+
`extractRootMotion(clip, rootJoint, fromSec, toSec, motion)` answers how far the root travelled, in
|
|
35
|
+
the root's own frame at `fromSec`, so you apply it as `position += worldRotation * motion.translation`
|
|
36
|
+
and `worldRotation = worldRotation * motion.rotation`. Then `stripRootMotion(clip, rootJoint, pose)`
|
|
37
|
+
pins the pose's root to the clip's value at time zero. **Extract without stripping and the character
|
|
38
|
+
moves twice, at exactly double speed, with nothing failing.**
|
|
39
|
+
|
|
40
|
+
Loops are accumulated rather than subtracted, which is the part you would otherwise have to write
|
|
41
|
+
yourself and get wrong: sampling the root at both times and subtracting answers a full stride
|
|
42
|
+
_backwards_ every time the clip wraps. What it gives up is a vertical bob authored on the root —
|
|
43
|
+
that leaves the pose with everything else and becomes motion you apply, so if your height is owned
|
|
44
|
+
by a physics controller and you ignore `translation[1]`, the bob is in neither.
|
|
45
|
+
|
|
46
|
+
**Secondary motion is sampled, not advanced, and that is what it is for.** `sampleSpring` takes a
|
|
47
|
+
time and an `anchorAt(t, out)` that must itself be a pure function of the time it is handed. It never
|
|
48
|
+
carries state between calls, so dragging a playhead backwards gives the same pose the forward pass
|
|
49
|
+
gave. The cost is arithmetic per sample instead of per frame: about ninety substeps of a four-multiply
|
|
50
|
+
2x2 for one spring, paid in whichever direction the caller is moving.
|
|
51
|
+
|
|
52
|
+
**`anchorAt` is called at times before the one you asked for**, going back `springSettleSec(settings)`
|
|
53
|
+
— which is how long the spring takes to forget, `-ln(1e-3) / (ζω)`. An anchor that reads a clock, or
|
|
54
|
+
that answers differently on a second call for one time, breaks the property silently. If yours comes
|
|
55
|
+
from a clip, sample the clip at the time it is handed.
|
|
56
|
+
|
|
57
|
+
**Damping of exactly 0 throws, and that is not a validation nicety.** An undamped spring rings
|
|
58
|
+
forever, so no lookback is long enough to sample it purely and there is no honest answer to return.
|
|
59
|
+
Anything above zero forgets. 1 is critical damping, which never overshoots; above 1 sags in.
|
|
60
|
+
|
|
61
|
+
**A chain is `sampleSpringChain`, and it is not the same as calling `sampleSpring` per link.** Each
|
|
62
|
+
link's anchor is where the link above it actually is — lagging and overshooting — so a chain
|
|
63
|
+
remembers longer than any of its links and needs `springChainSettleSec`, which is longer than
|
|
64
|
+
`springSettleSec` and grows with depth. It also has to march on one grid: evaluating a link on its
|
|
65
|
+
own would need its parent at every substep and the grandparent at every substep of each of those,
|
|
66
|
+
which is `steps^depth` anchor calls for the same answer.
|
|
67
|
+
|
|
68
|
+
## Where to start
|
|
69
|
+
|
|
70
|
+
`sampleClip` into a `Pose`, `Skeleton.palette` out of it, and `applyPoseToNode` for a rig you want to
|
|
71
|
+
drive without skinning at all.
|
|
72
|
+
|
|
73
|
+
Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
|
|
74
|
+
for how the packages divide, and why some features are compiled into core on demand instead.
|
package/dist/blend.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Pose } from './pose.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Combining poses: a blend between two, an additive layer over one, and a joint written directly.
|
|
4
|
+
*
|
|
5
|
+
* All three write into a caller-owned pose and allocate nothing, because every one of them runs
|
|
6
|
+
* per character per frame. All three are pure functions of their inputs — no clock, no RNG — which
|
|
7
|
+
* is the determinism contract `sampleClip` establishes and which a graph over clips has to keep.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Blend `a` toward `b` by `t`, into `out`.
|
|
11
|
+
*
|
|
12
|
+
* **Clamped rather than extrapolated.** A weight past the ends puts limbs outside their range,
|
|
13
|
+
* which reads as a broken rig rather than as a weight out of bounds — the same reasoning
|
|
14
|
+
* `sampleClip` holds at a track's ends for. What it costs is that a caller cannot deliberately
|
|
15
|
+
* overshoot; what would make it wrong is somebody wanting to, and the honest answer then is a
|
|
16
|
+
* function that says so in its name.
|
|
17
|
+
*
|
|
18
|
+
* Safe when `out` is also `a` or `b`: every component is read before it is written.
|
|
19
|
+
*/
|
|
20
|
+
export declare function blendPoses(a: Pose, b: Pose, t: number, out: Pose): void;
|
|
21
|
+
/**
|
|
22
|
+
* Apply `delta` on top of `base`, scaled by `weight`, into `out`.
|
|
23
|
+
*
|
|
24
|
+
* **A delta, not a target**, and the difference is the whole of what additive is for: a wave laid
|
|
25
|
+
* over a walk has to move the arm relative to wherever the walk put it rather than replace it. So
|
|
26
|
+
* translation adds, rotation composes, and **scale multiplies** — a scale delta of 1 means
|
|
27
|
+
* unchanged, where adding would make it double.
|
|
28
|
+
*
|
|
29
|
+
* Weight zero is the base untouched, which is what lets a layer fade in from nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function addPose(base: Pose, delta: Pose, weight: number, out: Pose): void;
|
|
32
|
+
/**
|
|
33
|
+
* Write one joint's transform straight into a pose.
|
|
34
|
+
*
|
|
35
|
+
* What a solver drives. Track B's ragdolls need a pose to be *data* rather than only the result of
|
|
36
|
+
* sampling, and this is the whole of that requirement — stated here so the seam exists before the
|
|
37
|
+
* track that needs it, rather than being retrofitted around a closed type.
|
|
38
|
+
*/
|
|
39
|
+
export declare function setJoint(pose: Pose, joint: number, translation: ArrayLike<number>, rotation: ArrayLike<number>, scale: ArrayLike<number>): void;
|
package/dist/blend.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combining poses: a blend between two, an additive layer over one, and a joint written directly.
|
|
3
|
+
*
|
|
4
|
+
* All three write into a caller-owned pose and allocate nothing, because every one of them runs
|
|
5
|
+
* per character per frame. All three are pure functions of their inputs — no clock, no RNG — which
|
|
6
|
+
* is the determinism contract `sampleClip` establishes and which a graph over clips has to keep.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Blend `a` toward `b` by `t`, into `out`.
|
|
10
|
+
*
|
|
11
|
+
* **Clamped rather than extrapolated.** A weight past the ends puts limbs outside their range,
|
|
12
|
+
* which reads as a broken rig rather than as a weight out of bounds — the same reasoning
|
|
13
|
+
* `sampleClip` holds at a track's ends for. What it costs is that a caller cannot deliberately
|
|
14
|
+
* overshoot; what would make it wrong is somebody wanting to, and the honest answer then is a
|
|
15
|
+
* function that says so in its name.
|
|
16
|
+
*
|
|
17
|
+
* Safe when `out` is also `a` or `b`: every component is read before it is written.
|
|
18
|
+
*/
|
|
19
|
+
export function blendPoses(a, b, t, out) {
|
|
20
|
+
const weight = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
21
|
+
const joints = out.rotation.length / 4;
|
|
22
|
+
for (let j = 0; j < joints; j++) {
|
|
23
|
+
const v = j * 3;
|
|
24
|
+
for (let c = 0; c < 3; c++) {
|
|
25
|
+
const from = a.translation[v + c];
|
|
26
|
+
out.translation[v + c] = from + (b.translation[v + c] - from) * weight;
|
|
27
|
+
const scaleFrom = a.scale[v + c];
|
|
28
|
+
out.scale[v + c] = scaleFrom + (b.scale[v + c] - scaleFrom) * weight;
|
|
29
|
+
}
|
|
30
|
+
slerpInto(a.rotation, j * 4, b.rotation, j * 4, weight, out.rotation, j * 4);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Apply `delta` on top of `base`, scaled by `weight`, into `out`.
|
|
35
|
+
*
|
|
36
|
+
* **A delta, not a target**, and the difference is the whole of what additive is for: a wave laid
|
|
37
|
+
* over a walk has to move the arm relative to wherever the walk put it rather than replace it. So
|
|
38
|
+
* translation adds, rotation composes, and **scale multiplies** — a scale delta of 1 means
|
|
39
|
+
* unchanged, where adding would make it double.
|
|
40
|
+
*
|
|
41
|
+
* Weight zero is the base untouched, which is what lets a layer fade in from nothing.
|
|
42
|
+
*/
|
|
43
|
+
export function addPose(base, delta, weight, out) {
|
|
44
|
+
const w = weight < 0 ? 0 : weight;
|
|
45
|
+
const joints = out.rotation.length / 4;
|
|
46
|
+
for (let j = 0; j < joints; j++) {
|
|
47
|
+
const v = j * 3;
|
|
48
|
+
for (let c = 0; c < 3; c++) {
|
|
49
|
+
out.translation[v + c] =
|
|
50
|
+
base.translation[v + c] + delta.translation[v + c] * w;
|
|
51
|
+
/* One at weight zero, the delta's own factor at weight one, interpolated between. */
|
|
52
|
+
const factor = 1 + (delta.scale[v + c] - 1) * w;
|
|
53
|
+
out.scale[v + c] = base.scale[v + c] * factor;
|
|
54
|
+
}
|
|
55
|
+
/*
|
|
56
|
+
* The delta's rotation is scaled by sliding it away from identity along the shorter arc, then
|
|
57
|
+
* composed onto the base. Scaling by slerping from identity rather than by multiplying the
|
|
58
|
+
* components is what keeps a half-weight delta a half *rotation* instead of a shortened
|
|
59
|
+
* quaternion that renormalises to the whole of it.
|
|
60
|
+
*/
|
|
61
|
+
slerpInto(IDENTITY, 0, delta.rotation, j * 4, w > 1 ? 1 : w, SCALED, 0);
|
|
62
|
+
multiplyInto(base.rotation, j * 4, SCALED, 0, out.rotation, j * 4);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write one joint's transform straight into a pose.
|
|
67
|
+
*
|
|
68
|
+
* What a solver drives. Track B's ragdolls need a pose to be *data* rather than only the result of
|
|
69
|
+
* sampling, and this is the whole of that requirement — stated here so the seam exists before the
|
|
70
|
+
* track that needs it, rather than being retrofitted around a closed type.
|
|
71
|
+
*/
|
|
72
|
+
export function setJoint(pose, joint, translation, rotation, scale) {
|
|
73
|
+
const v = joint * 3;
|
|
74
|
+
const r = joint * 4;
|
|
75
|
+
for (let c = 0; c < 3; c++) {
|
|
76
|
+
pose.translation[v + c] = translation[c];
|
|
77
|
+
pose.scale[v + c] = scale[c];
|
|
78
|
+
}
|
|
79
|
+
for (let c = 0; c < 4; c++)
|
|
80
|
+
pose.rotation[r + c] = rotation[c];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Spherical interpolation between two quaternions held in flat arrays, along the shorter arc.
|
|
84
|
+
*
|
|
85
|
+
* The same arithmetic `clip.ts` performs between two keys, over two poses instead — and with the
|
|
86
|
+
* same hemisphere correction, for the same reason: a quaternion and its negation are one
|
|
87
|
+
* orientation, so two poses can be numerically far apart while being geometrically close, and
|
|
88
|
+
* without the flip a limb rotates the long way through the body.
|
|
89
|
+
*
|
|
90
|
+
* Not shared with `clip.ts` because that one reads a keyframe stride and this one reads a joint
|
|
91
|
+
* stride; the arithmetic between them is eight lines, and a shared function taking four offsets
|
|
92
|
+
* would be harder to read than either. What would make that wrong is a third caller.
|
|
93
|
+
*/
|
|
94
|
+
function slerpInto(a, aAt, b, bAt, t, out, outAt) {
|
|
95
|
+
let ax = a[aAt];
|
|
96
|
+
let ay = a[aAt + 1];
|
|
97
|
+
let az = a[aAt + 2];
|
|
98
|
+
let aw = a[aAt + 3];
|
|
99
|
+
const bx = b[bAt];
|
|
100
|
+
const by = b[bAt + 1];
|
|
101
|
+
const bz = b[bAt + 2];
|
|
102
|
+
const bw = b[bAt + 3];
|
|
103
|
+
let dot = ax * bx + ay * by + az * bz + aw * bw;
|
|
104
|
+
if (dot < 0) {
|
|
105
|
+
ax = -ax;
|
|
106
|
+
ay = -ay;
|
|
107
|
+
az = -az;
|
|
108
|
+
aw = -aw;
|
|
109
|
+
dot = -dot;
|
|
110
|
+
}
|
|
111
|
+
let s0;
|
|
112
|
+
let s1;
|
|
113
|
+
/* Nearly parallel falls back to a straight blend: `acos` at or past 1 is 0 or NaN, and dividing
|
|
114
|
+
by `sin(0)` is the NaN that reaches the palette and takes every vertex the joint touches. */
|
|
115
|
+
if (dot > 0.9995) {
|
|
116
|
+
s0 = 1 - t;
|
|
117
|
+
s1 = t;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const theta = Math.acos(dot);
|
|
121
|
+
const sinTheta = Math.sin(theta);
|
|
122
|
+
s0 = Math.sin((1 - t) * theta) / sinTheta;
|
|
123
|
+
s1 = Math.sin(t * theta) / sinTheta;
|
|
124
|
+
}
|
|
125
|
+
let x = s0 * ax + s1 * bx;
|
|
126
|
+
let y = s0 * ay + s1 * by;
|
|
127
|
+
let z = s0 * az + s1 * bz;
|
|
128
|
+
let w = s0 * aw + s1 * bw;
|
|
129
|
+
const length = Math.hypot(x, y, z, w);
|
|
130
|
+
if (length > 0) {
|
|
131
|
+
x /= length;
|
|
132
|
+
y /= length;
|
|
133
|
+
z /= length;
|
|
134
|
+
w /= length;
|
|
135
|
+
}
|
|
136
|
+
out[outAt] = x;
|
|
137
|
+
out[outAt + 1] = y;
|
|
138
|
+
out[outAt + 2] = z;
|
|
139
|
+
out[outAt + 3] = w;
|
|
140
|
+
}
|
|
141
|
+
/** Quaternion product, in flat arrays. Safe when `out` overlaps either input. */
|
|
142
|
+
function multiplyInto(a, aAt, b, bAt, out, outAt) {
|
|
143
|
+
const ax = a[aAt];
|
|
144
|
+
const ay = a[aAt + 1];
|
|
145
|
+
const az = a[aAt + 2];
|
|
146
|
+
const aw = a[aAt + 3];
|
|
147
|
+
const bx = b[bAt];
|
|
148
|
+
const by = b[bAt + 1];
|
|
149
|
+
const bz = b[bAt + 2];
|
|
150
|
+
const bw = b[bAt + 3];
|
|
151
|
+
out[outAt] = aw * bx + ax * bw + ay * bz - az * by;
|
|
152
|
+
out[outAt + 1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
153
|
+
out[outAt + 2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
154
|
+
out[outAt + 3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
155
|
+
}
|
|
156
|
+
/* Module scope, claimed once: `addPose` runs per character per frame. */
|
|
157
|
+
const IDENTITY = new Float32Array([0, 0, 0, 1]);
|
|
158
|
+
const SCALED = new Float32Array(4);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AnimationClip } from './clip.ts';
|
|
2
|
+
import type { Pose } from './pose.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A tree of clips blended by parameters the game names.
|
|
5
|
+
*
|
|
6
|
+
* **The parameters are the consumer's own strings, never an engine enumeration**, for the reason
|
|
7
|
+
* the input action maps give: a game's verbs are a game's business, and an engine that enumerated
|
|
8
|
+
* them would be deciding what a character can be doing.
|
|
9
|
+
*
|
|
10
|
+
* Evaluation is a pure function of a caller-supplied time and the current parameters. Nothing here
|
|
11
|
+
* reads a clock, which is the contract `sampleClip` establishes and which a graph over clips has
|
|
12
|
+
* to keep or the replay story ends one layer up.
|
|
13
|
+
*
|
|
14
|
+
* **A parameter is one of two things, and the tree keeps them apart.** A *blend* parameter is a
|
|
15
|
+
* weight: which children a set brackets, how far a lerp has gone. A *clock* is a time: which
|
|
16
|
+
* instant of its own clip a node is sampled at, named by a `clip` node's `clock` and defaulting to
|
|
17
|
+
* the time `evaluate` was given. A name may be one or the other and never both — see `clocks` for
|
|
18
|
+
* why that is refused rather than allowed.
|
|
19
|
+
*/
|
|
20
|
+
export type BlendNode = {
|
|
21
|
+
readonly kind: 'clip';
|
|
22
|
+
readonly clip: AnimationClip;
|
|
23
|
+
/**
|
|
24
|
+
* The parameter whose value is this clip's own time, or omitted for the frame clock.
|
|
25
|
+
*
|
|
26
|
+
* **A tree had one clock for every node until 2026-08-28, and that cannot express the
|
|
27
|
+
* canonical blend space.** A locomotion set is stand, walk, run over one speed parameter —
|
|
28
|
+
* and a stride has to advance with **distance travelled** or the foot slides while the body
|
|
29
|
+
* passes over it, which is the fact `rootMotion` exists for, while an idle has to advance
|
|
30
|
+
* with **time**, because somebody standing still is still breathing. On one clock one of the
|
|
31
|
+
* two is wrong: share the distance and the idle freezes whenever nobody moves, share the time
|
|
32
|
+
* and the walk skates. Reported from outside, where the workaround was two clocks kept by
|
|
33
|
+
* hand and a `blendPoses` per overlay — which gives up the declared graph, the scratch poses
|
|
34
|
+
* claimed at construction, and a state machine's crossfades on top.
|
|
35
|
+
*
|
|
36
|
+
* **The value is a time on the clip's own axis, in seconds**, and what advances it is the
|
|
37
|
+
* caller's business: a stride measured in metres is divided by the metres a cycle covers and
|
|
38
|
+
* multiplied by the cycle's duration, and an angle turned is the same arithmetic. The engine
|
|
39
|
+
* does not know about metres, and a parameter that meant metres here would be the engine
|
|
40
|
+
* deciding what a character is doing.
|
|
41
|
+
*
|
|
42
|
+
* **What it gives up** is inheritance: a clock names one clip, so a subtree of four gait
|
|
43
|
+
* clips names it four times. **What would change that** is a consumer with a subtree deep
|
|
44
|
+
* enough for the repetition to hide a mistake, and the answer then is a clock on an interior
|
|
45
|
+
* node that its children inherit — which is a rule about scope, so it is worth having a
|
|
46
|
+
* reason for rather than adding now.
|
|
47
|
+
*/
|
|
48
|
+
readonly clock?: string;
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: 'lerp';
|
|
51
|
+
readonly a: BlendNode;
|
|
52
|
+
readonly b: BlendNode;
|
|
53
|
+
readonly parameter: string;
|
|
54
|
+
} | {
|
|
55
|
+
readonly kind: 'oneDimensional';
|
|
56
|
+
readonly children: readonly {
|
|
57
|
+
readonly at: number;
|
|
58
|
+
readonly node: BlendNode;
|
|
59
|
+
}[];
|
|
60
|
+
readonly parameter: string;
|
|
61
|
+
};
|
|
62
|
+
export declare class BlendTree {
|
|
63
|
+
private readonly root;
|
|
64
|
+
private readonly bind?;
|
|
65
|
+
private readonly parameters;
|
|
66
|
+
/**
|
|
67
|
+
* One scratch pose per interior node, claimed at construction.
|
|
68
|
+
*
|
|
69
|
+
* A tree evaluates depth-first and every interior node needs somewhere to put its two operands,
|
|
70
|
+
* so without these `evaluate` would allocate per node per frame. Keyed by the node object
|
|
71
|
+
* itself, which is stable because a tree is immutable.
|
|
72
|
+
*/
|
|
73
|
+
private readonly scratch;
|
|
74
|
+
/**
|
|
75
|
+
* Which names are clocks, so one cannot quietly be both.
|
|
76
|
+
*
|
|
77
|
+
* A parameter is a blend input and a clock is a time; one number doing both jobs is a rig that
|
|
78
|
+
* responds to the wrong dial, which reads as a broken tree rather than as a name used twice.
|
|
79
|
+
* `clock: 'speed'` written while meaning "the speed drives the blend" is the slip this catches.
|
|
80
|
+
*/
|
|
81
|
+
private readonly clocks;
|
|
82
|
+
/**
|
|
83
|
+
* @param bind The bind pose, or omitted for one whose channels start at rest.
|
|
84
|
+
*
|
|
85
|
+
* **Supply it whenever the clips are rotation-only, which is most of them.** `sampleClip` leaves
|
|
86
|
+
* a channel no track mentions exactly as it found it, so a rotation-only clip preserves whatever
|
|
87
|
+
* translations the pose already held. `blendPoses` cannot do that — it interpolates *every*
|
|
88
|
+
* channel of two poses — so without a bind pose here the scratch poses start at zero translation
|
|
89
|
+
* and every joint collapses onto its parent's origin the moment a tree or a transition is
|
|
90
|
+
* involved. Found by building a demo scene with it: one figure folded in on itself and the other,
|
|
91
|
+
* which happened to go through `retargetPose` instead, did not.
|
|
92
|
+
*/
|
|
93
|
+
constructor(root: BlendNode, jointCount: number, bind?: Pose | undefined);
|
|
94
|
+
/**
|
|
95
|
+
* Set a parameter, refusing a name no node declares.
|
|
96
|
+
*
|
|
97
|
+
* **Loud rather than ignored.** A typo silently does nothing, and what a consumer then sees is
|
|
98
|
+
* an animation that will not respond — a symptom a long way from the misspelling that caused it,
|
|
99
|
+
* and one no test of theirs would catch. What it costs is that a caller cannot set a parameter
|
|
100
|
+
* ahead of building the tree that uses it.
|
|
101
|
+
*/
|
|
102
|
+
set(parameter: string, value: number): void;
|
|
103
|
+
/**
|
|
104
|
+
* Sample the whole tree at `timeSec` into `out`. Allocates nothing.
|
|
105
|
+
*
|
|
106
|
+
* `timeSec` is the clock for every node that did not name one of its own, so a tree of ordinary
|
|
107
|
+
* clips behaves exactly as it did before clocks existed.
|
|
108
|
+
*/
|
|
109
|
+
evaluate(timeSec: number, out: Pose): void;
|
|
110
|
+
/** A scratch pose starting from the bind pose where one was given, or at rest where none was. */
|
|
111
|
+
private blank;
|
|
112
|
+
/** Collect parameter names and claim scratch, once, so evaluation allocates nothing. */
|
|
113
|
+
private prepare;
|
|
114
|
+
private walk;
|
|
115
|
+
}
|