reze-engine 0.29.1 → 0.30.1
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/README.md +19 -0
- package/dist/engine.d.ts +15 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +56 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/locomotion.d.ts +27 -0
- package/dist/locomotion.d.ts.map +1 -1
- package/dist/locomotion.js +120 -2
- package/dist/model.d.ts +18 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +99 -1
- package/package.json +1 -1
- package/src/engine.ts +64 -8
- package/src/index.ts +1 -0
- package/src/locomotion.ts +146 -2
- package/src/model.ts +107 -1
package/src/model.ts
CHANGED
|
@@ -313,6 +313,21 @@ export class Model {
|
|
|
313
313
|
private morphTrackIndices: Map<string, number> = new Map()
|
|
314
314
|
private lastAppliedClip: AnimationClip | null = null
|
|
315
315
|
|
|
316
|
+
// One-shot action layer: plays a clip ONCE over whatever else drives the pose
|
|
317
|
+
// (locomotion blend, a playing clip, a crossfade, or rest), with fade-in/out
|
|
318
|
+
// envelopes — the background keeps advancing and is what the fade-out returns to.
|
|
319
|
+
private oneShot: {
|
|
320
|
+
name: string
|
|
321
|
+
time: number
|
|
322
|
+
duration: number
|
|
323
|
+
fadeIn: number
|
|
324
|
+
fadeOut: number
|
|
325
|
+
cancelW: number
|
|
326
|
+
cancelling: boolean
|
|
327
|
+
onEnd: (() => void) | null
|
|
328
|
+
} | null = null
|
|
329
|
+
private readonly oneShotEntries: BlendEntry[] = []
|
|
330
|
+
|
|
316
331
|
// Blended pose: declarative N-clip mix (setBlendPose) or a running crossfade.
|
|
317
332
|
// Cursor caches are per clip so sampling several clips in one frame doesn't
|
|
318
333
|
// thrash the single-clip caches above; WeakMap so removed clips can collect.
|
|
@@ -1329,6 +1344,7 @@ export class Model {
|
|
|
1329
1344
|
}
|
|
1330
1345
|
this.blendEntries = null
|
|
1331
1346
|
this.crossfade = null
|
|
1347
|
+
this.oneShot = null
|
|
1332
1348
|
this.resetAllBones()
|
|
1333
1349
|
this.resetAllMorphs()
|
|
1334
1350
|
return this.animationState.play(name, options)
|
|
@@ -1337,6 +1353,7 @@ export class Model {
|
|
|
1337
1353
|
show(name: string): void {
|
|
1338
1354
|
this.blendEntries = null
|
|
1339
1355
|
this.crossfade = null
|
|
1356
|
+
this.oneShot = null
|
|
1340
1357
|
this.resetAllBones()
|
|
1341
1358
|
this.resetAllMorphs()
|
|
1342
1359
|
this.animationState.show(name)
|
|
@@ -1357,6 +1374,37 @@ export class Model {
|
|
|
1357
1374
|
this.blendEntries = null
|
|
1358
1375
|
}
|
|
1359
1376
|
|
|
1377
|
+
/** Play a clip ONCE over whatever currently drives the pose — the locomotion blend,
|
|
1378
|
+
* a playing clip, a crossfade, or rest. The background keeps advancing underneath;
|
|
1379
|
+
* fade-in ramps the one-shot over it and fade-out returns to whatever the
|
|
1380
|
+
* background is doing by then. Replaces any active one-shot immediately. */
|
|
1381
|
+
playOneShot(name: string, options?: { fadeIn?: number; fadeOut?: number; onEnd?: () => void }): boolean {
|
|
1382
|
+
const clip = this.animationState.getAnimationClip(name)
|
|
1383
|
+
if (!clip || clip.frameCount <= 0 || !Number.isFinite(clip.frameCount)) return false
|
|
1384
|
+
const duration = clip.frameCount / FPS
|
|
1385
|
+
const fadeIn = Math.max(0, options?.fadeIn ?? 0.15)
|
|
1386
|
+
const fadeOut = Math.max(0, Math.min(options?.fadeOut ?? 0.25, duration))
|
|
1387
|
+
this.oneShot = { name, time: 0, duration, fadeIn, fadeOut, cancelW: 1, cancelling: false, onEnd: options?.onEnd ?? null }
|
|
1388
|
+
this.clipApplySuspended = false
|
|
1389
|
+
return true
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/** Fade the active one-shot out early over `fadeOut` seconds (default 0.2). */
|
|
1393
|
+
cancelOneShot(fadeOut = 0.2): void {
|
|
1394
|
+
if (!this.oneShot) return
|
|
1395
|
+
if (fadeOut <= 0) {
|
|
1396
|
+
this.oneShot = null
|
|
1397
|
+
return
|
|
1398
|
+
}
|
|
1399
|
+
this.oneShot.cancelling = true
|
|
1400
|
+
this.oneShot.fadeOut = fadeOut
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** Name of the active one-shot, or null. */
|
|
1404
|
+
getOneShot(): string | null {
|
|
1405
|
+
return this.oneShot?.name ?? null
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1360
1408
|
/** Fade from the currently playing clip (or from the rest pose when nothing plays)
|
|
1361
1409
|
* into `name` over `seconds`. The target starts at frame 0 and becomes the current
|
|
1362
1410
|
* clip immediately — progress, looping and the camera clock report the target for
|
|
@@ -1364,6 +1412,7 @@ export class Model {
|
|
|
1364
1412
|
crossfadeTo(name: string, seconds: number, options?: { loop?: boolean }): boolean {
|
|
1365
1413
|
if (!this.animationState.hasAnimation(name)) return false
|
|
1366
1414
|
this.blendEntries = null
|
|
1415
|
+
this.oneShot = null
|
|
1367
1416
|
this.clipApplySuspended = false
|
|
1368
1417
|
|
|
1369
1418
|
const fromName = this.animationState.getCurrentAnimation()
|
|
@@ -1397,6 +1446,7 @@ export class Model {
|
|
|
1397
1446
|
stop(): void {
|
|
1398
1447
|
this.blendEntries = null
|
|
1399
1448
|
this.crossfade = null
|
|
1449
|
+
this.oneShot = null
|
|
1400
1450
|
this.animationState.stop()
|
|
1401
1451
|
}
|
|
1402
1452
|
|
|
@@ -1411,6 +1461,7 @@ export class Model {
|
|
|
1411
1461
|
clearAnimation(): void {
|
|
1412
1462
|
this.blendEntries = null
|
|
1413
1463
|
this.crossfade = null
|
|
1464
|
+
this.oneShot = null
|
|
1414
1465
|
this.animationState.clear()
|
|
1415
1466
|
}
|
|
1416
1467
|
|
|
@@ -1727,6 +1778,59 @@ export class Model {
|
|
|
1727
1778
|
}
|
|
1728
1779
|
}
|
|
1729
1780
|
|
|
1781
|
+
/** One one-shot step: weight envelope from fade-in/out (easeInOut-shaped), the
|
|
1782
|
+
* background entries scaled by 1-w underneath, the one-shot clip at w on top. */
|
|
1783
|
+
private applyOneShot(deltaTime: number): void {
|
|
1784
|
+
const os = this.oneShot
|
|
1785
|
+
if (os === null) return
|
|
1786
|
+
os.time += deltaTime
|
|
1787
|
+
|
|
1788
|
+
// Envelope: min of the fade-in ramp and the fade-out ramp (or the cancel ramp).
|
|
1789
|
+
const wIn = os.fadeIn > 0 ? Math.min(1, os.time / os.fadeIn) : 1
|
|
1790
|
+
let wOut = 1
|
|
1791
|
+
if (os.cancelling) {
|
|
1792
|
+
os.cancelW -= deltaTime / os.fadeOut
|
|
1793
|
+
wOut = Math.max(0, os.cancelW)
|
|
1794
|
+
} else if (os.fadeOut > 0) {
|
|
1795
|
+
wOut = Math.max(0, Math.min(1, (os.duration - os.time) / os.fadeOut))
|
|
1796
|
+
}
|
|
1797
|
+
const w = easeInOut(Math.min(wIn, wOut))
|
|
1798
|
+
|
|
1799
|
+
// Background entries at (1 - w): the still-advancing blend, the current clip, or
|
|
1800
|
+
// nothing (rest fill). Copied into a pooled array — never scale caller-owned weights.
|
|
1801
|
+
const pool = this.oneShotEntries
|
|
1802
|
+
let n = 0
|
|
1803
|
+
const put = (name: string, time: number, weight: number) => {
|
|
1804
|
+
if (pool.length <= n) pool.push({ name: "", time: 0, weight: 0 })
|
|
1805
|
+
const e = pool[n++]
|
|
1806
|
+
e.name = name
|
|
1807
|
+
e.time = time
|
|
1808
|
+
e.weight = weight
|
|
1809
|
+
}
|
|
1810
|
+
const bg = 1 - w
|
|
1811
|
+
if (bg > 1e-6) {
|
|
1812
|
+
if (this.blendEntries !== null && this.blendEntries.length > 0) {
|
|
1813
|
+
for (const e of this.blendEntries) put(e.name, e.time, e.weight * bg)
|
|
1814
|
+
} else {
|
|
1815
|
+
const clip = this.animationState.getCurrentClip()
|
|
1816
|
+
const name = this.animationState.getCurrentAnimation()
|
|
1817
|
+
if (clip !== null && name !== null && name !== os.name) {
|
|
1818
|
+
put(name, this.animationState.getCurrentFrame() / FPS, bg)
|
|
1819
|
+
}
|
|
1820
|
+
// else: rest pose fills the remainder inside applyBlendedPose
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
put(os.name, Math.min(os.time, os.duration), w)
|
|
1824
|
+
for (let i = n; i < pool.length; i++) pool[i].weight = 0
|
|
1825
|
+
this.applyBlendedPose(pool)
|
|
1826
|
+
|
|
1827
|
+
if (os.time >= os.duration || (os.cancelling && os.cancelW <= 0)) {
|
|
1828
|
+
const onEnd = os.onEnd
|
|
1829
|
+
this.oneShot = null
|
|
1830
|
+
onEnd?.()
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1730
1834
|
/** One crossfade step: advance the outgoing clock (the target's clock lives in
|
|
1731
1835
|
* animationState, already ticked by update), shape the weight with easeInOut,
|
|
1732
1836
|
* and hand both to the blend sampler. Holds in place while paused. */
|
|
@@ -1781,7 +1885,9 @@ export class Model {
|
|
|
1781
1885
|
|
|
1782
1886
|
this.animationState.update(deltaTime)
|
|
1783
1887
|
if (!this.clipApplySuspended) {
|
|
1784
|
-
if (this.
|
|
1888
|
+
if (this.oneShot !== null) {
|
|
1889
|
+
this.applyOneShot(deltaTime)
|
|
1890
|
+
} else if (this.blendEntries !== null && this.blendEntries.length > 0) {
|
|
1785
1891
|
this.applyBlendedPose(this.blendEntries)
|
|
1786
1892
|
} else if (this.crossfade !== null) {
|
|
1787
1893
|
this.applyCrossfade(deltaTime)
|