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 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(): void;
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;
@@ -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":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAMnC,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,IAAI,CAAA;CACpB,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAeD,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,kBAAkB,CAAY;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,WAAW,CAAC,CAAW;IAC/B,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,YAAY,CAAa;IAEjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,uBAAuB,CAAoB;IACnD,OAAO,CAAC,iBAAiB,CAAoB;IAE7C,OAAO,CAAC,eAAe,CAAoB;IAC3C,OAAO,CAAC,mBAAmB,CAAoB;IAC/C,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,sBAAsB,CAAqB;IACnD,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAW;IACpC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,uBAAuB,CAAC,CAAW;IAC3C,OAAO,CAAC,yBAAyB,CAAC,CAAoB;IACtD,OAAO,CAAC,0BAA0B,CAAC,CAAc;IACjD,OAAO,CAAC,eAAe,CAAC,CAAW;IACnC,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;IAChC,OAAO,CAAC,oBAAoB,CAA0B;IAEtD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAI;IACtC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAK;IAC5C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAI;IAE3C,OAAO,CAAC,OAAO,CAAc;IAE7B,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,sBAAsB,CAAiB;IAC/C,OAAO,CAAC,mBAAmB,CAAa;IACxC,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAa;IAEtC,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,oBAAoB,CAAoB;IAEhD,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,aAAa,CAAa;IAElC,OAAO,CAAC,qBAAqB,CAAC,CAAc;IAC5C,OAAO,CAAC,mBAAmB,CAAC,CAAc;IAC1C,OAAO,CAAC,mBAAmB,CAAC,CAAc;IAC1C,OAAO,CAAC,qBAAqB,CAAC,CAAc;IAE5C,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAe;IAErC,OAAO,CAAC,iBAAiB,CAAe;IAExC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,YAAY,CAAgC;IAEpD,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,oBAAoB,CAAiB;IAC7C,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,kBAAkB,CAAiB;IAC3C,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,uBAAuB,CAAiB;IAEhD,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,gBAAgB,CAAe;IACvC,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,KAAK,CAIZ;IACD,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,kBAAkB,CAA4B;IAEtD,OAAO,CAAC,eAAe,CAAoB;IAC3C,OAAO,CAAC,iBAAiB,CAAe;IACxC,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,sBAAsB,CAA+B;gBAEjD,MAAM,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,aAAa;IAYjD,IAAI;IA8BjB,OAAO,CAAC,eAAe;IA4sBvB,OAAO,CAAC,+BAA+B;IAwCvC,OAAO,CAAC,oBAAoB;IAwC5B,OAAO,CAAC,oBAAoB;IA4O5B,OAAO,CAAC,UAAU;IA+DlB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,YAAY;IA8EpB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,UAAU;IAIL,aAAa,CAAC,GAAG,EAAE,MAAM;IAM/B,aAAa,CAAC,OAAO,CAAC,EAAE;QAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC/C,cAAc,CAAC,EAAE,MAAM,CAAA;KACxB;IAqKM,aAAa;IAQpB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,cAAc;IAuDf,QAAQ,IAAI,WAAW;IAIvB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI;IAgBnC,cAAc;IAQd,OAAO;IAYD,SAAS,CAAC,IAAI,EAAE,MAAM;IAmB5B,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM;YAK5D,iBAAiB;YA0GjB,cAAc;YA+Pd,qBAAqB;IAmC5B,MAAM;IAmIb,OAAO,CAAC,UAAU;IAmGlB,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,eAAe;IAkBvB,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,kBAAkB;CAgF3B"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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()