lecodes-sdk 0.20.0 → 1.0.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/dist/global.d.ts +48 -5
- package/dist/inject.js +361 -260
- package/dist/types/audio/Bus.d.ts +45 -0
- package/dist/types/audio/Sound.d.ts +28 -0
- package/dist/types/audio/Voice.d.ts +27 -0
- package/dist/types/audio/audio.d.ts +83 -0
- package/dist/types/audio/support.d.ts +1 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +148 -0
- package/dist/types/gl/Geometry.d.ts +17 -0
- package/dist/types/gl/Light.d.ts +7 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +90 -2
- package/dist/types/gl/Mesh.d.ts +18 -1
- package/dist/types/gl/Model.d.ts +34 -0
- package/dist/types/gl/Particles.d.ts +13 -0
- package/dist/types/gl/Scene.d.ts +23 -0
- package/dist/types/gl/Texture.d.ts +29 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
- package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
- package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/core.d.ts +15 -10
- package/dist/types/gl/audio/AudioSource.d.ts +60 -0
- package/dist/types/gl/audio/AudioZone.d.ts +32 -0
- package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/state.d.ts +0 -1
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +33 -21
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/runtime/input.d.ts +11 -0
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types/ui/UIImage.d.ts +15 -5
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/dist/2d-game.md +408 -197
- package/prompts/dist/3d-app.md +491 -166
- package/prompts/dist/ar-app.md +373 -163
- package/prompts/dist/design.md +83 -87
- package/prompts/dist/ui-app.md +325 -136
- package/src/audio/Bus.ts +102 -0
- package/src/audio/Sound.ts +96 -0
- package/src/audio/Voice.ts +102 -0
- package/src/audio/audio.ts +161 -0
- package/src/audio/support.ts +6 -0
- package/src/bridges.d.ts +279 -32
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/compileProject.ts +30 -15
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +4 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/core/Aspect.ts +33 -8
- package/src/g2/Scene2D.ts +7 -0
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/DecalSet.ts +360 -0
- package/src/gl/Geometry.ts +348 -279
- package/src/gl/Light.ts +16 -0
- package/src/gl/Lightmap.ts +35 -7
- package/src/gl/Material.ts +173 -4
- package/src/gl/Mesh.ts +120 -83
- package/src/gl/Model.ts +33 -1
- package/src/gl/Node.ts +1 -1
- package/src/gl/Particles.ts +21 -3
- package/src/gl/Scene.ts +41 -7
- package/src/gl/Texture.ts +43 -3
- package/src/gl/animation/AnimationClip.ts +43 -20
- package/src/gl/animation/Animator.ts +4 -3
- package/src/gl/animation/DynamicBone.ts +459 -0
- package/src/gl/{IK.ts → animation/IK.ts} +4 -4
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/core.ts +20 -15
- package/src/gl/audio/AudioSource.ts +113 -0
- package/src/gl/audio/AudioZone.ts +75 -0
- package/src/gl/audio/SceneAudio.ts +26 -0
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +226 -212
- package/src/runtime/files.ts +32 -2
- package/src/runtime/input.ts +6 -1
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/gizmos.ts +148 -148
- package/src/scene/level.ts +2 -2
- package/src/ui/UIImage.ts +21 -7
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// DynamicBone — secondary motion for tails, ears, hair, ropes and cloaks, simulated in the engine.
|
|
2
|
+
// Attach to the ROOT bone of a chain; the bone's whole subtree becomes Verlet particle chains that
|
|
3
|
+
// swing, lag and settle under gravity, wind and the character's motion, pulled back toward the pose
|
|
4
|
+
// the animation gives them, kept out of the body by capsule colliders and above the floor.
|
|
5
|
+
//
|
|
6
|
+
// const tail = fox.bone('tail')!
|
|
7
|
+
// tail.aspect(DynamicBone, { stiffness: [0.6, 0.15], damping: 0.15, gravity: 2, angleLimit: 50 })
|
|
8
|
+
// fox.bone('earend')!.aspect(DynamicBone, { stiffness: 0.8, angleLimit: 25 })
|
|
9
|
+
// fox.bone('Hips')!.aspect(DynamicBoneCollider, { radius: 0.12, to: 'chest' }) // a capsule the tail can't enter
|
|
10
|
+
// tail.dynamicBone.weight = cutscene ? 0 : 1 // blend to the animation
|
|
11
|
+
// tail.dynamicBone.floor = hit.point // the ground under it
|
|
12
|
+
// tail.dynamicBone.reset() // after a teleport
|
|
13
|
+
//
|
|
14
|
+
// Per-bone values (`radius`, `stiffness`, `damping`, `gravity`, `angleLimit`) take one number for the
|
|
15
|
+
// whole chain or a `[root, tip]` pair interpolated down the chain — a tail is stiff at the base and
|
|
16
|
+
// loose at the tip. A cloak is one aspect on the common parent of its columns with `link` > 0: the
|
|
17
|
+
// bones at the same depth of neighbouring columns are tied at their rest distance.
|
|
18
|
+
//
|
|
19
|
+
// The simulation runs natively in a stage of its own after the late phase and before the skin flush
|
|
20
|
+
// (docs/parity: gl-dynamic-bones): the animator, an active ragdoll and late-phase aspects (IK) are
|
|
21
|
+
// its input; every field of this aspect is live — the values are pushed when they change.
|
|
22
|
+
|
|
23
|
+
import { Aspect } from "../../core/Aspect"
|
|
24
|
+
import type { FieldMeta } from "../../core/fields"
|
|
25
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
|
|
26
|
+
import { Node } from "../Node"
|
|
27
|
+
import { Model } from "../Model"
|
|
28
|
+
import { HUMANOID, humanoidParts } from "../physics/Ragdoll"
|
|
29
|
+
|
|
30
|
+
/** One value for the chain, or `[root, tip]` interpolated down it by depth. */
|
|
31
|
+
export type DynamicBoneCurve = number | readonly [root: number, tip: number]
|
|
32
|
+
/** `'none'`, `'probe'` (a ray from the chain root down against the physics world, every frame), a
|
|
33
|
+
* height (a horizontal plane at that world y) or a world point (a plane through it, `floorNormal` up). */
|
|
34
|
+
export type DynamicBoneFloor = "none" | "probe" | number | Vec3Like
|
|
35
|
+
/** `'auto'` = every DynamicBoneCollider under the model; `'humanoid'` = those plus capsules generated
|
|
36
|
+
* on the standard humanoid bones (hips, spine, head, limbs — the Ragdoll's layout); `'none'`; or the
|
|
37
|
+
* nodes carrying the colliders to use. */
|
|
38
|
+
export type DynamicBoneColliders = "auto" | "humanoid" | "none" | Node[]
|
|
39
|
+
/** The cloth's OUTSIDE, which makes every collider one-sided: `'none'` = a collider pushes a bone to its
|
|
40
|
+
* nearest surface (tails, ropes); `'auto'` = away from the root bone's own axis (a cloak's strips round a
|
|
41
|
+
* spine face out from it); a vector = a fixed direction in the root bone's frame (a cape on the back:
|
|
42
|
+
* `[0, 0, -1]`). With a side the body is under the cloth by definition: an arm that swung through the
|
|
43
|
+
* cape pushes it away, and a bone that ended up inside a capsule leaves it on the outside — never
|
|
44
|
+
* through it to be held against the chest from the wrong side. */
|
|
45
|
+
export type DynamicBoneSide = "none" | "auto" | Vec3Like
|
|
46
|
+
|
|
47
|
+
const VERSION = 1
|
|
48
|
+
const PARAMS = 25 // CANIM_DYN_COUNT
|
|
49
|
+
const HEADER = 3 + PARAMS
|
|
50
|
+
const STRIDE = 7 // CANIM_DYN_BONE_STRIDE
|
|
51
|
+
const FLOOR_NONE = 0, FLOOR_PLANE = 1, FLOOR_PROBE = 2
|
|
52
|
+
|
|
53
|
+
let warned = false
|
|
54
|
+
const supported = (): boolean => {
|
|
55
|
+
if (_creator.dynamicBoneCreate) return true
|
|
56
|
+
if (!warned) { warned = true; console.warn("DynamicBone needs a newer host (no dynamicBoneCreate) — the animation shows as is") }
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const at = (c: DynamicBoneCurve, t: number): number => typeof c === "number" ? c : c[0] + (c[1] - c[0]) * t
|
|
61
|
+
|
|
62
|
+
/** The model a bone belongs to: the topmost Model ancestor, else the topmost ancestor. */
|
|
63
|
+
const modelOf = (n: Node): Node => {
|
|
64
|
+
let top: Node = n, model: Node | null = null
|
|
65
|
+
for (let p: Node | null = n; p; p = p.parent) { top = p; if (p instanceof Model) model = p }
|
|
66
|
+
return model ?? top
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A capsule (or a sphere) riding a bone — what dynamic bones stay out of. Attach to a body bone:
|
|
70
|
+
* `hips.aspect(DynamicBoneCollider, { radius: 0.12, to: 'Spine' })`. The capsule runs from
|
|
71
|
+
* `offset` (the bone's origin by default) to the `to` bone's origin, or to `end` (both in the bone's
|
|
72
|
+
* local space); neither = a sphere at `offset`. Picked up by every DynamicBone on the same model
|
|
73
|
+
* with `colliders: 'auto'` / `'humanoid'`, or listed explicitly. */
|
|
74
|
+
export class DynamicBoneCollider extends Aspect<"dynamicBoneCollider", Node> {
|
|
75
|
+
static readonly aspect = "dynamicBoneCollider"
|
|
76
|
+
|
|
77
|
+
/** Radius in metres, in the world: the bone's scale does not touch it (`offset` / `end` are in the bone's own units). */
|
|
78
|
+
radius = 0.1
|
|
79
|
+
/** Start of the capsule in the bone's local space (default: the bone's origin). */
|
|
80
|
+
offset: Vec3Like = [ 0, 0, 0 ]
|
|
81
|
+
/** End of the capsule in the bone's local space (a sphere when neither `end` nor `to` is set). */
|
|
82
|
+
end?: Vec3Like
|
|
83
|
+
/** The bone the capsule reaches (its origin), instead of `end`. */
|
|
84
|
+
to?: string
|
|
85
|
+
|
|
86
|
+
static fields: FieldMeta<DynamicBoneCollider> = {
|
|
87
|
+
radius: { min: 0, max: 2, step: 0.005 },
|
|
88
|
+
offset: { hidden: true },
|
|
89
|
+
end: { hidden: true },
|
|
90
|
+
to: { hidden: true },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _id = 0
|
|
94
|
+
|
|
95
|
+
/** @internal the engine's collider id (0 when the host has none). */
|
|
96
|
+
get _colliderId(): number { return this._id }
|
|
97
|
+
|
|
98
|
+
private _segment(): [Vec3, Vec3] {
|
|
99
|
+
const a = new Vec3(this.offset)
|
|
100
|
+
if (this.to) {
|
|
101
|
+
const target = modelOf(this.node).bone(this.to)
|
|
102
|
+
if (!target) throw new Error(`DynamicBoneCollider: no bone '${this.to}' (the 'to' of '${this.node.name}')`)
|
|
103
|
+
const b = this.node.worldMatrix.invert().transformPoint(target.worldPosition)
|
|
104
|
+
return [ a, b ]
|
|
105
|
+
}
|
|
106
|
+
return [ a, this.end ? new Vec3(this.end) : a ]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onAttach(): void {
|
|
110
|
+
if (!supported()) return
|
|
111
|
+
const [ a, b ] = this._segment()
|
|
112
|
+
this._id = _creator.dynamicBoneColliderCreate!(this.node.id, a.x, a.y, a.z, b.x, b.y, b.z, this.radius)
|
|
113
|
+
DynamicBone._collidersChanged()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onReconfigure(): void {
|
|
117
|
+
if (!this._id) return
|
|
118
|
+
const [ a, b ] = this._segment()
|
|
119
|
+
_creator.dynamicBoneColliderSet!(this._id, a.x, a.y, a.z, b.x, b.y, b.z, this.radius)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onDetach(): void {
|
|
123
|
+
if (this._id) { _creator.dynamicBoneColliderDestroy!(this._id); this._id = 0; DynamicBone._collidersChanged() }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class DynamicBone extends Aspect<"dynamicBone", Node> {
|
|
128
|
+
static readonly aspect = "dynamicBone"
|
|
129
|
+
|
|
130
|
+
/** Only these bones (by name) join the chain — the root always does, a bone left out takes its
|
|
131
|
+
* subtree with it. Default: the root's whole subtree. */
|
|
132
|
+
bones?: string[]
|
|
133
|
+
/** Depth limit under the root (0 = no limit). */
|
|
134
|
+
depth = 0
|
|
135
|
+
/** Particle radius for the collisions, metres. */
|
|
136
|
+
radius: DynamicBoneCurve = 0.02
|
|
137
|
+
/** 0..1 — how fast a bone returns to the animated shape (per 1/60 s). 0 = a rope, 1 = rigid. */
|
|
138
|
+
stiffness: DynamicBoneCurve = 0.3
|
|
139
|
+
/** 0..1 — velocity lost per 1/60 s. 0.02 swings like a rope, 0.1 settles like a tail, 0.3 is honey. */
|
|
140
|
+
damping: DynamicBoneCurve = 0.1
|
|
141
|
+
/** m/s² downward. A hanging rest pose feels none of it (the bone length cancels it); a pose that
|
|
142
|
+
* sticks out droops by as much as `stiffness` lets it. */
|
|
143
|
+
gravity: DynamicBoneCurve = 0
|
|
144
|
+
/** Max deviation from the animated direction, degrees per bone (0 = free). */
|
|
145
|
+
angleLimit: DynamicBoneCurve = 0
|
|
146
|
+
/** Relative mass per bone. A bone-length constraint moves its two ends in inverse proportion to
|
|
147
|
+
* their masses (the root is kinematic): `[3, 1]` makes a tail's base carry its tip, `1` shares
|
|
148
|
+
* evenly like a rope. */
|
|
149
|
+
mass: DynamicBoneCurve = 1
|
|
150
|
+
/** Bones (by name) that ride the animation exactly while their subtrees still simulate: a sheet's
|
|
151
|
+
* several roots under one anchor (a cloak's seven strips under the spine), so ONE chain owns them all
|
|
152
|
+
* and `link` ties neighbouring strips. */
|
|
153
|
+
pinned?: string[]
|
|
154
|
+
/** Metres a PINNED bone may be pushed off its animated place by a collider — a soft pin. Its particle
|
|
155
|
+
* collides and takes the edge pushes like a free bone (the strip below hangs from where it IS) and is
|
|
156
|
+
* drawn back to the animation when the collider leaves; the bone's local translation follows. A hard
|
|
157
|
+
* pin (0) at a capsule's edge made the first free bone below it jitter: the edge push on a segment
|
|
158
|
+
* with an immovable end is a lever, and a touch near the root threw the child out. 2–3 cm for a
|
|
159
|
+
* cloak's roots under a shoulder capsule. */
|
|
160
|
+
pinGive = 0
|
|
161
|
+
/** 0..1 of the character's travel the chain takes rigidly (0 = the full whip on a dash, 1 = it
|
|
162
|
+
* moves with the body and only the pose's own motion swings it). */
|
|
163
|
+
follow = 0
|
|
164
|
+
/** World wind, m/s². */
|
|
165
|
+
wind: Vec3Like = [ 0, 0, 0 ]
|
|
166
|
+
/** The BODY'S TURN, ≥ 0. Above 0 the chain lives in the parent bone's ROTATING frame: `damping` acts
|
|
167
|
+
* against the body's rigid motion (its travel plus its spin), so a heavily damped cloak RIDES a turn —
|
|
168
|
+
* sweeps round with the back — instead of standing in the world while the character spins under it and
|
|
169
|
+
* being dragged round after. The turn's inertia comes back as the frame's forces, scaled by this value:
|
|
170
|
+
* a centrifugal push `ω² · r` along each bone's animated direction from the axis (the hem, farthest
|
|
171
|
+
* from the spine, flies out and, on the chain's length, up). The frame's own physics is built in: the
|
|
172
|
+
* whip back when the turn starts and forward when it stops, Coriolis. 1 = the physical push; more = a
|
|
173
|
+
* bigger fling. With low damping the frame reproduces a free particle (nothing is counted twice);
|
|
174
|
+
* an animation's spine sway is ~1 % of a 900°/s turn. A rotation over 90° in one frame (a respawn
|
|
175
|
+
* facing) counts as a snap, not a spin. 0 = the plain translational frame, no turn forces. */
|
|
176
|
+
spin = 0
|
|
177
|
+
/** With `spin`: the cloth's INERTIA against the body's turn, seconds — the cloth's own rotation follows the
|
|
178
|
+
* body's with this time constant. It falls behind when a turn starts (by about ω·τ: 45° at 900°/s and 0.05),
|
|
179
|
+
* rides once caught up, and keeps turning past the back when the body stops (the same angle, eased out over
|
|
180
|
+
* τ). Smooth by construction. 0 = glued to the turn; large = the cloth stays in the world while the body spins. */
|
|
181
|
+
spinInertia = 0
|
|
182
|
+
/** 0..1 strength of the ties between neighbouring columns at the same depth (a cloak). */
|
|
183
|
+
link = 0
|
|
184
|
+
/** 0..1 blend from the animation (0) to the simulation (1). At 0 nothing is simulated and the
|
|
185
|
+
* chain re-arms on the animated pose when it comes back. */
|
|
186
|
+
weight = 1
|
|
187
|
+
/** Substep rate, Hz (≤ 4 substeps per frame). */
|
|
188
|
+
rate = 60
|
|
189
|
+
/** A root jump longer than this in one frame (metres) resets the chain instead of whipping it. */
|
|
190
|
+
teleport = 1
|
|
191
|
+
/** What the particles stay above: `'none'`, `'probe'`, a height, or a world point. */
|
|
192
|
+
floor: DynamicBoneFloor = "none"
|
|
193
|
+
/** The floor plane's normal when `floor` is a point. */
|
|
194
|
+
floorNormal: Vec3Like = [ 0, 1, 0 ]
|
|
195
|
+
/** Friction against the floor, a Coulomb coefficient: a resting bone's slide loses up to
|
|
196
|
+
* `floorFriction · gravity · dt` of speed per substep (0 = ice, 1 = a rubber sole). While a bone
|
|
197
|
+
* rests on the floor or a collider, `stiffness` and `angleLimit` let go of it: the surface's
|
|
198
|
+
* reaction outranks the spring, or a hem folded on the floor would run away along it. */
|
|
199
|
+
floorFriction = 0.5
|
|
200
|
+
/** Constraint passes per substep (1..8, default 4). Each pass shares every bone length between its
|
|
201
|
+
* ends, so a pull needs passes to travel down a long chain: a long rope may want 8, a short tail is
|
|
202
|
+
* fine with 2. */
|
|
203
|
+
iterations = 4
|
|
204
|
+
/** What the chain collides with (see DynamicBoneColliders). */
|
|
205
|
+
colliders: DynamicBoneColliders = "auto"
|
|
206
|
+
/** The cloth's outside — makes the colliders one-sided (see DynamicBoneSide). */
|
|
207
|
+
side: DynamicBoneSide = "none"
|
|
208
|
+
/** How firmly `guide()` targets are HELD through the constraint passes, 0..1. At 0 a guide is applied once
|
|
209
|
+
* before the passes and the length / link passes may drag the bone back toward its un-guided neighbours
|
|
210
|
+
* within the same frame; at 0.5 it is re-pulled after every pass with half its weight (converges with the
|
|
211
|
+
* passes), at 1 with its full weight (a target the bone length cannot reach then jitters). */
|
|
212
|
+
guideHold = 0
|
|
213
|
+
/** The chain's EDGES collide with the capsules too — every bone segment and every link, not only the bones'
|
|
214
|
+
* particles: a capsule thinner than a bone's length (a forearm) no longer passes through the cloth between
|
|
215
|
+
* two bones. Off = particles only. */
|
|
216
|
+
edges = true
|
|
217
|
+
|
|
218
|
+
static fields: FieldMeta<DynamicBone> = {
|
|
219
|
+
depth: { min: 0, max: 32, step: 1 },
|
|
220
|
+
radius: { min: 0, max: 1, step: 0.005 },
|
|
221
|
+
stiffness: { min: 0, max: 1, step: 0.01 },
|
|
222
|
+
damping: { min: 0, max: 1, step: 0.01 },
|
|
223
|
+
gravity: { min: 0, max: 30, step: 0.1 },
|
|
224
|
+
angleLimit: { label: "Angle limit (°)", min: 0, max: 180, step: 1 },
|
|
225
|
+
mass: { min: 0.1, max: 10, step: 0.1 },
|
|
226
|
+
follow: { min: 0, max: 1, step: 0.01 },
|
|
227
|
+
link: { min: 0, max: 1, step: 0.01 },
|
|
228
|
+
weight: { min: 0, max: 1, step: 0.01 },
|
|
229
|
+
rate: { min: 30, max: 240, step: 10 },
|
|
230
|
+
teleport: { min: 0, max: 20, step: 0.1 },
|
|
231
|
+
floorFriction: { label: "Floor friction", min: 0, max: 1, step: 0.01 },
|
|
232
|
+
iterations: { min: 1, max: 8, step: 1 },
|
|
233
|
+
bones: { hidden: true },
|
|
234
|
+
pinned: { hidden: true },
|
|
235
|
+
pinGive: { label: "Pin give (m)", min: 0, max: 0.2, step: 0.005 },
|
|
236
|
+
wind: { hidden: true },
|
|
237
|
+
spin: { min: 0, max: 4, step: 0.05 },
|
|
238
|
+
spinInertia: { label: "Spin inertia (s)", min: 0, max: 0.3, step: 0.005 },
|
|
239
|
+
floor: { hidden: true },
|
|
240
|
+
floorNormal: { hidden: true },
|
|
241
|
+
colliders: { hidden: true },
|
|
242
|
+
side: { hidden: true },
|
|
243
|
+
guideHold: { min: 0, max: 1, step: 0.05 },
|
|
244
|
+
edges: { hidden: true },
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private _id = 0
|
|
248
|
+
private _chain: Node[] = []
|
|
249
|
+
private _depths: number[] = []
|
|
250
|
+
private _blob = new Float32Array(0)
|
|
251
|
+
private _sent = new Float32Array(0)
|
|
252
|
+
private _floorKey = ""
|
|
253
|
+
private _colliderKey = ""
|
|
254
|
+
private _collidersDirty = true
|
|
255
|
+
private _collidersRef: DynamicBoneColliders | null = null
|
|
256
|
+
private _own: number[] = [] // colliders generated for 'humanoid' (destroyed with the aspect)
|
|
257
|
+
private _colliderSet: (Node | "auto" | "humanoid" | "none")[] | null = null
|
|
258
|
+
|
|
259
|
+
/** The bones of the chain, the root first. */
|
|
260
|
+
get chain(): readonly Node[] { return this._chain }
|
|
261
|
+
|
|
262
|
+
onAttach(): void {
|
|
263
|
+
if (!supported()) return
|
|
264
|
+
this._collect()
|
|
265
|
+
if (this._chain.length === 0) return
|
|
266
|
+
const n = this._chain.length
|
|
267
|
+
const bones = new Uint32Array(n)
|
|
268
|
+
for (let i = 0; i < n; i++) bones[i] = this._chain[i]!.id
|
|
269
|
+
this._blob = new Float32Array(HEADER + n * STRIDE)
|
|
270
|
+
this._fill(this._blob)
|
|
271
|
+
this._id = _creator.dynamicBoneCreate!(modelOf(this.node).id, bones, this._blob, this._links())
|
|
272
|
+
if (!this._id) { console.warn(`DynamicBone: the host refused the chain under '${this.node.name}'`); return }
|
|
273
|
+
this._sent = this._blob.slice()
|
|
274
|
+
this._floorKey = ""; this._colliderKey = ""; this._collidersDirty = true
|
|
275
|
+
this._pushFloor()
|
|
276
|
+
this._pushColliders()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
onReconfigure(): void { this._sync() }
|
|
280
|
+
|
|
281
|
+
/** Every field is live: what changed since the last frame is pushed here, before the engine's stage. */
|
|
282
|
+
update(): void { this._sync() }
|
|
283
|
+
|
|
284
|
+
onDetach(): void {
|
|
285
|
+
if (this._id) { _creator.dynamicBoneDestroy!(this._id); this._id = 0 }
|
|
286
|
+
for (const id of this._own) _creator.dynamicBoneColliderDestroy!(id)
|
|
287
|
+
this._own = []
|
|
288
|
+
this._chain = []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Snap the chain onto the animated pose next frame (a teleport, a camera cut). */
|
|
292
|
+
reset(): void { if (this._id) _creator.dynamicBoneReset!(this._id) }
|
|
293
|
+
|
|
294
|
+
/** GUIDES: world points the bones' particles are drawn to each substep before the constraints —
|
|
295
|
+
* weight 1 = there before them, the rest of the chain hangs, collides and links as before (a cape's
|
|
296
|
+
* strips riding the arms). Replaces the previous set; an empty list clears. A late-phase script
|
|
297
|
+
* (after the animator, before the engine's chain stage) refreshes it every frame. */
|
|
298
|
+
guide(rows: readonly { bone: Node, at: Vec3Like, weight: number }[]): void {
|
|
299
|
+
if (!this._id) return
|
|
300
|
+
const out: number[] = []
|
|
301
|
+
for (const r of rows) {
|
|
302
|
+
const i = this._chain.indexOf(r.bone)
|
|
303
|
+
if (i < 0) continue
|
|
304
|
+
out.push(i, cx(r.at), cy(r.at), cz(r.at), r.weight)
|
|
305
|
+
}
|
|
306
|
+
_creator.dynamicBoneTargets?.(this._id, out.length ? new Float32Array(out) : null)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The particles' world positions (the bones, then the leaves' virtual tips) — for a debug draw. */
|
|
310
|
+
get particles(): Vec3[] {
|
|
311
|
+
if (!this._id) return []
|
|
312
|
+
const out = new Float32Array((this._chain.length * 2) * 3)
|
|
313
|
+
const n = _creator.dynamicBoneParticles!(this._id, out)
|
|
314
|
+
const pts: Vec3[] = []
|
|
315
|
+
for (let i = 0; i < n; i++) pts.push(new Vec3(out[i * 3]!, out[i * 3 + 1]!, out[i * 3 + 2]!))
|
|
316
|
+
return pts
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- internals ----------------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
private _collect(): void {
|
|
322
|
+
const chain: Node[] = [], depths: number[] = []
|
|
323
|
+
const only = this.bones ? new Set(this.bones) : null
|
|
324
|
+
const walk = (n: Node, d: number): void => {
|
|
325
|
+
chain.push(n); depths.push(d)
|
|
326
|
+
if (this.depth > 0 && d >= this.depth) return
|
|
327
|
+
for (const c of n.children) {
|
|
328
|
+
if (only && !only.has(c.name)) continue
|
|
329
|
+
if (c.has(DynamicBone) || c.has(DynamicBoneCollider)) continue // another chain / a body bone
|
|
330
|
+
walk(c, d + 1)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
walk(this.node, 0)
|
|
334
|
+
this._chain = chain; this._depths = depths
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private _fill(b: Float32Array): void {
|
|
338
|
+
const n = this._chain.length
|
|
339
|
+
let maxDepth = 0
|
|
340
|
+
for (const d of this._depths) if (d > maxDepth) maxDepth = d
|
|
341
|
+
b[0] = VERSION; b[1] = n; b[2] = STRIDE
|
|
342
|
+
b[3] = this.weight; b[4] = this.follow
|
|
343
|
+
b[5] = cx(this.wind); b[6] = cy(this.wind); b[7] = cz(this.wind)
|
|
344
|
+
b[8] = this.link; b[9] = this.rate; b[10] = this.teleport
|
|
345
|
+
b[11] = 0; b[12] = 0; b[13] = 0; b[14] = 0; b[15] = 0; b[16] = 1; b[17] = 0 // the floor goes through setFloor
|
|
346
|
+
b[18] = this.floorFriction
|
|
347
|
+
b[19] = Math.max(1, Math.min(8, Math.round(this.iterations)))
|
|
348
|
+
const side = this.side
|
|
349
|
+
const sideVec = side === "none" || side === "auto" ? null : side
|
|
350
|
+
b[20] = side === "none" ? 0 : side === "auto" ? 1 : 2
|
|
351
|
+
b[21] = sideVec ? cx(sideVec) : 0; b[22] = sideVec ? cy(sideVec) : 0; b[23] = sideVec ? cz(sideVec) : 0
|
|
352
|
+
b[24] = Math.max(0, Math.min(1, this.guideHold))
|
|
353
|
+
b[25] = this.edges ? 1 : 0
|
|
354
|
+
b[26] = Math.max(0, this.spin)
|
|
355
|
+
b[27] = Math.max(0, this.spinInertia)
|
|
356
|
+
const pin = this.pinned?.length ? new Set(this.pinned) : null
|
|
357
|
+
for (let i = 0; i < n; i++) {
|
|
358
|
+
const t = maxDepth > 0 ? this._depths[i]! / maxDepth : 0
|
|
359
|
+
const o = HEADER + i * STRIDE
|
|
360
|
+
b[o] = at(this.radius, t); b[o + 1] = at(this.stiffness, t); b[o + 2] = at(this.damping, t)
|
|
361
|
+
b[o + 3] = at(this.gravity, t); b[o + 4] = at(this.angleLimit, t)
|
|
362
|
+
const isPinned = !!pin && i > 0 && pin.has(this._chain[i]!.name)
|
|
363
|
+
b[o + 5] = isPinned ? -1 : at(this.mass, t) // mass < 0 = pinned
|
|
364
|
+
b[o + 6] = isPinned ? Math.max(0, this.pinGive) : 0
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Neighbour links: the bones at the same depth, in tree order, tied pairwise. */
|
|
369
|
+
private _links(): Uint16Array | undefined {
|
|
370
|
+
if (this.link <= 0) return undefined
|
|
371
|
+
const byDepth = new Map<number, number[]>()
|
|
372
|
+
this._depths.forEach((d, i) => { if (d > 0) (byDepth.get(d) ?? byDepth.set(d, []).get(d)!).push(i) })
|
|
373
|
+
const pairs: number[] = []
|
|
374
|
+
for (const row of byDepth.values()) for (let k = 0; k + 1 < row.length; k++) pairs.push(row[k]!, row[k + 1]!)
|
|
375
|
+
return pairs.length ? new Uint16Array(pairs) : undefined
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private _sync(): void {
|
|
379
|
+
if (!this._id) return
|
|
380
|
+
this._fill(this._blob)
|
|
381
|
+
let changed = false
|
|
382
|
+
for (let i = 0; i < this._blob.length; i++) if (this._blob[i] !== this._sent[i]) { changed = true; break }
|
|
383
|
+
if (changed) { _creator.dynamicBoneSet!(this._id, this._blob); this._sent.set(this._blob) }
|
|
384
|
+
this._pushFloor()
|
|
385
|
+
this._pushColliders()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private _pushFloor(): void {
|
|
389
|
+
const f = this.floor
|
|
390
|
+
let key: string, mode: 0 | 1 | 2 = FLOOR_NONE, x = 0, y = 0, z = 0, nx = 0, ny = 1, nz = 0
|
|
391
|
+
if (f === "probe") { mode = FLOOR_PROBE; key = "probe" }
|
|
392
|
+
else if (f === "none" || f === undefined || f === null) { key = "none" }
|
|
393
|
+
else if (typeof f === "number") { mode = FLOOR_PLANE; y = f; key = `y${f}` }
|
|
394
|
+
else {
|
|
395
|
+
mode = FLOOR_PLANE; x = cx(f); y = cy(f); z = cz(f)
|
|
396
|
+
nx = cx(this.floorNormal); ny = cy(this.floorNormal); nz = cz(this.floorNormal)
|
|
397
|
+
key = `p${x},${y},${z},${nx},${ny},${nz}`
|
|
398
|
+
}
|
|
399
|
+
if (key === this._floorKey) return
|
|
400
|
+
this._floorKey = key
|
|
401
|
+
_creator.dynamicBoneSetFloor!(this._id, mode, x, y, z, nx, ny, nz)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _pushColliders(): void {
|
|
405
|
+
// re-listed only when a collider came or went, or the field was reassigned (per frame otherwise)
|
|
406
|
+
if (!this._collidersDirty && this._collidersRef === this.colliders) return
|
|
407
|
+
this._collidersDirty = false
|
|
408
|
+
this._collidersRef = this.colliders
|
|
409
|
+
const ids: number[] = []
|
|
410
|
+
const model = modelOf(this.node)
|
|
411
|
+
const c = this.colliders
|
|
412
|
+
if (c === "auto" || c === "humanoid") {
|
|
413
|
+
for (const col of Aspect.all(DynamicBoneCollider)) if (col._colliderId && modelOf(col.node) === model) ids.push(col._colliderId)
|
|
414
|
+
if (c === "humanoid") { if (this._own.length === 0) this._makeHumanoid(model); ids.push(...this._own) }
|
|
415
|
+
} else if (Array.isArray(c)) {
|
|
416
|
+
for (const n of c) {
|
|
417
|
+
const col = n.get(DynamicBoneCollider)
|
|
418
|
+
if (col?._colliderId) ids.push(col._colliderId)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const key = ids.join(",")
|
|
422
|
+
if (key === this._colliderKey) return
|
|
423
|
+
this._colliderKey = key
|
|
424
|
+
_creator.dynamicBoneSetColliders!(this._id, new Uint32Array(ids))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Capsules on the standard humanoid bones, sized like the Ragdoll's parts. */
|
|
428
|
+
private _makeHumanoid(model: Node): void {
|
|
429
|
+
const find = (role: string): string | null => {
|
|
430
|
+
for (const name of HUMANOID[role] ?? []) if (model.bone(name)) return name
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
let parts: ReturnType<typeof humanoidParts>
|
|
434
|
+
try { parts = humanoidParts(find) } catch { return } // no hips: not a humanoid, nothing generated
|
|
435
|
+
for (const p of parts) {
|
|
436
|
+
const bone = model.bone(p.bone)
|
|
437
|
+
if (!bone) continue
|
|
438
|
+
const inv = bone.worldMatrix.invert()
|
|
439
|
+
let end: Vec3
|
|
440
|
+
const to = p.to ? model.bone(p.to) : null
|
|
441
|
+
if (to) end = inv.transformPoint(to.worldPosition)
|
|
442
|
+
else {
|
|
443
|
+
// a leaf: along the line from the parent part's origin through this bone
|
|
444
|
+
const parentBone = p.parent ? model.bone(p.parent) : null
|
|
445
|
+
const dir = parentBone ? bone.worldPosition.sub(parentBone.worldPosition) : new Vec3(0, 1, 0)
|
|
446
|
+
const local = inv.transformDirection(dir)
|
|
447
|
+
const len = local.length()
|
|
448
|
+
end = len > 1e-6 ? local.scale((p.length ?? 0.2) / len) : new Vec3(0, p.length ?? 0.2, 0)
|
|
449
|
+
}
|
|
450
|
+
const id = _creator.dynamicBoneColliderCreate!(bone.id, 0, 0, 0, end.x, end.y, end.z, p.radius ?? 0.05)
|
|
451
|
+
if (id) this._own.push(id)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** @internal a collider attached / detached: every chain that collects them re-lists. */
|
|
456
|
+
static _collidersChanged(): void {
|
|
457
|
+
for (const d of Aspect.all(DynamicBone)) if (d._id && (d.colliders === "auto" || d.colliders === "humanoid")) { d._collidersDirty = true; d._pushColliders() }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
// Pure SDK/JS: a handful of world-matrix reads + two quaternion writes per solve. A native solver can
|
|
12
12
|
// slot in later behind the same API.
|
|
13
13
|
|
|
14
|
-
import { Aspect } from "
|
|
15
|
-
import { Vec3, type Vec3Like } from "
|
|
16
|
-
import { Quat } from "
|
|
17
|
-
import type { Node } from "
|
|
14
|
+
import { Aspect } from "../../core/Aspect"
|
|
15
|
+
import { Vec3, type Vec3Like } from "../../math/vec"
|
|
16
|
+
import { Quat } from "../../math/quat"
|
|
17
|
+
import type { Node } from "../Node"
|
|
18
18
|
|
|
19
19
|
type Target = Node | Vec3Like
|
|
20
20
|
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
// how fast the gait moves, where its foot contacts fall), so nothing here is a hand-measured table.
|
|
28
28
|
// Give `angle` / `speed` only to override a measurement.
|
|
29
29
|
|
|
30
|
-
import { Aspect } from "
|
|
31
|
-
import { Vec3, cx, cz, type Vec3Like } from "
|
|
32
|
-
import { Animator } from "./
|
|
33
|
-
import type { FeetOptions } from "./
|
|
34
|
-
import type { WarpOptions } from "./
|
|
35
|
-
import { CharacterController } from "
|
|
36
|
-
import type { Node } from "
|
|
30
|
+
import { Aspect } from "../../core/Aspect"
|
|
31
|
+
import { Vec3, cx, cz, type Vec3Like } from "../../math/vec"
|
|
32
|
+
import { Animator } from "./Animator"
|
|
33
|
+
import type { FeetOptions } from "./Feet"
|
|
34
|
+
import type { WarpOptions } from "./Warp"
|
|
35
|
+
import { CharacterController } from "../physics/CharacterController"
|
|
36
|
+
import type { Node } from "../Node"
|
|
37
37
|
|
|
38
38
|
/** How fast the character wants to go: the gait picks its clips and its speed. */
|
|
39
39
|
export type Gait = "walk" | "run" | "sprint"
|
package/src/gl/animation/core.ts
CHANGED
|
@@ -18,19 +18,17 @@ import type { Node } from "../Node"
|
|
|
18
18
|
/** A position on a blend axis (1D) or plane (2D). */
|
|
19
19
|
export type BlendPosition = number | readonly [number, number]
|
|
20
20
|
|
|
21
|
-
/** The transition time (seconds) a play / playLoop without `fade` uses. */
|
|
22
|
-
export const DEFAULT_FADE = 0.2
|
|
23
|
-
|
|
24
21
|
export type PlayOptions = {
|
|
25
|
-
/** Transition seconds — how long the difference to the pose the layer SHOWED
|
|
26
|
-
*
|
|
22
|
+
/** Transition seconds — how long the difference to the pose the layer SHOWED (its loop, a one-shot,
|
|
23
|
+
* or rest) takes to decay. Default `0` = cut: a fade is always asked for, never implied. Also the
|
|
24
|
+
* transition back to the loop at the end unless `fadeOut` overrides it. */
|
|
27
25
|
fade?: number
|
|
28
26
|
/** Transition seconds for the way in only (overrides `fade`). */
|
|
29
27
|
fadeIn?: number
|
|
30
|
-
/** One-shots: the transition BACK at the end (overrides `fade`
|
|
31
|
-
* clip ends — while it still plays — so the hand-over lands on the
|
|
32
|
-
* layer with no loop
|
|
33
|
-
*
|
|
28
|
+
/** One-shots: the transition BACK to the loop at the end (overrides `fade`; `0` = cut at the end).
|
|
29
|
+
* It starts this long before the clip ends — while it still plays — so the hand-over lands on the
|
|
30
|
+
* last pose, not after it. On a layer with no loop the clip HOLDS its last frame (a rock that broke
|
|
31
|
+
* stays broken) — `anim.stop({ fade })` is the way back to rest. */
|
|
34
32
|
fadeOut?: number
|
|
35
33
|
/** Playback rate for this clip (default 1). */
|
|
36
34
|
speed?: number
|
|
@@ -52,8 +50,8 @@ export type PlayOptions = {
|
|
|
52
50
|
export type StopOptions = { fade?: number }
|
|
53
51
|
|
|
54
52
|
export type LoopOptions = {
|
|
55
|
-
/** Transition seconds from whatever the layer shows — its previous loop,
|
|
56
|
-
*
|
|
53
|
+
/** Transition seconds from whatever the layer shows — its previous loop, a one-shot still playing,
|
|
54
|
+
* or rest. Default `0` = cut: a loop wanting a transition asks for one (`{ fade: 0.2 }`). */
|
|
57
55
|
fade?: number
|
|
58
56
|
/** Playback rate of the loop's clips (default 1). */
|
|
59
57
|
speed?: number
|
|
@@ -145,6 +143,12 @@ export type ClipInfo = {
|
|
|
145
143
|
turnAt(time: number): number
|
|
146
144
|
/** the root's own speed at `time`, m/s — how fast the clip is moving the body right there */
|
|
147
145
|
speedAt(time: number): number
|
|
146
|
+
/** the PELVIS' yaw relative to the clip's heading at `time` (rad, + = left, wrapped to ±π): the stance twist
|
|
147
|
+
* the heading leaves out — an idle stands ~43° off the course it starts along, a stop's tail turns into that
|
|
148
|
+
* stance while the heading holds. A controller that switches clips mid-pose can keep the PELVIS continuous
|
|
149
|
+
* (turn the node by the difference between the two clips' values) where keeping the course would swing
|
|
150
|
+
* the body. Without a baked heading: the pelvis' yaw at frame 0 throughout. */
|
|
151
|
+
pelvisYawAt(time: number): number
|
|
148
152
|
/** the unit travel direction at `time` ([x, z], model space, held through stills) — integrate
|
|
149
153
|
* direction × d(travel) to reconstruct the root's 2D path (a treadmill display, a turn's arc) */
|
|
150
154
|
directionAt(time: number): [number, number]
|
|
@@ -215,7 +219,7 @@ const ensureEvents = (): void => {
|
|
|
215
219
|
if (eventsInstalled) return
|
|
216
220
|
eventsInstalled = true
|
|
217
221
|
_creator.setOnAnimatorEvent((animatorId, slot, type) => handlers.get(animatorId)?.(slot, type))
|
|
218
|
-
_creator.setOnAnimatorStep((animatorId, side, x, y, z) => stepHandlers.get(animatorId)?.(side, x, y, z))
|
|
222
|
+
_creator.setOnAnimatorStep?.((animatorId, side, x, y, z) => stepHandlers.get(animatorId)?.(side, x, y, z))
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
const settled = (slot: SlotRec): PlaybackRec => {
|
|
@@ -364,8 +368,8 @@ export class Core {
|
|
|
364
368
|
const s = this.slotFor(L, name, clip)
|
|
365
369
|
if (!s) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
|
|
366
370
|
if (s.inBlend) { console.warn(`Animator: '${name}' is part of this layer's loop — use playLoop() to change what it rests on`); return new Playback(settled(s), this) }
|
|
367
|
-
const fadeIn = o.fadeIn ?? o.fade ??
|
|
368
|
-
const fadeOut = o.fadeOut ?? o.fade ??
|
|
371
|
+
const fadeIn = o.fadeIn ?? o.fade ?? 0
|
|
372
|
+
const fadeOut = o.fadeOut ?? o.fade ?? 0
|
|
369
373
|
const phase = this.entryPhase(L, o.phase) // read BEFORE the play: what the layer shows now
|
|
370
374
|
this.settleLayer(L) // the layer had one source: whatever it was, this play takes over from it
|
|
371
375
|
let resolve = (_n: boolean): void => {}
|
|
@@ -399,7 +403,7 @@ export class Core {
|
|
|
399
403
|
// ---- loop ----
|
|
400
404
|
loop(L: LayerRec, def: LoopDef, o: LoopOptions): Loop | undefined {
|
|
401
405
|
if (!this.ensure()) return undefined
|
|
402
|
-
const fade = o.fade ??
|
|
406
|
+
const fade = o.fade ?? 0
|
|
403
407
|
const phase = this.entryPhase(L, o.phase) // read BEFORE the members replace the layer's source
|
|
404
408
|
if (L.loop) { L.loop.dead = true; for (const m of L.loop.members) m.slot.inBlend = false; L.loop = undefined }
|
|
405
409
|
this.settleLayer(L) // a one-shot on the layer is cut short by the new loop
|
|
@@ -576,6 +580,7 @@ export class Core {
|
|
|
576
580
|
travelAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 1, time),
|
|
577
581
|
turnAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 2, time),
|
|
578
582
|
speedAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 3, time),
|
|
583
|
+
pelvisYawAt: (time: number) => { const v = _creator.animatorSlotCurveAt(id, slot, 6, time); return Math.atan2(Math.sin(v), Math.cos(v)) },
|
|
579
584
|
directionAt: (time: number) => [ _creator.animatorSlotCurveAt(id, slot, 4, time), _creator.animatorSlotCurveAt(id, slot, 5, time) ] as [number, number],
|
|
580
585
|
timeAtTurn: (yaw: number) => _creator.animatorSlotTimeAtTurn(id, slot, yaw),
|
|
581
586
|
kneePoleAt: (side: "left" | "right", time: number) => {
|