lecodes-sdk 1.0.0 → 1.2.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 +18 -4
- package/dist/inject.js +260 -361
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +28 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +55 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +12 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +235 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +49 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +23 -12
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +80 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +35 -12
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +33 -2
- package/src/inject.ts +236 -226
- package/src/runtime/input.ts +11 -0
- package/src/scene/defineScene.ts +72 -62
- package/src/scene/gizmos.ts +148 -148
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/src/gl/animation/IK.ts
CHANGED
|
@@ -1,151 +1,173 @@
|
|
|
1
|
-
// IK —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// IK — chains the ENGINE solves in its late pass: after every aspect's update(dt) ran, before the
|
|
2
|
+
// dynamic bones and the skin flush. Attach to the END bone of the chain (the chain is its parent and
|
|
3
|
+
// grandparent); nothing runs in JS per frame — the aspect only describes the chain, the host reads the
|
|
4
|
+
// target nodes itself each frame and creator-anim (anchors.cpp) solves.
|
|
5
5
|
//
|
|
6
6
|
// const foot = hero.bone('LeftFoot')!
|
|
7
7
|
// foot.aspect(IK.TwoBone, { target: footTarget, pole: kneeHint }) // upLeg → leg → foot
|
|
8
8
|
// foot.ik.weight = grounded ? 1 : 0 // blend in/out
|
|
9
9
|
// hero.bone('Head')!.aspect(IK.LookAt, { target: camera, limit: 70 }) // head tracks the camera
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// ANCHORED chains — hands on a weapon's grips (the gun-master rig): the chain rides a SOCKET of the
|
|
12
|
+
// rig (`anim.sockets(...)`: named empties on a bone, a weapon's grip / magazine / bolt), keeping the
|
|
13
|
+
// animated hand's pose RELATIVE to that socket on the gun the take was authored on and replaying it
|
|
14
|
+
// relative to the socket on the gun in the hands — shifted and turned by the sockets' difference,
|
|
15
|
+
// never re-authored. A clip's anchor spans (`clip.anchors(...)`) move the hand between sockets while
|
|
16
|
+
// it plays (grip → magazine → free → bolt → grip through a reload):
|
|
17
|
+
//
|
|
18
|
+
// arms.anim.sockets('ik_hand_gun', { grip_r: weapon.gripR, grip_l: weapon.gripL, mag: weapon.mag })
|
|
19
|
+
// arms.anim.sockets('ik_hand_gun', { grip_r: {...}, grip_l: {...}, mag: {...} }, { set: 'tr15' }) // the donor's
|
|
20
|
+
// arms.bone('hand_l')!.aspect(IK.TwoBone, { anchor: 'grip_l' })
|
|
21
|
+
// arms.anim.clip('R_ReloadEmpty')!.anchors('hand_l', { donor: 'tr15', spans: [[0.9, 1.4, 'mag'], [1.4, 2.0, ''], [2.0, 2.4, 'mag']] })
|
|
22
|
+
//
|
|
23
|
+
// Since the engine solves after the late phase, a target the game moves in its own update(dt) — the
|
|
24
|
+
// gun bone placed for aiming and recoil — is reached the SAME frame.
|
|
13
25
|
|
|
14
26
|
import { Aspect } from "../../core/Aspect"
|
|
15
27
|
import { Vec3, type Vec3Like } from "../../math/vec"
|
|
16
28
|
import { Quat } from "../../math/quat"
|
|
17
29
|
import type { Node } from "../Node"
|
|
30
|
+
import { Animator } from "./Animator"
|
|
31
|
+
import type { Core } from "./core"
|
|
18
32
|
|
|
19
33
|
type Target = Node | Vec3Like
|
|
20
34
|
|
|
21
|
-
const
|
|
22
|
-
if (t === undefined || t === null) return null
|
|
23
|
-
const n = t as Node
|
|
24
|
-
if (typeof (n as { worldPosition?: unknown }).worldPosition === "object" && typeof n.id === "number") return n.worldPosition
|
|
25
|
-
return new Vec3(t as Vec3Like)
|
|
26
|
-
}
|
|
35
|
+
const isNode = (t: unknown): t is Node => typeof t === "object" && t !== null && typeof (t as Node).id === "number" && typeof (t as { worldMatrix?: unknown }).worldMatrix === "object"
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
const
|
|
37
|
+
// CANIM_IK_* (creator-anim.h): the fixed-order parameter array of a chain.
|
|
38
|
+
const P = { TARGET: 0, POLE: 3, HAS_POLE: 6, WEIGHT: 7, ROT: 8, ROT_WEIGHT: 12, AXIS: 13, LIMIT: 16, ENABLED: 17, ANCHOR_TIME: 18, ANCHOR_PIN: 19, COUNT: 20 }
|
|
39
|
+
const stateBuf = new Float32Array(4)
|
|
30
40
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
/** The rig a bone belongs to: the nearest ancestor carrying an Animator (every Model has one); a bare
|
|
42
|
+
* node tree gets one on its topmost node. */
|
|
43
|
+
const rigOf = (bone: Node): Core => {
|
|
44
|
+
let top: Node = bone
|
|
45
|
+
for (let n: Node | null = bone; n; n = n.parent) {
|
|
46
|
+
const anim = n.get(Animator)
|
|
47
|
+
if (anim) return anim._core
|
|
48
|
+
top = n
|
|
49
|
+
}
|
|
50
|
+
top.aspect(Animator)
|
|
51
|
+
return top.get(Animator)!._core
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return Quat.fromAxisAngle(axis, q.w < 0 ? -maxRad : maxRad)
|
|
45
|
-
}
|
|
54
|
+
/** What both chains share: the native handle, the parameter push, the readback. */
|
|
55
|
+
abstract class Chain<Name extends string> extends Aspect<Name, Node> {
|
|
56
|
+
protected _rig?: Core
|
|
57
|
+
protected _ik = 0
|
|
58
|
+
protected _weight = 1
|
|
59
|
+
protected _enabled = true
|
|
60
|
+
protected abstract readonly kind: 0 | 1
|
|
46
61
|
|
|
47
|
-
/** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })`
|
|
48
|
-
* solves upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend
|
|
49
|
-
* (knee/elbow) — a Node or world point. */
|
|
50
|
-
class TwoBone extends Aspect<"ik", Node> {
|
|
51
|
-
static readonly aspect = "ik"
|
|
52
|
-
|
|
53
|
-
/** Where the end bone should be (Node or world position). */
|
|
54
|
-
target?: Target
|
|
55
|
-
/** Bend hint — the mid joint is pulled toward it (Node or world position). */
|
|
56
|
-
pole?: Target
|
|
57
|
-
/** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat.
|
|
58
|
-
* Unset = the end bone keeps its animated local rotation (a hand on a grip, a foot on a slope
|
|
59
|
-
* want it set). Blended by `weight · rotationWeight`. */
|
|
60
|
-
rotation?: Node | Quat
|
|
61
|
-
/** 0–1 contribution of `rotation` (on top of `weight`). */
|
|
62
|
-
rotationWeight = 1
|
|
63
62
|
/** 0–1 contribution (blend in/out, e.g. foot planting only while grounded). */
|
|
64
|
-
weight
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
get weight(): number { return this._weight }
|
|
64
|
+
set weight(v: number) { this._weight = v; this.push() }
|
|
65
|
+
/** Solve every frame (default). */
|
|
66
|
+
get enabled(): boolean { return this._enabled }
|
|
67
|
+
set enabled(v: boolean) { this._enabled = v; this.push() }
|
|
68
|
+
/** After the last solve: how far the end bone still is from its target, metres (0 = reached). */
|
|
69
|
+
get error(): number { return this._rig && this._ik && this._rig.ikState(this._ik, stateBuf) ? stateBuf[0]! : 0 }
|
|
70
|
+
|
|
71
|
+
onAttach(): void {
|
|
72
|
+
this._rig = rigOf(this.node)
|
|
73
|
+
this._ik = this._rig.ikCreate(this.kind, this.node.id)
|
|
74
|
+
if (!this._ik) console.warn(`IK: ${this.node.name || "the bone"} is no joint of its rig (or this host has no IK)`)
|
|
75
|
+
this.push()
|
|
70
76
|
}
|
|
77
|
+
onDetach(): void {
|
|
78
|
+
if (this._rig && this._ik) this._rig.ikDestroy(this._ik)
|
|
79
|
+
this._ik = 0
|
|
80
|
+
this._rig = undefined
|
|
81
|
+
}
|
|
82
|
+
onReconfigure(): void { this.push() }
|
|
83
|
+
|
|
84
|
+
protected params(): Float32Array {
|
|
85
|
+
const p = new Float32Array(P.COUNT)
|
|
86
|
+
p[P.WEIGHT] = this._weight
|
|
87
|
+
p[P.ENABLED] = this._enabled ? 1 : 0
|
|
88
|
+
p[P.LIMIT] = 80
|
|
89
|
+
p[P.AXIS + 2] = -1
|
|
90
|
+
p[P.ROT + 3] = 1
|
|
91
|
+
p[P.ANCHOR_TIME] = 0.05
|
|
92
|
+
return p
|
|
93
|
+
}
|
|
94
|
+
/** Push the description to the engine (a no-op until attached). */
|
|
95
|
+
protected abstract push(): void
|
|
96
|
+
}
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const b2 = mid.worldPosition
|
|
124
|
-
const midProj = b2.sub(a).sub(dir.scale(b2.sub(a).dot(dir)))
|
|
125
|
-
const poleProj = p.sub(a).sub(dir.scale(p.sub(a).dot(dir)))
|
|
126
|
-
if (midProj.lengthSq() > 1e-10 && poleProj.lengthSq() > 1e-10) {
|
|
127
|
-
setWorldRot(root, Quat.fromTo(midProj, poleProj).mul(worldRot(root)))
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 4. weight: blend from the animated pose to the solved one
|
|
132
|
-
if (w < 1) {
|
|
133
|
-
root.quaternion = rootLocal0.slerp(root.quaternion, w)
|
|
134
|
-
mid.quaternion = midLocal0.slerp(mid.quaternion, w)
|
|
135
|
-
}
|
|
98
|
+
/** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })` solves
|
|
99
|
+
* upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend (knee /
|
|
100
|
+
* elbow) — a Node or world point; `anchor` makes it ride a socket of the rig instead (see the file
|
|
101
|
+
* header). */
|
|
102
|
+
class TwoBone extends Chain<"ik"> {
|
|
103
|
+
static readonly aspect = "ik"
|
|
104
|
+
protected readonly kind = 0 as const
|
|
105
|
+
private _target?: Target
|
|
106
|
+
private _pole?: Target
|
|
107
|
+
private _rotation?: Node | Quat
|
|
108
|
+
private _rotationWeight = 1
|
|
109
|
+
private _anchor = ""
|
|
110
|
+
private _anchorTime = 0.05
|
|
111
|
+
private _anchorPin = 0
|
|
112
|
+
|
|
113
|
+
/** Where the end bone should be (a Node — read by the engine every frame — or a world position). */
|
|
114
|
+
get target(): Target | undefined { return this._target }
|
|
115
|
+
set target(v: Target | undefined) { this._target = v; this.push() }
|
|
116
|
+
/** Bend hint — the mid joint is pulled toward it (a Node or a world point). */
|
|
117
|
+
get pole(): Target | undefined { return this._pole }
|
|
118
|
+
set pole(v: Target | undefined) { this._pole = v; this.push() }
|
|
119
|
+
/** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat. Unset =
|
|
120
|
+
* the end bone keeps its animated rotation (a hand on a grip, a foot on a slope want it set). Blended
|
|
121
|
+
* by `weight · rotationWeight`. An anchored chain takes its rotation from the socket instead. */
|
|
122
|
+
get rotation(): Node | Quat | undefined { return this._rotation }
|
|
123
|
+
set rotation(v: Node | Quat | undefined) { this._rotation = v; this.push() }
|
|
124
|
+
/** 0–1 contribution of `rotation` (on top of `weight`); for an anchored chain, of the socket's turn. */
|
|
125
|
+
get rotationWeight(): number { return this._rotationWeight }
|
|
126
|
+
set rotationWeight(v: number) { this._rotationWeight = v; this.push() }
|
|
127
|
+
/** ANCHORED: the name of the LIVE socket the end bone rides (`anim.sockets(...)`) — its target and
|
|
128
|
+
* rotation come from the socket and the playing clips' anchor spans; `target` / `pole` / `rotation`
|
|
129
|
+
* are ignored. '' = a plain chain. */
|
|
130
|
+
get anchor(): string { return this._anchor }
|
|
131
|
+
set anchor(v: string) { this._anchor = v ?? ""; this.push() }
|
|
132
|
+
/** Anchored: how fast the hand moves between sockets — the halflife (s) of the spring the applied
|
|
133
|
+
* delta follows the wanted one with (default 0.05). */
|
|
134
|
+
get anchorTime(): number { return this._anchorTime }
|
|
135
|
+
set anchorTime(v: number) { this._anchorTime = v; this.push() }
|
|
136
|
+
/** Anchored: 0..1 how hard the end bone is PINNED on its socket while the playing clips leave it there
|
|
137
|
+
* (a GAP in their spans — the hand is holding the gun). 1 = it sits exactly on the socket, so nothing
|
|
138
|
+
* in the pose below can slide it along the gun; 0 (default) = it keeps the take's own relation to the
|
|
139
|
+
* socket, moved by the delta. A clip's SPANS carry their own pin and default to 0, because a hand on
|
|
140
|
+
* its way to a magazine must keep the take's motion — so this knob is about holding, not reaching. */
|
|
141
|
+
get anchorPin(): number { return this._anchorPin }
|
|
142
|
+
set anchorPin(v: number) { this._anchorPin = v; this.push() }
|
|
143
|
+
/** After the last solve: the anchor delta applied — how far (m) and how much (rad) the live socket
|
|
144
|
+
* moved the hand off the take's own pose. 0 for a plain chain. */
|
|
145
|
+
get anchorDelta(): { distance: number, angle: number } {
|
|
146
|
+
if (!this._rig || !this._ik || !this._rig.ikState(this._ik, stateBuf)) return { distance: 0, angle: 0 }
|
|
147
|
+
return { distance: stateBuf[1]!, angle: stateBuf[2]! }
|
|
148
|
+
}
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
protected push(): void {
|
|
151
|
+
const rig = this._rig
|
|
152
|
+
if (!rig || !this._ik) return
|
|
153
|
+
const p = this.params()
|
|
154
|
+
p[P.ANCHOR_TIME] = this._anchorTime
|
|
155
|
+
p[P.ANCHOR_PIN] = this._anchorPin
|
|
156
|
+
const anchored = this._anchor !== ""
|
|
157
|
+
p[P.ROT_WEIGHT] = anchored || this._rotation !== undefined ? this._rotationWeight : 0
|
|
158
|
+
let targetNode = 0, poleNode = 0, rotNode = 0
|
|
159
|
+
if (!anchored) {
|
|
160
|
+
const t = this._target, pl = this._pole, r = this._rotation
|
|
161
|
+
if (isNode(t)) targetNode = t.id
|
|
162
|
+
else if (t !== undefined) { const v = new Vec3(t); p[P.TARGET] = v.x; p[P.TARGET + 1] = v.y; p[P.TARGET + 2] = v.z }
|
|
163
|
+
if (isNode(pl)) poleNode = pl.id
|
|
164
|
+
else if (pl !== undefined) { const v = new Vec3(pl); p[P.POLE] = v.x; p[P.POLE + 1] = v.y; p[P.POLE + 2] = v.z; p[P.HAS_POLE] = 1 }
|
|
165
|
+
if (isNode(r)) rotNode = r.id
|
|
166
|
+
else if (r !== undefined) { p[P.ROT] = r.x; p[P.ROT + 1] = r.y; p[P.ROT + 2] = r.z; p[P.ROT + 3] = r.w }
|
|
148
167
|
}
|
|
168
|
+
rig.ikSet(this._ik, p)
|
|
169
|
+
rig.ikNodes(this._ik, targetNode, poleNode, rotNode)
|
|
170
|
+
rig.ikAnchor(this._ik, this._anchor)
|
|
149
171
|
}
|
|
150
172
|
}
|
|
151
173
|
|
|
@@ -153,37 +175,36 @@ class TwoBone extends Aspect<"ik", Node> {
|
|
|
153
175
|
* `head.aspect(IK.LookAt, { target: camera, limit: 70 })`. `axis` is the bone's LOCAL forward
|
|
154
176
|
* (the direction that should point at the target) — rigs differ; default −Z, Mixamo heads look
|
|
155
177
|
* along +Z of the head bone in most exports, so pass `axis: [0, 0, 1]` there if it faces backwards. */
|
|
156
|
-
class LookAt extends
|
|
178
|
+
class LookAt extends Chain<"lookAt"> {
|
|
157
179
|
static readonly aspect = "lookAt"
|
|
158
|
-
|
|
159
|
-
|
|
180
|
+
protected readonly kind = 1 as const
|
|
181
|
+
private _target?: Target
|
|
182
|
+
private _axis: Vec3Like = [ 0, 0, -1 ]
|
|
183
|
+
private _limit = 80
|
|
184
|
+
|
|
185
|
+
/** What to look at (a Node — read by the engine every frame — or a world position). */
|
|
186
|
+
get target(): Target | undefined { return this._target }
|
|
187
|
+
set target(v: Target | undefined) { this._target = v; this.push() }
|
|
160
188
|
/** The bone's local axis that should point at the target. */
|
|
161
|
-
axis: Vec3Like
|
|
189
|
+
get axis(): Vec3Like { return this._axis }
|
|
190
|
+
set axis(v: Vec3Like) { this._axis = v; this.push() }
|
|
162
191
|
/** Max deflection from the animated direction, in degrees (default 80). */
|
|
163
|
-
limit
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const dir = t.sub(from)
|
|
180
|
-
if (dir.lengthSq() < 1e-10) return
|
|
181
|
-
const rot = worldRot(bone)
|
|
182
|
-
const current = new Vec3(this.axis).rotate(rot)
|
|
183
|
-
let delta = Quat.fromTo(current, dir)
|
|
184
|
-
delta = clampAngle(delta, (this.limit * Math.PI) / 180)
|
|
185
|
-
setWorldRot(bone, delta.mul(rot))
|
|
186
|
-
if (w < 1) bone.quaternion = local0.slerp(bone.quaternion, w)
|
|
192
|
+
get limit(): number { return this._limit }
|
|
193
|
+
set limit(v: number) { this._limit = v; this.push() }
|
|
194
|
+
|
|
195
|
+
protected push(): void {
|
|
196
|
+
const rig = this._rig
|
|
197
|
+
if (!rig || !this._ik) return
|
|
198
|
+
const p = this.params()
|
|
199
|
+
const a = new Vec3(this._axis)
|
|
200
|
+
p[P.AXIS] = a.x; p[P.AXIS + 1] = a.y; p[P.AXIS + 2] = a.z
|
|
201
|
+
p[P.LIMIT] = this._limit
|
|
202
|
+
let targetNode = 0
|
|
203
|
+
const t = this._target
|
|
204
|
+
if (isNode(t)) targetNode = t.id
|
|
205
|
+
else if (t !== undefined) { const v = new Vec3(t); p[P.TARGET] = v.x; p[P.TARGET + 1] = v.y; p[P.TARGET + 2] = v.z }
|
|
206
|
+
rig.ikSet(this._ik, p)
|
|
207
|
+
rig.ikNodes(this._ik, targetNode, 0, 0)
|
|
187
208
|
}
|
|
188
209
|
}
|
|
189
210
|
|
|
@@ -37,8 +37,9 @@ 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"
|
|
40
|
-
/** What the engine shows: the idle, a start, the gait loop, a turn while moving, a stop,
|
|
41
|
-
|
|
40
|
+
/** What the engine shows: the idle, a start, the gait loop, a turn while moving, a stop, a turn on the spot —
|
|
41
|
+
* an ACTION (`act()`): a one-shot that owns the body until it hands over — or the STRAFE blend (`move({ strafe })`). */
|
|
42
|
+
export type LocomotionState = "idle" | "start" | "move" | "turn" | "stop" | "spin" | "action" | "strafe"
|
|
42
43
|
/** Who moves the body:
|
|
43
44
|
* `'hybrid'` (the default) — the clip's own recorded motion moves it; while the gait loop shows it
|
|
44
45
|
* is ADJUSTED toward what the simulation wants by at most `tuning.adjust` metres a second, and a
|
|
@@ -70,6 +71,10 @@ export type LocomotionSet = {
|
|
|
70
71
|
turns?: LocomotionClip[]
|
|
71
72
|
/** turns on the spot — what a facing still owed by a standing body plays */
|
|
72
73
|
spins?: LocomotionClip[]
|
|
74
|
+
/** the STRAFE set: the same gait recorded forward, backward, to either side and on the diagonals — the members of
|
|
75
|
+
* the directional blend `move(dir, { face, strafe: true })` shows. `angle` = the way a take travels in the body's
|
|
76
|
+
* frame (degrees, + = left); left out, the engine reads it off the take's root. */
|
|
77
|
+
strafes?: LocomotionClip[]
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/** The tuning. Times are seconds, speeds m/s, angles degrees. */
|
|
@@ -201,7 +206,7 @@ export type StopCandidate = {
|
|
|
201
206
|
|
|
202
207
|
const STOP_ROW = 9
|
|
203
208
|
const STOP_ROWS = 8
|
|
204
|
-
const IN = { DIR_X: 0, DIR_Z: 1, MAG: 2, FACE_X: 3, FACE_Z: 4, GAIT: 5,
|
|
209
|
+
const IN = { DIR_X: 0, DIR_Z: 1, MAG: 2, FACE_X: 3, FACE_Z: 4, GAIT: 5, ACT_SEQ: 6, ACT_SLOT: 7, ACT_DIR_X: 8, ACT_DIR_Z: 9, ACT_ANGLE: 10, ACT_TURN: 11, ACT_AT: 12, ACT_EXIT: 13, ACT_FADE: 14, ACT_RATE: 15, ACT_OUT: 16, ACT_TRAVEL: 17, ACT_TRAVEL_AT: 18, ACT_TRAVEL_AFTER: 19, STRAFE: 20, COUNT: 21 }
|
|
205
210
|
const OUT = { STATE: 0, SPEED: 1, VEL_X: 2, VEL_Y: 3, VEL_Z: 4, YAW: 5, YAW_RATE: 6, PHASE: 7, SEQ: 8, PRED: 9, YAW_LEFT: 21, CLIP_TIME: 22, EXIT_AT: 23, STOP_OFF: 24, STOP_STEPS: 25, STOP_WAIT: 26, COUNT: 27 }
|
|
206
211
|
const P = {
|
|
207
212
|
HALFLIFE_WALK: 0, HALFLIFE_RUN: 1, HALFLIFE_FACING: 2, SPEED_WALK: 3, SPEED_RUN: 4, SPEED_SPRINT: 5,
|
|
@@ -210,8 +215,8 @@ const P = {
|
|
|
210
215
|
HALFLIFE_STOP: 20, ADJUST: 21, YAW_WARP: 22, EXIT_TOL: 23, RESUME_SPEED: 24, EXIT_STEPS: 25, STOP_EARLY: 26, STOP_STEPS: 27, STOP_FIT: 28, STOP_POSE: 29,
|
|
211
216
|
STRIDE_SHARE: 30, STRIDE_MIN: 31, STRIDE_MAX: 32, WARP_STRIDE: 33, WARP_RATE: 34, COUNT: 35,
|
|
212
217
|
}
|
|
213
|
-
const KIND = { IDLE: 0, GAIT: 1, START: 2, STOP: 3, TURN: 4, MATCH: 5, SPIN: 6 }
|
|
214
|
-
const STATES: LocomotionState[] = [ "idle", "start", "move", "turn", "stop", "spin" ]
|
|
218
|
+
const KIND = { IDLE: 0, GAIT: 1, START: 2, STOP: 3, TURN: 4, MATCH: 5, SPIN: 6, STRAFE: 8 }
|
|
219
|
+
const STATES: LocomotionState[] = [ "idle", "start", "move", "turn", "stop", "spin", "action", "strafe" ]
|
|
215
220
|
const GAITS: Gait[] = [ "walk", "run", "sprint" ]
|
|
216
221
|
const RAD2DEG = 180 / Math.PI
|
|
217
222
|
|
|
@@ -249,6 +254,8 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
249
254
|
private _mag = 0
|
|
250
255
|
private _gait: Gait = "walk"
|
|
251
256
|
private _seq = -1
|
|
257
|
+
private _act = 0
|
|
258
|
+
private _strafe = false
|
|
252
259
|
private readonly _in = new Float32Array(IN.COUNT)
|
|
253
260
|
private readonly _out = new Float32Array(OUT.COUNT)
|
|
254
261
|
private readonly _names = new Map<number, string>()
|
|
@@ -290,6 +297,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
290
297
|
for (const c of this.set.stops ?? []) this.enter(c, KIND.STOP)
|
|
291
298
|
for (const c of this.set.turns ?? []) this.enter(c, KIND.TURN)
|
|
292
299
|
for (const c of this.set.spins ?? []) this.enter(c, KIND.SPIN)
|
|
300
|
+
for (const c of this.set.strafes ?? []) this.enter(c, KIND.STRAFE)
|
|
293
301
|
if (this.warp !== undefined) anim.warp.set(this.warp)
|
|
294
302
|
anim.feet.set(this.feet === undefined ? { lock: true } : this.feet === true ? { lock: true, ik: true } : this.feet === false ? { lock: false, ik: false } : this.feet)
|
|
295
303
|
this.pushTuning()
|
|
@@ -306,7 +314,8 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
306
314
|
this._names.set(slot, n)
|
|
307
315
|
const o = typeof clip === "string" ? undefined : clip
|
|
308
316
|
const measured = anim.clipInfo(n)
|
|
309
|
-
|
|
317
|
+
// (a strafe member's angle is the way it TRAVELS, which the engine reads off its root: 1000 = measure)
|
|
318
|
+
const angle = o?.angle ?? (kind === KIND.STRAFE ? 1000 : measured ? measured.turn * RAD2DEG : 0)
|
|
310
319
|
const gait = kind === KIND.GAIT ? -1 : o?.gait ? GAITS.indexOf(o.gait) : -1
|
|
311
320
|
_creator.locoSetEntry(this._loco, slot, kind, angle, o?.speed ?? 0, gait)
|
|
312
321
|
}
|
|
@@ -379,8 +388,11 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
379
388
|
/** State the movement intent for this frame: a world direction (its length is the stick's pull,
|
|
380
389
|
* clamped to 1) and the gait. `null` = no movement — the body stops, but it keeps facing where it
|
|
381
390
|
* was last asked to, and turns there on the spot if it still owes the turn.
|
|
382
|
-
* `face` states a facing of its own (aiming,
|
|
383
|
-
|
|
391
|
+
* `face` states a facing of its own (aiming, a camera-relative shooter) — and with `strafe: true` the body MOVES
|
|
392
|
+
* where the direction points while it faces there: the loop becomes the directional blend of `set.strafes` (a
|
|
393
|
+
* guard, a lock-on), the facing is steered standing too, state `'strafe'`. Stated every frame like the rest. */
|
|
394
|
+
move(direction: Vec3Like | null | undefined, options: { gait?: Gait, face?: Vec3Like, strafe?: boolean } = {}): void {
|
|
395
|
+
this._strafe = !!options.strafe
|
|
384
396
|
const dx = direction ? cx(direction) : 0, dz = direction ? cz(direction) : 0
|
|
385
397
|
const len = Math.hypot(dx, dz)
|
|
386
398
|
if (len > 1e-4) {
|
|
@@ -395,6 +407,57 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
395
407
|
if (options.gait) this._gait = options.gait
|
|
396
408
|
}
|
|
397
409
|
|
|
410
|
+
/** Play an ACTION: a one-shot that OWNS the body the way a start or a stop does — a roll, a backstep, a lunging
|
|
411
|
+
* attack. Its recording moves the body alone (in every displacement mode but `'data'`, where the host does),
|
|
412
|
+
* the steering and the selector stand by, and when it hands over the usual rules carry on: a direction held
|
|
413
|
+
* starts (or, still carrying speed, runs on), nothing held stands.
|
|
414
|
+
*
|
|
415
|
+
* `direction` is where its TRAVEL must go, world (`Vec3`, or a heading in degrees, 0 = +Z, + = toward +X). A
|
|
416
|
+
* clip travels some way relative to the body — forward, left, back; measured off its own root, or `angle`
|
|
417
|
+
* (degrees, + = left) — and the body is turned by what is left between that and `direction`, over `turn`
|
|
418
|
+
* seconds (default 0.12), so the travel lands exactly where it was pointed. Which clip to play is yours to
|
|
419
|
+
* pick: the one whose travel is nearest the direction IN THE BODY'S FRAME (`direction − facing`) leaves the
|
|
420
|
+
* least to turn. Left out = it plays where the body faces.
|
|
421
|
+
*
|
|
422
|
+
* `at` enters the clip that many seconds in (a wind-up skipped), `rate` plays it faster or slower (its metres
|
|
423
|
+
* stay, the time changes), `travel` takes only that share of its recorded travel (an attack aimed at a target:
|
|
424
|
+
* 0.3 to stop at the blade's length from one that is close, 1.3 to reach one a step too far — the pose is
|
|
425
|
+
* untouched, so keep it to a fast step; `{ share, at, after }` takes another share from clip time `at` on: a
|
|
426
|
+
* swing's take travels on after its cut, into a body that stands at the blade's length), `fade` is the transition into it and `out` the one out of it (default: the tuning's
|
|
427
|
+
* `blend`; an action ends in ITS stance, not the idle's, and a longer way out is what hides that), and `exit` is the
|
|
428
|
+
* clip time from which a MOVE intent may take over — the cancel window: along the way the body faces at once, a
|
|
429
|
+
* heading that needs a turn as soon as the body has slowed to where a start can answer; with nothing asked for it plays out, to
|
|
430
|
+
* its end (an action ends in its own recovery, at rest), and the body then STANDS THE WAY THE ACTION LEFT IT:
|
|
431
|
+
* the facing intent that outlives a released key is not a demand after an action — no turn on the spot.
|
|
432
|
+
* WHEN an action may be asked for (out of another one, out of a start) is the caller's rule: this cuts into
|
|
433
|
+
* whatever shows. `state` reads `'action'` while it does. False = no such clip. */
|
|
434
|
+
act(clip: string, options: { direction?: Vec3Like | number, angle?: number, turn?: number, at?: number, exit?: number, fade?: number, out?: number, rate?: number, travel?: number | { share: number, at: number, after: number } } = {}): boolean {
|
|
435
|
+
const anim = this.anim
|
|
436
|
+
if (!anim || !this._loco) return false
|
|
437
|
+
const slot = anim._slot(clip)
|
|
438
|
+
if (slot < 0) { console.warn(`Locomotion: no clip '${clip}' on ${this.node.name || "the character"}`); return false }
|
|
439
|
+
const i = this._in
|
|
440
|
+
let dx = 0, dz = 0
|
|
441
|
+
const d = options.direction
|
|
442
|
+
if (typeof d === "number") { dx = Math.sin(d / RAD2DEG); dz = Math.cos(d / RAD2DEG) }
|
|
443
|
+
else if (d) { dx = cx(d); dz = cz(d) }
|
|
444
|
+
i[IN.ACT_SEQ] = ++this._act
|
|
445
|
+
i[IN.ACT_SLOT] = slot
|
|
446
|
+
i[IN.ACT_DIR_X] = dx; i[IN.ACT_DIR_Z] = dz
|
|
447
|
+
i[IN.ACT_ANGLE] = options.angle ?? 1000 // 1000 = measure the clip's own travel
|
|
448
|
+
i[IN.ACT_TURN] = options.turn ?? 0.12
|
|
449
|
+
i[IN.ACT_AT] = options.at ?? 0
|
|
450
|
+
i[IN.ACT_EXIT] = options.exit ?? 0
|
|
451
|
+
i[IN.ACT_FADE] = options.fade ?? 0
|
|
452
|
+
i[IN.ACT_RATE] = options.rate ?? 0
|
|
453
|
+
i[IN.ACT_OUT] = options.out ?? 0
|
|
454
|
+
const tr = options.travel
|
|
455
|
+
i[IN.ACT_TRAVEL] = typeof tr === "number" ? tr : tr?.share ?? 0
|
|
456
|
+
i[IN.ACT_TRAVEL_AT] = typeof tr === "object" ? tr.at : 0
|
|
457
|
+
i[IN.ACT_TRAVEL_AFTER] = typeof tr === "object" ? tr.after : 0
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
|
|
398
461
|
/** Face this way without moving — the facing intent on its own (`Vec3`, or a heading in degrees,
|
|
399
462
|
* 0 = +Z, + = toward +X). A standing body turns to it on the spot. */
|
|
400
463
|
face(direction: Vec3Like | number): void {
|
|
@@ -477,6 +540,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
477
540
|
i[IN.DIR_X] = this._dir.x; i[IN.DIR_Z] = this._dir.z; i[IN.MAG] = this._mag
|
|
478
541
|
i[IN.FACE_X] = this._face.x; i[IN.FACE_Z] = this._face.z
|
|
479
542
|
i[IN.GAIT] = Math.max(0, GAITS.indexOf(this._gait))
|
|
543
|
+
i[IN.STRAFE] = this._strafe ? 1 : 0
|
|
480
544
|
_creator.locoSetInput(this._loco, i)
|
|
481
545
|
_creator.locoUpdate(this._loco, dt)
|
|
482
546
|
_creator.locoRead(this._loco, this._out)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Playback — the handle `play()` returns (docs/animator-plan.md). Thenable: `await anim.play('slash')`
|
|
2
|
-
// resolves at the clip's HAND-OVER —
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// resolves at the clip's HAND-OVER — its end (the `end` of its window) — with `true`; `false` when it
|
|
3
|
+
// was cut short (replaced / stopped). What you call right after the await is what its last pose
|
|
4
|
+
// transitions into: `anim.play(next)` (a chained one-shot), `anim.playLoop(next)` — nothing = back to
|
|
5
|
+
// the loop over `fadeOut` (or hold, on a layer without one). A cancel window is game logic (a clip
|
|
6
|
+
// event, `progress`, `busy`), not a fade: play the next clip whenever the game says so.
|
|
6
7
|
// Returning one from an `async` function flattens it into `Promise<boolean>` — return `p.done` (or
|
|
7
8
|
// make the function sync) when the caller needs the handle.
|
|
8
9
|
|
package/src/gl/animation/Warp.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// before the feet (docs/animation-v2-plan.md §2.7). `stride` scales each leg's step to the speed the body
|
|
3
3
|
// really travels at (a walk played at 1.9 m/s stops skating); `orientation` turns the lower body toward
|
|
4
4
|
// where the body really goes while the spine counter-turns (arcs and strafing without a clip per angle).
|
|
5
|
-
// Both need the body's motion
|
|
5
|
+
// Both need the body's motion — a `Locomotion` feeds it every frame, or `anim.motion = velocity` when the
|
|
6
|
+
// gait is driven by hand; without either leave it off — and the CLIP's motion: its root motion, or for an
|
|
7
|
+
// in-place loop the pace its pack declared (`lecodes.velocity` in the GLB animation's extras). Off by
|
|
6
8
|
// default. `step` is the STEP WARP: stride / lift / pitch / slope dials solved in the knee hinge plane.
|
|
7
9
|
//
|
|
8
10
|
// model.anim.warp.set(true) // both, with the defaults
|
|
@@ -16,7 +18,8 @@ import type { Core } from "./core"
|
|
|
16
18
|
export type WarpOptions = {
|
|
17
19
|
/** Fit the stride to the speed the body actually travels at. `[min, max]` clamps the scale (default
|
|
18
20
|
* 0.85…1.2). A CORRECTION: a pack whose takes already read right at the speeds it is played at wants
|
|
19
|
-
* none of this; open the range for a pack that must cover speeds it was never recorded at.
|
|
21
|
+
* none of this; open the range for a pack that must cover speeds it was never recorded at. The clip's
|
|
22
|
+
* own speed is its root motion — an in-place loop takes part only with a declared pace (see above). */
|
|
20
23
|
stride?: boolean | [number, number]
|
|
21
24
|
/** Turn the lower body toward where the body really travels; a number caps the turn in degrees
|
|
22
25
|
* (default 20). The whole twist lives in one joint: a few degrees read as a lean, a lot as a broken
|