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/Particles.ts
CHANGED
|
@@ -40,6 +40,11 @@ const TAG_ANGULAR_VELOCITY = 17
|
|
|
40
40
|
const TAG_GROUND = 18
|
|
41
41
|
const TAG_VELOCITY_LIFE = 19
|
|
42
42
|
const TAG_ORDER = 20
|
|
43
|
+
const TAG_SMOOTH_PATH = 21
|
|
44
|
+
const TAG_RIBBON_ORIENT = 22
|
|
45
|
+
const TAG_LIT = 23
|
|
46
|
+
/** `TrailOptions.orient` → the native axis code (0 = the camera-facing default). */
|
|
47
|
+
const RIBBON_AXIS: Record<"camera" | "x" | "y" | "z", number> = { camera: 0, x: 1, y: 2, z: 3 }
|
|
43
48
|
|
|
44
49
|
/** @internal — exported for the tag-sync test only. */
|
|
45
50
|
export const _particleTags = {
|
|
@@ -49,7 +54,8 @@ export const _particleTags = {
|
|
|
49
54
|
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
50
55
|
RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
|
|
51
56
|
ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
|
|
52
|
-
ORDER: TAG_ORDER,
|
|
57
|
+
ORDER: TAG_ORDER, SMOOTH_PATH: TAG_SMOOTH_PATH, RIBBON_ORIENT: TAG_RIBBON_ORIENT,
|
|
58
|
+
LIT: TAG_LIT,
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
// --- curve builders -----------------------------------------------------------------------------
|
|
@@ -286,11 +292,29 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
|
286
292
|
* spread evenly along the path — trail density independent of speed, no per-frame clumps.
|
|
287
293
|
* Meant for `space: 'world'`. */
|
|
288
294
|
rateOverDistance?: number
|
|
295
|
+
/** Spread those spawn points along a CURVE through the emitter's recent path instead of the
|
|
296
|
+
* straight line between where it was last frame and where it is now. A straight line cuts the
|
|
297
|
+
* corner by however far the path bows inside one frame, which grows with the frame TIME — so a
|
|
298
|
+
* fast curving emitter looks faceted, and looks worse the slower the machine. Default false
|
|
299
|
+
* (the straight line); costs three stored positions and one curve evaluation per spawn.
|
|
300
|
+
*
|
|
301
|
+
* The curve needs a point one frame AHEAD, so each frame's spawns are placed provisionally and
|
|
302
|
+
* nudged into place on the next frame, once it exists. Nothing else observes them in between. */
|
|
303
|
+
smooth?: boolean
|
|
289
304
|
/** Draw order among blended systems at the same depth — higher draws later, i.e. on top (Unity's
|
|
290
305
|
* sortingOrder). Systems are depth-sorted by their node, so the emitters of one effect tie and
|
|
291
306
|
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
292
307
|
* Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
|
|
293
308
|
order?: number
|
|
309
|
+
/** Sprites take the light of the PLACE each one is in — the level's baked light grid (`env.lightmap.volume`).
|
|
310
|
+
* A sprite is unlit: its colour is the picture, which is right for fire and wrong for dust — a puff kicked up
|
|
311
|
+
* under an awning glows as if it stood in the sun. With `lit` every particle's colour is multiplied by the light
|
|
312
|
+
* where it is, relative to the level's OPEN ground: exactly the authored colour out in the sun, the ambient's
|
|
313
|
+
* share of it (tinted the way the shade is) under a roof — per particle, so a trail of puffs laid from the sun
|
|
314
|
+
* into the shade is lit along its length. Author the colour for the open; nothing else to tune. Without a grid
|
|
315
|
+
* — a level with no bake, a host that has none — the colours are drawn as authored. Points, quads, stretch and
|
|
316
|
+
* `Trail`; mesh particles are lit by their material. Default false. */
|
|
317
|
+
lit?: boolean
|
|
294
318
|
/** Coarse draw order among ALL blended draws, 0 (first) … 7 (last) — see `Mesh.renderPriority`.
|
|
295
319
|
* Default 5: meshes sit at 4 and decals at 3, so smoke covers a car's glass and its skid marks
|
|
296
320
|
* whatever the camera does (the engine's depth sort compares object centres, and an emitter's
|
|
@@ -477,7 +501,9 @@ export class Particles extends Node {
|
|
|
477
501
|
if (options.space !== undefined) out.push(TAG_SPACE, 1, options.space === "world" ? 1 : 0)
|
|
478
502
|
if (options.inheritVelocity !== undefined) out.push(TAG_INHERIT_VELOCITY, 1, options.inheritVelocity)
|
|
479
503
|
if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
|
|
504
|
+
if (options.smooth !== undefined) out.push(TAG_SMOOTH_PATH, 1, options.smooth ? 1 : 0)
|
|
480
505
|
if (options.order !== undefined) out.push(TAG_ORDER, 1, options.order)
|
|
506
|
+
if (options.lit !== undefined) out.push(TAG_LIT, 1, options.lit ? 1 : 0)
|
|
481
507
|
if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
|
|
482
508
|
if (options.shape !== undefined) pushShape(out, options.shape)
|
|
483
509
|
if (options.velocityOverLife !== undefined) pushVelocityLife(out, options.velocityOverLife)
|
|
@@ -530,7 +556,11 @@ export class Particles extends Node {
|
|
|
530
556
|
set space(val: "local" | "world") { this._send([ TAG_SPACE, 1, val === "world" ? 1 : 0 ]) }
|
|
531
557
|
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
532
558
|
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
559
|
+
/** Lay a frame's spawns along a curve through the emitter's path, not the straight chord. */
|
|
560
|
+
set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
|
|
533
561
|
set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
|
|
562
|
+
/** Colours × the baked light where each particle is — see `ParticlesOptions.lit`. */
|
|
563
|
+
set lit(val: boolean) { this._send([ TAG_LIT, 1, val ? 1 : 0 ]) }
|
|
534
564
|
/** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
|
|
535
565
|
set renderPriority(v: number) {
|
|
536
566
|
if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
|
|
@@ -620,8 +650,31 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
|
|
|
620
650
|
width?: ParticleValue
|
|
621
651
|
/** Minimum emitter movement (world units) between recorded points. Default 0.05. */
|
|
622
652
|
minDistance?: number
|
|
623
|
-
/** Point capacity (default 128).
|
|
653
|
+
/** Point capacity (default 128). A spawn that does not fit is DROPPED, so a fast emitter at a
|
|
654
|
+
* small `minDistance` wants headroom: a sword tip covers metres inside one `time` window. */
|
|
624
655
|
maxPoints?: number
|
|
656
|
+
/** Follow a CURVE through the emitter's recent path instead of the straight line between frames.
|
|
657
|
+
* A trail is where this shows most — the strip is the path — and it shows worse the lower the
|
|
658
|
+
* frame rate, since a longer frame bows further off its own chord. Default false; see
|
|
659
|
+
* `ParticlesOptions.smooth`. */
|
|
660
|
+
smooth?: boolean
|
|
661
|
+
/** The strip takes the baked light of the place each of its points is at — `ParticlesOptions.lit`. Smoke and
|
|
662
|
+
* dust trails; leave it off for a glowing one. Default false. */
|
|
663
|
+
lit?: boolean
|
|
664
|
+
/** Which way the strip's WIDTH points.
|
|
665
|
+
*
|
|
666
|
+
* `'camera'` (default) rolls the strip about its own length to stay flat to the viewer — it can
|
|
667
|
+
* never disappear, which is why it is the default, but it bears no relation to the thing that drew
|
|
668
|
+
* it. An AXIS (`'x'`/`'y'`/`'z'`) uses that axis of the emitter's own transform, as it was when
|
|
669
|
+
* each point was laid: on a node riding a sword's blade, `'z'` (the blade) makes the strip the
|
|
670
|
+
* surface the blade actually swept, and `width` stops being a made-up number — it is the blade. */
|
|
671
|
+
orient?: "camera" | "x" | "y" | "z"
|
|
672
|
+
/** With `orient` on an axis: the floor under the strip's APPARENT width, 0..1 of its real width,
|
|
673
|
+
* below which it rolls back toward the camera. An honestly oriented strip goes edge-on — and
|
|
674
|
+
* vanishes — whenever the swing happens in the plane of view; this is the hybrid that keeps it
|
|
675
|
+
* readable. 0 = never roll (honest, and it will vanish), 1 = always (the same as `'camera'`).
|
|
676
|
+
* Default 0.4: a sweep still reads as a sweep, and a swing toward the camera still shows. */
|
|
677
|
+
faceCamera?: number
|
|
625
678
|
color?: ParticleColor
|
|
626
679
|
/** Opacity along the trail. Defaults to fading the tail out (`curve().to(0)`). */
|
|
627
680
|
opacity?: ParticleValue
|
|
@@ -635,6 +688,8 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
|
|
|
635
688
|
export class Trail extends Node {
|
|
636
689
|
private _material: Material
|
|
637
690
|
private _rateDistance: number
|
|
691
|
+
private _orient = 0
|
|
692
|
+
private _faceCamera = 0.4
|
|
638
693
|
|
|
639
694
|
constructor(options: TrailOptions = {}) {
|
|
640
695
|
super()
|
|
@@ -650,6 +705,13 @@ export class Trail extends Node {
|
|
|
650
705
|
TAG_RATE_DISTANCE, 1, this._rateDistance,
|
|
651
706
|
TAG_LIFETIME, 2, time, time,
|
|
652
707
|
]
|
|
708
|
+
if (options.smooth) out.push(TAG_SMOOTH_PATH, 1, 1)
|
|
709
|
+
if (options.lit) out.push(TAG_LIT, 1, 1)
|
|
710
|
+
if (options.orient !== undefined || options.faceCamera !== undefined) {
|
|
711
|
+
this._orient = RIBBON_AXIS[options.orient ?? "camera"]
|
|
712
|
+
if (options.faceCamera !== undefined) this._faceCamera = options.faceCamera
|
|
713
|
+
out.push(TAG_RIBBON_ORIENT, 2, this._orient, this._faceCamera)
|
|
714
|
+
}
|
|
653
715
|
pushParam(out, 0, options.width ?? 0.1)
|
|
654
716
|
pushParam(out, 2, options.opacity ?? curve().to(0))
|
|
655
717
|
if (options.color !== undefined) pushColor(out, options.color)
|
|
@@ -686,6 +748,22 @@ export class Trail extends Node {
|
|
|
686
748
|
this._send([ TAG_RATE_DISTANCE, 1, this._rateDistance ])
|
|
687
749
|
}
|
|
688
750
|
|
|
751
|
+
/** Follow a curve through the emitter's path instead of the straight chord between frames. */
|
|
752
|
+
set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
|
|
753
|
+
set lit(val: boolean) { this._send([ TAG_LIT, 1, val ? 1 : 0 ]) }
|
|
754
|
+
|
|
755
|
+
/** Which way the strip's width points — see `TrailOptions.orient`. Keeps the current `faceCamera`. */
|
|
756
|
+
set orient(val: "camera" | "x" | "y" | "z") {
|
|
757
|
+
this._orient = RIBBON_AXIS[val]
|
|
758
|
+
this._send([ TAG_RIBBON_ORIENT, 2, this._orient, this._faceCamera ])
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** The hybrid's floor — see `TrailOptions.faceCamera`. */
|
|
762
|
+
set faceCamera(val: number) {
|
|
763
|
+
this._faceCamera = val
|
|
764
|
+
this._send([ TAG_RIBBON_ORIENT, 2, this._orient, val ])
|
|
765
|
+
}
|
|
766
|
+
|
|
689
767
|
/** Pause/resume laying down points — existing ones still age out, so the trail fades naturally
|
|
690
768
|
* after e.g. a sword swing ends. */
|
|
691
769
|
set emitting(val: boolean) { this._send([ TAG_RATE_DISTANCE, 1, val ? this._rateDistance : 0 ]) }
|
package/src/gl/Scene.ts
CHANGED
|
@@ -177,7 +177,8 @@ const installRenderSyncedFrames = (): void => {
|
|
|
177
177
|
export class Scene implements Presentable {
|
|
178
178
|
/** @internal native scene handle. */
|
|
179
179
|
readonly _id: number
|
|
180
|
-
/** @internal
|
|
180
|
+
/** @internal what `environmentIntensity` currently asks for — the accessor's backing field, so it
|
|
181
|
+
* answers on hosts whose engine has no live setter too. */
|
|
181
182
|
_environmentIntensity = 20000
|
|
182
183
|
readonly camera: Camera
|
|
183
184
|
/** The listener + global 3D audio knobs (docs/audio-plan.md). */
|
|
@@ -309,6 +310,26 @@ export class Scene implements Presentable {
|
|
|
309
310
|
setBloom(enabled: boolean, intensity = 0.2): void {
|
|
310
311
|
_creator.setBloomOptions(this._id, enabled, intensity, 1)
|
|
311
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* How bright the environment (IBL) lights the scene, in lux — `SceneOptions.environmentIntensity`
|
|
315
|
+
* after the fact. Live: it changes the probe's intensity, not the probe, so it costs nothing and
|
|
316
|
+
* can be dragged. (`setDefaultIbl`, the call that installs a probe, rebuilds the cubemap from the
|
|
317
|
+
* ktx every time — never drive a slider through a scene option that has to re-open.)
|
|
318
|
+
*
|
|
319
|
+
* `lecodes lightmap bake` reads the same number off the live probe, so a scene dimmed here bakes
|
|
320
|
+
* dimmed. Hosts without the call keep whatever the scene opened with, and the getter still
|
|
321
|
+
* reports what was asked for.
|
|
322
|
+
*/
|
|
323
|
+
get environmentIntensity(): number { return this._environmentIntensity }
|
|
324
|
+
set environmentIntensity(lux: number) {
|
|
325
|
+
this._environmentIntensity = lux
|
|
326
|
+
if (_creator.setEnvironmentIntensity) _creator.setEnvironmentIntensity(this._id, lux)
|
|
327
|
+
else if (!Scene._warnedEnvIntensity) {
|
|
328
|
+
Scene._warnedEnvIntensity = true
|
|
329
|
+
console.warn("[scene] this host has no setEnvironmentIntensity — environmentIntensity stays as the scene opened")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
private static _warnedEnvIntensity = false
|
|
312
333
|
/** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
|
|
313
334
|
setStencil(enabled: boolean): void {
|
|
314
335
|
if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
|
|
@@ -367,6 +388,18 @@ export class Scene implements Presentable {
|
|
|
367
388
|
return new Promise<void>((res) => _creator.warmRender(this._id, res))
|
|
368
389
|
}
|
|
369
390
|
|
|
391
|
+
/** Precompile the scene's shader variants so nothing is compiled mid-game. Every material the host
|
|
392
|
+
* knows is queued for the sun / shadow / fog / skinning variants and the dynamic-light key — the
|
|
393
|
+
* first point light (a muzzle flash, an explosion) would otherwise rebuild every lit shader in view
|
|
394
|
+
* on that frame. Call it once the sun and fog are set, typically under a loading screen; materials
|
|
395
|
+
* loaded later are queued in the background as they are created. Resolves at once on a host
|
|
396
|
+
* without the bridge. */
|
|
397
|
+
precompileShaders(): Promise<void> {
|
|
398
|
+
const fn = _creator.precompileShaders
|
|
399
|
+
if (!fn) return Promise.resolve()
|
|
400
|
+
return new Promise<void>((res) => fn.call(_creator, this._id, res))
|
|
401
|
+
}
|
|
402
|
+
|
|
370
403
|
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
371
404
|
// A scene is a destination like a screen: opening it replaces whatever is visible (suspending
|
|
372
405
|
// an active Router until Router.restore()), and it can be pushed onto the Router stack. The
|
|
@@ -82,6 +82,8 @@ export class AnimationClip {
|
|
|
82
82
|
readonly _index: number
|
|
83
83
|
/** @internal events: seconds + name, sorted (the engine gets them normalized via setClipEvents). */
|
|
84
84
|
_events: { t: number, name: string }[] = []
|
|
85
|
+
/** @internal anchor spans per bone, seconds (the engine gets them normalized via setClipAnchors). */
|
|
86
|
+
_anchors: { bone: string, donor: string, spans: { from: number, to: number, socket: string, rotation: number, pin: number }[] }[] = []
|
|
85
87
|
|
|
86
88
|
private constructor(set: number, index: number, info: ClipInfo) {
|
|
87
89
|
this._set = set
|
|
@@ -119,6 +121,48 @@ export class AnimationClip {
|
|
|
119
121
|
_creator.setClipEvents(this._set, this._index, new Float32Array(this._events.map((e) => (d > 0 ? e.t / d : 0))))
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/** ANCHOR SPANS for a bone — what an anchored IK chain ending in `bone` rides while this clip plays
|
|
125
|
+
* (`IK.TwoBone` with `anchor`; the sockets come from `anim.sockets(...)`). `donor` names the socket
|
|
126
|
+
* set of the gun the take was AUTHORED on ('' = the live set: no delta); `spans` = `[from, to,
|
|
127
|
+
* socket, rotation?, pin?]` in SECONDS — the socket the hand is on through that window (`''` = free:
|
|
128
|
+
* it rides the joint itself), `rotation` 0..1 how much of the socket's turn it takes (default 1),
|
|
129
|
+
* `pin` 0..1 how much it sits ON the socket instead of keeping its own relation to it (default 0: a
|
|
130
|
+
* magazine is REACHED, and the take's motion is what carries the hand there).
|
|
131
|
+
* Gaps between spans = the chain's own socket, pinned by the chain's `anchorPin` — that is where a
|
|
132
|
+
* hand holding the gun is nailed to the grip, so the layers below cannot slide it. A clip WITHOUT
|
|
133
|
+
* anchors takes no part: the hand keeps the take's own relation to the gun. Part of the clip, like
|
|
134
|
+
* events; `AnimationClip.from` carries them re-timed. Chainable.
|
|
135
|
+
* reload.anchors('hand_l', { donor: 'tr15', spans: [[0.9, 1.4, 'mag'], [1.4, 2.0, ''], [2.0, 2.4, 'mag'], [2.4, 2.7, 'bolt']] }) */
|
|
136
|
+
anchors(bone: string, def: { donor?: string, spans?: readonly (readonly [number, number, string, number?, number?])[] }): this {
|
|
137
|
+
const spans = (def.spans ?? []).map(([ from, to, socket, rotation, pin ]) => ({
|
|
138
|
+
from: Math.min(Math.max(0, from), this.duration), to: Math.min(Math.max(0, to), this.duration), socket,
|
|
139
|
+
rotation: rotation ?? 1, pin: pin ?? 0,
|
|
140
|
+
})).filter((s) => s.to > s.from).sort((a, b) => a.from - b.from)
|
|
141
|
+
const rec = { bone, donor: def.donor ?? "", spans }
|
|
142
|
+
const i = this._anchors.findIndex((a) => a.bone === bone)
|
|
143
|
+
if (i >= 0) this._anchors[i] = rec
|
|
144
|
+
else this._anchors.push(rec)
|
|
145
|
+
this._pushAnchors(rec)
|
|
146
|
+
return this
|
|
147
|
+
}
|
|
148
|
+
/** Drop a bone's anchor spans. */
|
|
149
|
+
clearAnchors(bone: string): this {
|
|
150
|
+
this._anchors = this._anchors.filter((a) => a.bone !== bone)
|
|
151
|
+
_creator.setClipAnchors?.(this._set, this._index, bone, "", "", new Float32Array(0))
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
private _pushAnchors(a: { bone: string, donor: string, spans: { from: number, to: number, socket: string, rotation: number, pin: number }[] }): void {
|
|
155
|
+
const d = this.duration
|
|
156
|
+
const spans = new Float32Array(a.spans.length * 4)
|
|
157
|
+
a.spans.forEach((s, i) => {
|
|
158
|
+
spans[i * 4] = d > 0 ? s.from / d : 0
|
|
159
|
+
spans[i * 4 + 1] = d > 0 ? s.to / d : 0
|
|
160
|
+
spans[i * 4 + 2] = s.rotation
|
|
161
|
+
spans[i * 4 + 3] = s.pin
|
|
162
|
+
})
|
|
163
|
+
_creator.setClipAnchors?.(this._set, this._index, a.bone, a.donor, a.spans.map((s) => s.socket).join("\n"), spans)
|
|
164
|
+
}
|
|
165
|
+
|
|
122
166
|
/** Load ONE clip from a GLB: the file's only/first clip, or the one named / at the given index. */
|
|
123
167
|
static load(source: string | FetchResponse, clip?: string | number): Promise<AnimationClip> {
|
|
124
168
|
return AnimationClip._loadSet(source).then(([setId, infos]) => {
|
|
@@ -200,6 +244,14 @@ export class AnimationClip {
|
|
|
200
244
|
out._events = windowed
|
|
201
245
|
? clip._events.filter((ev) => ev.t >= st - 1e-6 && ev.t <= e + 1e-6).map((ev) => ({ t: Math.min(Math.max(0, ev.t - st), e - st), name: ev.name }))
|
|
202
246
|
: clip._events.map((ev) => ({ ...ev }))
|
|
247
|
+
// the anchor spans too: clipped to the window and re-timed (a mirrored clip keeps its bone names —
|
|
248
|
+
// mirror the sockets yourself if a hand's anchors must swap sides)
|
|
249
|
+
for (const a of clip._anchors) {
|
|
250
|
+
const spans = windowed
|
|
251
|
+
? a.spans.map((s) => ({ ...s, from: Math.max(0, s.from - st), to: Math.min(e - st, s.to - st) })).filter((s) => s.to > s.from)
|
|
252
|
+
: a.spans.map((s) => ({ ...s }))
|
|
253
|
+
if (spans.length > 0 || a.donor) out.anchors(a.bone, { donor: a.donor, spans: spans.map((s) => [ s.from, s.to, s.socket, s.rotation ] as const) })
|
|
254
|
+
}
|
|
203
255
|
return out
|
|
204
256
|
}
|
|
205
257
|
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
// default. A clip may be replaced at any time; a one-shot on a layer with no loop holds its last frame.
|
|
18
18
|
//
|
|
19
19
|
// The surface, top to bottom: clips → playing → root motion → the feet and the warp (`anim.feet`,
|
|
20
|
-
// `anim.warp` — locomotion's post-processing of the pose) →
|
|
20
|
+
// `anim.warp` — locomotion's post-processing of the pose) → sockets (what IK chains ride) → level of
|
|
21
|
+
// detail → engine-facing.
|
|
21
22
|
|
|
22
23
|
import { Aspect } from "../../core/Aspect"
|
|
24
|
+
import { Vec3, type Vec3Like } from "../../math/vec"
|
|
25
|
+
import { Quat, type QuatLike } from "../../math/quat"
|
|
23
26
|
import type { Node } from "../Node"
|
|
24
27
|
import type { AnimationClip } from "./AnimationClip"
|
|
25
28
|
import { Core, type ActiveClip, type ClipInfo, type ClipEventHandler, type LayerOptions, type LoopDef, type LoopOptions, type PlayOptions, type StopOptions } from "./core"
|
|
@@ -122,6 +125,41 @@ export class Animator extends Aspect<"anim", Node> {
|
|
|
122
125
|
* turn too); on for a rig whose root carries the heading (`lecodes assets retarget --root-rotation yaw`). */
|
|
123
126
|
get rootRotation(): boolean { return this._rootRotation }
|
|
124
127
|
set rootRotation(on: boolean) { this._rootRotation = on; if (this._rootMotion) this._c.setRootMotion(true, on) }
|
|
128
|
+
/** The body's WORLD velocity this frame (m/s): what `anim.warp`'s stride and orientation fit the clips
|
|
129
|
+
* to. A `Locomotion` feeds it itself; when you drive the gait by hand set it every frame (the
|
|
130
|
+
* CharacterController's velocity, or your own). Reads back the last value fed. */
|
|
131
|
+
get motion(): readonly [number, number, number] { return this._motion }
|
|
132
|
+
set motion(v: { x: number, y: number, z: number } | readonly [number, number, number]) {
|
|
133
|
+
const [ x, y, z ] = Array.isArray(v) ? v as readonly [number, number, number] : [ (v as { x: number }).x, (v as { y: number }).y, (v as { z: number }).z ]
|
|
134
|
+
this._motion = [ x, y, z ]
|
|
135
|
+
this._c.setMotion(x, y, z)
|
|
136
|
+
}
|
|
137
|
+
private _motion: readonly [number, number, number] = [ 0, 0, 0 ]
|
|
138
|
+
|
|
139
|
+
// ---- sockets (what anchored IK chains ride — IK.ts) ---------------------------------------------
|
|
140
|
+
/** SOCKETS: named frames on a bone of this rig — a weapon's grip / magazine / bolt empties, placed in
|
|
141
|
+
* the scene editor. A NODE socket is read by the engine every frame (a socket on a moving part
|
|
142
|
+
* follows it); a `{ position, quaternion }` one is fixed in the bone's space. The default set is the
|
|
143
|
+
* LIVE one (the weapon in the hands); `set: 'tr15'` names a DONOR set — the same sockets on the gun a
|
|
144
|
+
* take was authored on, which a clip's `anchors()` refer to. Chainable.
|
|
145
|
+
* arms.anim.sockets('ik_hand_gun', { grip_r: w.gripR, grip_l: w.gripL, mag: w.mag })
|
|
146
|
+
* arms.anim.sockets('ik_hand_gun', { grip_l: { position: [0.03, -0.12, -0.05] } }, { set: 'tr15' }) */
|
|
147
|
+
sockets(joint: Node | string, table: Record<string, Node | { position?: Vec3Like, quaternion?: QuatLike }>, options: { set?: string } = {}): this {
|
|
148
|
+
const bone = typeof joint === "string" ? this.node.bone(joint) : joint
|
|
149
|
+
if (!bone) { console.warn(`Animator.sockets: no bone '${String(joint)}' under ${this.node.name || "the node"}`); return this }
|
|
150
|
+
const set = options.set ?? ""
|
|
151
|
+
for (const [ name, at ] of Object.entries(table)) {
|
|
152
|
+
if (typeof (at as Node).id === "number") this._c.setSocket(set, name, bone.id, (at as Node).id, null)
|
|
153
|
+
else {
|
|
154
|
+
const o = at as { position?: Vec3Like, quaternion?: QuatLike }
|
|
155
|
+
const p = new Vec3(o.position ?? [ 0, 0, 0 ]), q = o.quaternion ? new Quat(o.quaternion) : Quat.identity
|
|
156
|
+
this._c.setSocket(set, name, bone.id, 0, new Float32Array([ p.x, p.y, p.z, q.x, q.y, q.z, q.w ]))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return this
|
|
160
|
+
}
|
|
161
|
+
/** Drop a socket (the live set, or `set`). */
|
|
162
|
+
removeSocket(name: string, set = ""): this { this._c.removeSocket(set, name); return this }
|
|
125
163
|
|
|
126
164
|
// ---- level of detail --------------------------------------------------------------------------
|
|
127
165
|
/** `'auto'` (default): a character small on screen or out of view is evaluated every 2nd / 4th frame
|
|
@@ -130,9 +168,11 @@ export class Animator extends Aspect<"anim", Node> {
|
|
|
130
168
|
get lod(): "auto" | "full" { return this._lod }
|
|
131
169
|
set lod(v: "auto" | "full") { this._lod = v; _pushLod(this.node) }
|
|
132
170
|
|
|
133
|
-
// ---- engine-facing (a Locomotion, a bench)
|
|
171
|
+
// ---- engine-facing (a Locomotion, a bench, the IK aspects) ----------------------------------
|
|
134
172
|
/** The native animator's id — 0 until something is played or bound. */
|
|
135
173
|
get _id(): number { return this._c.id }
|
|
174
|
+
/** @internal the engine-facing core (IK.ts) */
|
|
175
|
+
get _core(): Core { return this._c }
|
|
136
176
|
/** Bind a clip to the base layer without playing it; its native slot (-1 = no such clip). */
|
|
137
177
|
_slot(clip: string | number): number { return this._c.slotIndex(clip) }
|
|
138
178
|
}
|
|
@@ -36,12 +36,14 @@ export type DynamicBoneFloor = "none" | "probe" | number | Vec3Like
|
|
|
36
36
|
* on the standard humanoid bones (hips, spine, head, limbs — the Ragdoll's layout); `'none'`; or the
|
|
37
37
|
* nodes carrying the colliders to use. */
|
|
38
38
|
export type DynamicBoneColliders = "auto" | "humanoid" | "none" | Node[]
|
|
39
|
-
/** The cloth's
|
|
40
|
-
* nearest surface (tails, ropes); `'auto'` = away from the root bone's own axis (a cloak's strips round
|
|
41
|
-
* spine face out from it); a vector = a fixed direction in the root bone's frame (a cape
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* through it
|
|
39
|
+
/** The cloth's SIDE of every collider marked `oneSided`: `'none'` = no side, every collider pushes a bone to
|
|
40
|
+
* its nearest surface (tails, ropes); `'auto'` = away from the root bone's own axis (a cloak's strips round
|
|
41
|
+
* a spine face out from it); a vector = a fixed direction in the root bone's frame (a cape hanging BEHIND
|
|
42
|
+
* the arms: the body's backward). A one-sided collider that is moving AWAY from that side — an arm swinging
|
|
43
|
+
* forward under the cape — puts a bone it has run into over to the side instead of carrying it: the arm
|
|
44
|
+
* passes through the cloth and the cape hangs behind it again, instead of being dragged round the body.
|
|
45
|
+
* Still, or moving toward the cloth, it pushes like any other collider, so cloth draped on an arm at rest
|
|
46
|
+
* stays where it is. Two-sided colliders (the body's) never read the side. */
|
|
45
47
|
export type DynamicBoneSide = "none" | "auto" | Vec3Like
|
|
46
48
|
|
|
47
49
|
const VERSION = 1
|
|
@@ -82,12 +84,17 @@ export class DynamicBoneCollider extends Aspect<"dynamicBoneCollider", Node> {
|
|
|
82
84
|
end?: Vec3Like
|
|
83
85
|
/** The bone the capsule reaches (its origin), instead of `end`. */
|
|
84
86
|
to?: string
|
|
87
|
+
/** ONE-SIDED: the cloth belongs on the chain's `side` of this capsule (see DynamicBone.side). While the capsule
|
|
88
|
+
* moves away from that side it puts a bone it has run into over to the side instead of carrying it — an arm
|
|
89
|
+
* under a cape may push the cape back, never drag it forward round the body. Needs a `side` on the DynamicBone. */
|
|
90
|
+
oneSided = false
|
|
85
91
|
|
|
86
92
|
static fields: FieldMeta<DynamicBoneCollider> = {
|
|
87
93
|
radius: { min: 0, max: 2, step: 0.005 },
|
|
88
94
|
offset: { hidden: true },
|
|
89
95
|
end: { hidden: true },
|
|
90
96
|
to: { hidden: true },
|
|
97
|
+
oneSided: { label: "One-sided" },
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
private _id = 0
|
|
@@ -109,14 +116,14 @@ export class DynamicBoneCollider extends Aspect<"dynamicBoneCollider", Node> {
|
|
|
109
116
|
onAttach(): void {
|
|
110
117
|
if (!supported()) return
|
|
111
118
|
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)
|
|
119
|
+
this._id = _creator.dynamicBoneColliderCreate!(this.node.id, a.x, a.y, a.z, b.x, b.y, b.z, this.radius, this.oneSided ? 1 : 0)
|
|
113
120
|
DynamicBone._collidersChanged()
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
onReconfigure(): void {
|
|
117
124
|
if (!this._id) return
|
|
118
125
|
const [ a, b ] = this._segment()
|
|
119
|
-
_creator.dynamicBoneColliderSet!(this._id, a.x, a.y, a.z, b.x, b.y, b.z, this.radius)
|
|
126
|
+
_creator.dynamicBoneColliderSet!(this._id, a.x, a.y, a.z, b.x, b.y, b.z, this.radius, this.oneSided ? 1 : 0)
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
onDetach(): void {
|
|
@@ -149,7 +156,8 @@ export class DynamicBone extends Aspect<"dynamicBone", Node> {
|
|
|
149
156
|
mass: DynamicBoneCurve = 1
|
|
150
157
|
/** Bones (by name) that ride the animation exactly while their subtrees still simulate: a sheet's
|
|
151
158
|
* 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
|
|
159
|
+
* and `link` ties neighbouring strips — neighbours in THIS order, so list the strips round the ring
|
|
160
|
+
* (front-left … back … front-right); the front stays open. */
|
|
153
161
|
pinned?: string[]
|
|
154
162
|
/** Metres a PINNED bone may be pushed off its animated place by a collider — a soft pin. Its particle
|
|
155
163
|
* collides and takes the edge pushes like a free bone (the strip below hangs from where it IS) and is
|
|
@@ -365,13 +373,28 @@ export class DynamicBone extends Aspect<"dynamicBone", Node> {
|
|
|
365
373
|
}
|
|
366
374
|
}
|
|
367
375
|
|
|
368
|
-
/** Neighbour links: the bones at the same depth, in
|
|
376
|
+
/** Neighbour links: the bones at the same depth tied pairwise, the columns in the order of `pinned` (a cloak's
|
|
377
|
+
* strips listed round the ring) — columns not listed there follow in tree order. Tree order alone is the
|
|
378
|
+
* engine's child order, which for a GLB is the file's node order REVERSED (Filament prepends each child), so
|
|
379
|
+
* strips added to a rig later were tied across the body instead of to their neighbours: the ermine's cloak
|
|
380
|
+
* had one true neighbour pair out of eight, the rest were rods through the chest. */
|
|
369
381
|
private _links(): Uint16Array | undefined {
|
|
370
382
|
if (this.link <= 0) return undefined
|
|
383
|
+
const index = new Map<Node, number>(this._chain.map((n, i) => [ n, i ]))
|
|
384
|
+
const column = new Int32Array(this._chain.length).fill(-1) // per bone: the chain index of its depth-1 ancestor
|
|
385
|
+
for (let i = 1; i < this._chain.length; i++) {
|
|
386
|
+
const p = index.get(this._chain[i]!.parent!) ?? 0
|
|
387
|
+
column[i] = this._depths[i] === 1 ? i : column[p]!
|
|
388
|
+
}
|
|
389
|
+
const ring = this.pinned ?? []
|
|
390
|
+
const key = (i: number): number => { const k = ring.indexOf(this._chain[column[i]!]!.name); return k < 0 ? ring.length + column[i]! : k }
|
|
371
391
|
const byDepth = new Map<number, number[]>()
|
|
372
392
|
this._depths.forEach((d, i) => { if (d > 0) (byDepth.get(d) ?? byDepth.set(d, []).get(d)!).push(i) })
|
|
373
393
|
const pairs: number[] = []
|
|
374
|
-
for (const row of byDepth.values())
|
|
394
|
+
for (const row of byDepth.values()) {
|
|
395
|
+
row.sort((a, b) => key(a) - key(b))
|
|
396
|
+
for (let k = 0; k + 1 < row.length; k++) pairs.push(row[k]!, row[k + 1]!)
|
|
397
|
+
}
|
|
375
398
|
return pairs.length ? new Uint16Array(pairs) : undefined
|
|
376
399
|
}
|
|
377
400
|
|
|
@@ -447,7 +470,7 @@ export class DynamicBone extends Aspect<"dynamicBone", Node> {
|
|
|
447
470
|
const len = local.length()
|
|
448
471
|
end = len > 1e-6 ? local.scale((p.length ?? 0.2) / len) : new Vec3(0, p.length ?? 0.2, 0)
|
|
449
472
|
}
|
|
450
|
-
const id = _creator.dynamicBoneColliderCreate!(bone.id, 0, 0, 0, end.x, end.y, end.z, p.radius ?? 0.05)
|
|
473
|
+
const id = _creator.dynamicBoneColliderCreate!(bone.id, 0, 0, 0, end.x, end.y, end.z, p.radius ?? 0.05, 0)
|
|
451
474
|
if (id) this._own.push(id)
|
|
452
475
|
}
|
|
453
476
|
}
|