reze-engine 0.2.13 → 0.2.14
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/engine.d.ts +8 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +101 -1
- package/package.json +1 -1
- package/src/engine.ts +124 -1
package/dist/engine.d.ts
CHANGED
|
@@ -100,6 +100,8 @@ export declare class Engine {
|
|
|
100
100
|
private gpuMemoryMB;
|
|
101
101
|
private hasAnimation;
|
|
102
102
|
private playingAnimation;
|
|
103
|
+
private breathingTimeout;
|
|
104
|
+
private breathingBaseRotations;
|
|
103
105
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
|
|
104
106
|
init(): Promise<void>;
|
|
105
107
|
private createPipelines;
|
|
@@ -114,8 +116,13 @@ export declare class Engine {
|
|
|
114
116
|
private addLight;
|
|
115
117
|
private setAmbient;
|
|
116
118
|
loadAnimation(url: string): Promise<void>;
|
|
117
|
-
playAnimation(
|
|
119
|
+
playAnimation(options?: {
|
|
120
|
+
breathBones?: string[] | Record<string, number>;
|
|
121
|
+
breathDuration?: number;
|
|
122
|
+
}): void;
|
|
118
123
|
stopAnimation(): void;
|
|
124
|
+
private stopBreathing;
|
|
125
|
+
private startBreathing;
|
|
119
126
|
getStats(): EngineStats;
|
|
120
127
|
runRenderLoop(callback?: () => void): void;
|
|
121
128
|
stopRenderLoop(): void;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -55,6 +55,8 @@ export class Engine {
|
|
|
55
55
|
this.gpuMemoryMB = 0;
|
|
56
56
|
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
57
57
|
this.playingAnimation = false; // Set to true when playAnimation is called
|
|
58
|
+
this.breathingTimeout = null;
|
|
59
|
+
this.breathingBaseRotations = new Map();
|
|
58
60
|
this.canvas = canvas;
|
|
59
61
|
if (options) {
|
|
60
62
|
this.ambient = options.ambient ?? 1.0;
|
|
@@ -1268,11 +1270,26 @@ export class Engine {
|
|
|
1268
1270
|
this.animationFrames = frames;
|
|
1269
1271
|
this.hasAnimation = true;
|
|
1270
1272
|
}
|
|
1271
|
-
playAnimation() {
|
|
1273
|
+
playAnimation(options) {
|
|
1272
1274
|
if (this.animationFrames.length === 0)
|
|
1273
1275
|
return;
|
|
1274
1276
|
this.stopAnimation();
|
|
1277
|
+
this.stopBreathing();
|
|
1275
1278
|
this.playingAnimation = true;
|
|
1279
|
+
// Enable breathing if breathBones is provided
|
|
1280
|
+
const enableBreath = options?.breathBones !== undefined && options.breathBones !== null;
|
|
1281
|
+
let breathBones = [];
|
|
1282
|
+
let breathRotationRanges = undefined;
|
|
1283
|
+
if (enableBreath && options.breathBones) {
|
|
1284
|
+
if (Array.isArray(options.breathBones)) {
|
|
1285
|
+
breathBones = options.breathBones;
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
breathBones = Object.keys(options.breathBones);
|
|
1289
|
+
breathRotationRanges = options.breathBones;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const breathDuration = options?.breathDuration ?? 4000;
|
|
1276
1293
|
const allBoneKeyFrames = [];
|
|
1277
1294
|
for (const keyFrame of this.animationFrames) {
|
|
1278
1295
|
for (const boneFrame of keyFrame.boneFrames) {
|
|
@@ -1360,6 +1377,40 @@ export class Engine {
|
|
|
1360
1377
|
}
|
|
1361
1378
|
}
|
|
1362
1379
|
}
|
|
1380
|
+
// Setup breathing animation if enabled
|
|
1381
|
+
if (enableBreath && this.currentModel) {
|
|
1382
|
+
// Find the last frame time
|
|
1383
|
+
let maxTime = 0;
|
|
1384
|
+
for (const keyFrame of this.animationFrames) {
|
|
1385
|
+
if (keyFrame.time > maxTime) {
|
|
1386
|
+
maxTime = keyFrame.time;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
// Get last frame rotations directly from animation data for breathing bones
|
|
1390
|
+
const lastFrameRotations = new Map();
|
|
1391
|
+
for (const bone of breathBones) {
|
|
1392
|
+
const keyFrames = boneKeyFramesByBone.get(bone);
|
|
1393
|
+
if (keyFrames && keyFrames.length > 0) {
|
|
1394
|
+
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1395
|
+
let lastRotation = null;
|
|
1396
|
+
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1397
|
+
if (keyFrames[i].time <= maxTime) {
|
|
1398
|
+
lastRotation = keyFrames[i].rotation;
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (lastRotation) {
|
|
1403
|
+
lastFrameRotations.set(bone, lastRotation);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// Start breathing after animation completes
|
|
1408
|
+
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1409
|
+
const animationEndTime = maxTime * 1000 + 200; // Small buffer for final tweens to complete
|
|
1410
|
+
this.breathingTimeout = window.setTimeout(() => {
|
|
1411
|
+
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration);
|
|
1412
|
+
}, animationEndTime);
|
|
1413
|
+
}
|
|
1363
1414
|
}
|
|
1364
1415
|
stopAnimation() {
|
|
1365
1416
|
for (const timeoutId of this.animationTimeouts) {
|
|
@@ -1368,6 +1419,54 @@ export class Engine {
|
|
|
1368
1419
|
this.animationTimeouts = [];
|
|
1369
1420
|
this.playingAnimation = false;
|
|
1370
1421
|
}
|
|
1422
|
+
stopBreathing() {
|
|
1423
|
+
if (this.breathingTimeout !== null) {
|
|
1424
|
+
clearTimeout(this.breathingTimeout);
|
|
1425
|
+
this.breathingTimeout = null;
|
|
1426
|
+
}
|
|
1427
|
+
this.breathingBaseRotations.clear();
|
|
1428
|
+
}
|
|
1429
|
+
startBreathing(bones, baseRotations, rotationRanges, durationMs = 4000) {
|
|
1430
|
+
if (!this.currentModel)
|
|
1431
|
+
return;
|
|
1432
|
+
// Store base rotations directly from last frame of animation data
|
|
1433
|
+
// These are the exact rotations from the animation - use them as-is
|
|
1434
|
+
for (const bone of bones) {
|
|
1435
|
+
const baseRot = baseRotations.get(bone);
|
|
1436
|
+
if (baseRot) {
|
|
1437
|
+
this.breathingBaseRotations.set(bone, baseRot);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const halfCycleMs = durationMs / 2;
|
|
1441
|
+
const defaultRotation = 0.02; // Default rotation range if not specified per bone
|
|
1442
|
+
// Start breathing cycle - oscillate around exact base rotation (final pose)
|
|
1443
|
+
// Each bone can have its own rotation range, or use default
|
|
1444
|
+
const animate = (isInhale) => {
|
|
1445
|
+
if (!this.currentModel)
|
|
1446
|
+
return;
|
|
1447
|
+
const breathingBoneNames = [];
|
|
1448
|
+
const breathingQuats = [];
|
|
1449
|
+
for (const bone of bones) {
|
|
1450
|
+
const baseRot = this.breathingBaseRotations.get(bone);
|
|
1451
|
+
if (!baseRot)
|
|
1452
|
+
continue;
|
|
1453
|
+
// Get rotation range for this bone (per-bone or default)
|
|
1454
|
+
const rotation = rotationRanges?.[bone] ?? defaultRotation;
|
|
1455
|
+
// Oscillate around base rotation with the bone's rotation range
|
|
1456
|
+
// isInhale: base * rotation, exhale: base * (-rotation)
|
|
1457
|
+
const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0);
|
|
1458
|
+
const finalRot = baseRot.multiply(oscillationRot);
|
|
1459
|
+
breathingBoneNames.push(bone);
|
|
1460
|
+
breathingQuats.push(finalRot);
|
|
1461
|
+
}
|
|
1462
|
+
if (breathingBoneNames.length > 0) {
|
|
1463
|
+
this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs);
|
|
1464
|
+
}
|
|
1465
|
+
this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs);
|
|
1466
|
+
};
|
|
1467
|
+
// Start breathing from exhale position (closer to base) to minimize initial movement
|
|
1468
|
+
animate(false);
|
|
1469
|
+
}
|
|
1371
1470
|
getStats() {
|
|
1372
1471
|
return { ...this.stats };
|
|
1373
1472
|
}
|
|
@@ -1392,6 +1491,7 @@ export class Engine {
|
|
|
1392
1491
|
dispose() {
|
|
1393
1492
|
this.stopRenderLoop();
|
|
1394
1493
|
this.stopAnimation();
|
|
1494
|
+
this.stopBreathing();
|
|
1395
1495
|
if (this.camera)
|
|
1396
1496
|
this.camera.detachControl();
|
|
1397
1497
|
if (this.resizeObserver) {
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -139,6 +139,8 @@ export class Engine {
|
|
|
139
139
|
private gpuMemoryMB: number = 0
|
|
140
140
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
141
141
|
private playingAnimation = false // Set to true when playAnimation is called
|
|
142
|
+
private breathingTimeout: number | null = null
|
|
143
|
+
private breathingBaseRotations: Map<string, Quat> = new Map()
|
|
142
144
|
|
|
143
145
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
144
146
|
this.canvas = canvas
|
|
@@ -1420,12 +1422,32 @@ export class Engine {
|
|
|
1420
1422
|
this.hasAnimation = true
|
|
1421
1423
|
}
|
|
1422
1424
|
|
|
1423
|
-
public playAnimation(
|
|
1425
|
+
public playAnimation(options?: {
|
|
1426
|
+
breathBones?: string[] | Record<string, number> // Array of bone names or map of bone name -> rotation range
|
|
1427
|
+
breathDuration?: number // Breathing cycle duration in milliseconds
|
|
1428
|
+
}) {
|
|
1424
1429
|
if (this.animationFrames.length === 0) return
|
|
1425
1430
|
|
|
1426
1431
|
this.stopAnimation()
|
|
1432
|
+
this.stopBreathing()
|
|
1427
1433
|
this.playingAnimation = true
|
|
1428
1434
|
|
|
1435
|
+
// Enable breathing if breathBones is provided
|
|
1436
|
+
const enableBreath = options?.breathBones !== undefined && options.breathBones !== null
|
|
1437
|
+
let breathBones: string[] = []
|
|
1438
|
+
let breathRotationRanges: Record<string, number> | undefined = undefined
|
|
1439
|
+
|
|
1440
|
+
if (enableBreath && options.breathBones) {
|
|
1441
|
+
if (Array.isArray(options.breathBones)) {
|
|
1442
|
+
breathBones = options.breathBones
|
|
1443
|
+
} else {
|
|
1444
|
+
breathBones = Object.keys(options.breathBones)
|
|
1445
|
+
breathRotationRanges = options.breathBones
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const breathDuration = options?.breathDuration ?? 4000
|
|
1450
|
+
|
|
1429
1451
|
const allBoneKeyFrames: BoneKeyFrame[] = []
|
|
1430
1452
|
for (const keyFrame of this.animationFrames) {
|
|
1431
1453
|
for (const boneFrame of keyFrame.boneFrames) {
|
|
@@ -1529,6 +1551,43 @@ export class Engine {
|
|
|
1529
1551
|
}
|
|
1530
1552
|
}
|
|
1531
1553
|
}
|
|
1554
|
+
|
|
1555
|
+
// Setup breathing animation if enabled
|
|
1556
|
+
if (enableBreath && this.currentModel) {
|
|
1557
|
+
// Find the last frame time
|
|
1558
|
+
let maxTime = 0
|
|
1559
|
+
for (const keyFrame of this.animationFrames) {
|
|
1560
|
+
if (keyFrame.time > maxTime) {
|
|
1561
|
+
maxTime = keyFrame.time
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Get last frame rotations directly from animation data for breathing bones
|
|
1566
|
+
const lastFrameRotations = new Map<string, Quat>()
|
|
1567
|
+
for (const bone of breathBones) {
|
|
1568
|
+
const keyFrames = boneKeyFramesByBone.get(bone)
|
|
1569
|
+
if (keyFrames && keyFrames.length > 0) {
|
|
1570
|
+
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1571
|
+
let lastRotation: Quat | null = null
|
|
1572
|
+
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1573
|
+
if (keyFrames[i].time <= maxTime) {
|
|
1574
|
+
lastRotation = keyFrames[i].rotation
|
|
1575
|
+
break
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (lastRotation) {
|
|
1579
|
+
lastFrameRotations.set(bone, lastRotation)
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Start breathing after animation completes
|
|
1585
|
+
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1586
|
+
const animationEndTime = maxTime * 1000 + 200 // Small buffer for final tweens to complete
|
|
1587
|
+
this.breathingTimeout = window.setTimeout(() => {
|
|
1588
|
+
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration)
|
|
1589
|
+
}, animationEndTime)
|
|
1590
|
+
}
|
|
1532
1591
|
}
|
|
1533
1592
|
|
|
1534
1593
|
public stopAnimation() {
|
|
@@ -1539,6 +1598,69 @@ export class Engine {
|
|
|
1539
1598
|
this.playingAnimation = false
|
|
1540
1599
|
}
|
|
1541
1600
|
|
|
1601
|
+
private stopBreathing() {
|
|
1602
|
+
if (this.breathingTimeout !== null) {
|
|
1603
|
+
clearTimeout(this.breathingTimeout)
|
|
1604
|
+
this.breathingTimeout = null
|
|
1605
|
+
}
|
|
1606
|
+
this.breathingBaseRotations.clear()
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
private startBreathing(
|
|
1610
|
+
bones: string[],
|
|
1611
|
+
baseRotations: Map<string, Quat>,
|
|
1612
|
+
rotationRanges?: Record<string, number>,
|
|
1613
|
+
durationMs: number = 4000
|
|
1614
|
+
) {
|
|
1615
|
+
if (!this.currentModel) return
|
|
1616
|
+
|
|
1617
|
+
// Store base rotations directly from last frame of animation data
|
|
1618
|
+
// These are the exact rotations from the animation - use them as-is
|
|
1619
|
+
for (const bone of bones) {
|
|
1620
|
+
const baseRot = baseRotations.get(bone)
|
|
1621
|
+
if (baseRot) {
|
|
1622
|
+
this.breathingBaseRotations.set(bone, baseRot)
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const halfCycleMs = durationMs / 2
|
|
1627
|
+
const defaultRotation = 0.02 // Default rotation range if not specified per bone
|
|
1628
|
+
|
|
1629
|
+
// Start breathing cycle - oscillate around exact base rotation (final pose)
|
|
1630
|
+
// Each bone can have its own rotation range, or use default
|
|
1631
|
+
const animate = (isInhale: boolean) => {
|
|
1632
|
+
if (!this.currentModel) return
|
|
1633
|
+
|
|
1634
|
+
const breathingBoneNames: string[] = []
|
|
1635
|
+
const breathingQuats: Quat[] = []
|
|
1636
|
+
|
|
1637
|
+
for (const bone of bones) {
|
|
1638
|
+
const baseRot = this.breathingBaseRotations.get(bone)
|
|
1639
|
+
if (!baseRot) continue
|
|
1640
|
+
|
|
1641
|
+
// Get rotation range for this bone (per-bone or default)
|
|
1642
|
+
const rotation = rotationRanges?.[bone] ?? defaultRotation
|
|
1643
|
+
|
|
1644
|
+
// Oscillate around base rotation with the bone's rotation range
|
|
1645
|
+
// isInhale: base * rotation, exhale: base * (-rotation)
|
|
1646
|
+
const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0)
|
|
1647
|
+
const finalRot = baseRot.multiply(oscillationRot)
|
|
1648
|
+
|
|
1649
|
+
breathingBoneNames.push(bone)
|
|
1650
|
+
breathingQuats.push(finalRot)
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (breathingBoneNames.length > 0) {
|
|
1654
|
+
this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs)
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs)
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Start breathing from exhale position (closer to base) to minimize initial movement
|
|
1661
|
+
animate(false)
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1542
1664
|
public getStats(): EngineStats {
|
|
1543
1665
|
return { ...this.stats }
|
|
1544
1666
|
}
|
|
@@ -1570,6 +1692,7 @@ export class Engine {
|
|
|
1570
1692
|
public dispose() {
|
|
1571
1693
|
this.stopRenderLoop()
|
|
1572
1694
|
this.stopAnimation()
|
|
1695
|
+
this.stopBreathing()
|
|
1573
1696
|
if (this.camera) this.camera.detachControl()
|
|
1574
1697
|
if (this.resizeObserver) {
|
|
1575
1698
|
this.resizeObserver.disconnect()
|