reze-engine 0.2.12 → 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
@@ -1,5 +1,4 @@
1
1
  import { Quat, Vec3 } from "./math";
2
- import { PoolOptions } from "./pool";
3
2
  export type EngineOptions = {
4
3
  ambient?: number;
5
4
  bloomIntensity?: number;
@@ -76,7 +75,6 @@ export declare class Engine {
76
75
  private currentModel;
77
76
  private modelDir;
78
77
  private physics;
79
- private pool;
80
78
  private materialSampler;
81
79
  private textureCache;
82
80
  private opaqueDraws;
@@ -102,6 +100,8 @@ export declare class Engine {
102
100
  private gpuMemoryMB;
103
101
  private hasAnimation;
104
102
  private playingAnimation;
103
+ private breathingTimeout;
104
+ private breathingBaseRotations;
105
105
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
106
106
  init(): Promise<void>;
107
107
  private createPipelines;
@@ -112,20 +112,22 @@ export declare class Engine {
112
112
  private setupResize;
113
113
  private handleResize;
114
114
  private setupCamera;
115
- private createCameraBindGroupLayout;
116
- private createCameraBindGroup;
117
115
  private setupLighting;
118
116
  private addLight;
119
117
  private setAmbient;
120
118
  loadAnimation(url: string): Promise<void>;
121
- playAnimation(): void;
119
+ playAnimation(options?: {
120
+ breathBones?: string[] | Record<string, number>;
121
+ breathDuration?: number;
122
+ }): void;
122
123
  stopAnimation(): void;
124
+ private stopBreathing;
125
+ private startBreathing;
123
126
  getStats(): EngineStats;
124
127
  runRenderLoop(callback?: () => void): void;
125
128
  stopRenderLoop(): void;
126
129
  dispose(): void;
127
130
  loadModel(path: string): Promise<void>;
128
- addPool(options?: PoolOptions): Promise<void>;
129
131
  rotateBones(bones: string[], rotations: Quat[], durationMs?: number): void;
130
132
  private setupModelBuffers;
131
133
  private setupMaterials;
@@ -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
@@ -1,6 +1,5 @@
1
1
  import { Camera } from "./camera";
2
2
  import { Quat, Vec3 } from "./math";
3
- import { Pool } from "./pool";
4
3
  import { PmxLoader } from "./pmx-loader";
5
4
  import { Physics } from "./physics";
6
5
  import { VMDLoader } from "./vmd-loader";
@@ -27,7 +26,6 @@ export class Engine {
27
26
  this.currentModel = null;
28
27
  this.modelDir = "";
29
28
  this.physics = null;
30
- this.pool = null;
31
29
  this.textureCache = new Map();
32
30
  // Draw lists
33
31
  this.opaqueDraws = [];
@@ -57,6 +55,8 @@ export class Engine {
57
55
  this.gpuMemoryMB = 0;
58
56
  this.hasAnimation = false; // Set to true when loadAnimation is called
59
57
  this.playingAnimation = false; // Set to true when playAnimation is called
58
+ this.breathingTimeout = null;
59
+ this.breathingBaseRotations = new Map();
60
60
  this.canvas = canvas;
61
61
  if (options) {
62
62
  this.ambient = options.ambient ?? 1.0;
@@ -1231,36 +1231,6 @@ export class Engine {
1231
1231
  this.camera.aspect = this.canvas.width / this.canvas.height;
1232
1232
  this.camera.attachControl(this.canvas);
1233
1233
  }
1234
- // Create camera bind group layout for pool (camera-only)
1235
- createCameraBindGroupLayout() {
1236
- return this.device.createBindGroupLayout({
1237
- label: "camera bind group layout",
1238
- entries: [
1239
- {
1240
- binding: 0,
1241
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
1242
- buffer: {
1243
- type: "uniform",
1244
- },
1245
- },
1246
- ],
1247
- });
1248
- }
1249
- // Create camera bind group for pool
1250
- createCameraBindGroup(layout) {
1251
- return this.device.createBindGroup({
1252
- label: "camera bind group for pool",
1253
- layout: layout,
1254
- entries: [
1255
- {
1256
- binding: 0,
1257
- resource: {
1258
- buffer: this.cameraUniformBuffer,
1259
- },
1260
- },
1261
- ],
1262
- });
1263
- }
1264
1234
  // Step 5: Create lighting buffers
1265
1235
  setupLighting() {
1266
1236
  this.lightUniformBuffer = this.device.createBuffer({
@@ -1300,11 +1270,26 @@ export class Engine {
1300
1270
  this.animationFrames = frames;
1301
1271
  this.hasAnimation = true;
1302
1272
  }
1303
- playAnimation() {
1273
+ playAnimation(options) {
1304
1274
  if (this.animationFrames.length === 0)
1305
1275
  return;
1306
1276
  this.stopAnimation();
1277
+ this.stopBreathing();
1307
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;
1308
1293
  const allBoneKeyFrames = [];
1309
1294
  for (const keyFrame of this.animationFrames) {
1310
1295
  for (const boneFrame of keyFrame.boneFrames) {
@@ -1392,6 +1377,40 @@ export class Engine {
1392
1377
  }
1393
1378
  }
1394
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
+ }
1395
1414
  }
1396
1415
  stopAnimation() {
1397
1416
  for (const timeoutId of this.animationTimeouts) {
@@ -1400,6 +1419,54 @@ export class Engine {
1400
1419
  this.animationTimeouts = [];
1401
1420
  this.playingAnimation = false;
1402
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
+ }
1403
1470
  getStats() {
1404
1471
  return { ...this.stats };
1405
1472
  }
@@ -1424,6 +1491,7 @@ export class Engine {
1424
1491
  dispose() {
1425
1492
  this.stopRenderLoop();
1426
1493
  this.stopAnimation();
1494
+ this.stopBreathing();
1427
1495
  if (this.camera)
1428
1496
  this.camera.detachControl();
1429
1497
  if (this.resizeObserver) {
@@ -1449,14 +1517,6 @@ export class Engine {
1449
1517
  this.physics = new Physics(model.getRigidbodies(), model.getJoints());
1450
1518
  await this.setupModelBuffers(model);
1451
1519
  }
1452
- async addPool(options) {
1453
- if (!this.device) {
1454
- throw new Error("Engine must be initialized before adding pool");
1455
- }
1456
- const cameraLayout = this.createCameraBindGroupLayout();
1457
- this.pool = new Pool(this.device, cameraLayout, this.cameraUniformBuffer, options);
1458
- await this.pool.init();
1459
- }
1460
1520
  rotateBones(bones, rotations, durationMs) {
1461
1521
  this.currentModel?.rotateBones(bones, rotations, durationMs);
1462
1522
  }
@@ -1820,11 +1880,6 @@ export class Engine {
1820
1880
  }
1821
1881
  const pass = encoder.beginRenderPass(this.renderPassDescriptor);
1822
1882
  this.drawCallCount = 0;
1823
- // Render pool first if no model
1824
- if (this.pool && !this.currentModel) {
1825
- this.pool.render(pass);
1826
- this.drawCallCount++;
1827
- }
1828
1883
  if (this.currentModel) {
1829
1884
  pass.setVertexBuffer(0, this.vertexBuffer);
1830
1885
  pass.setVertexBuffer(1, this.jointsBuffer);
@@ -1839,16 +1894,6 @@ export class Engine {
1839
1894
  this.drawCallCount++;
1840
1895
  }
1841
1896
  }
1842
- // Pass 1.5: Pool (water plane) - render after opaque, before eyes
1843
- if (this.pool) {
1844
- this.pool.render(pass, {
1845
- vertexBuffer: this.vertexBuffer,
1846
- jointsBuffer: this.jointsBuffer,
1847
- weightsBuffer: this.weightsBuffer,
1848
- indexBuffer: this.indexBuffer,
1849
- });
1850
- this.drawCallCount++;
1851
- }
1852
1897
  // Pass 2: Eyes (writes stencil value for hair to test against)
1853
1898
  pass.setPipeline(this.eyePipeline);
1854
1899
  pass.setStencilReference(this.STENCIL_EYE_VALUE);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { Engine, type EngineStats } from "./engine";
2
2
  export { Vec3, Quat, Mat4 } from "./math";
3
- export { Pool, type PoolOptions } from "./pool";
4
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,2 @@
1
1
  export { Engine } from "./engine";
2
2
  export { Vec3, Quat, Mat4 } from "./math";
3
- export { Pool } from "./pool";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.2.12",
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
@@ -1,7 +1,6 @@
1
1
  import { Camera } from "./camera"
2
2
  import { Quat, Vec3 } from "./math"
3
3
  import { Model } from "./model"
4
- import { Pool, PoolOptions } from "./pool"
5
4
  import { PmxLoader } from "./pmx-loader"
6
5
  import { Physics } from "./physics"
7
6
  import { VMDKeyFrame, VMDLoader } from "./vmd-loader"
@@ -108,7 +107,6 @@ export class Engine {
108
107
  private currentModel: Model | null = null
109
108
  private modelDir: string = ""
110
109
  private physics: Physics | null = null
111
- private pool: Pool | null = null
112
110
  private materialSampler!: GPUSampler
113
111
  private textureCache = new Map<string, GPUTexture>()
114
112
  // Draw lists
@@ -141,6 +139,8 @@ export class Engine {
141
139
  private gpuMemoryMB: number = 0
142
140
  private hasAnimation = false // Set to true when loadAnimation is called
143
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()
144
144
 
145
145
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
146
146
  this.canvas = canvas
@@ -1376,38 +1376,6 @@ export class Engine {
1376
1376
  this.camera.attachControl(this.canvas)
1377
1377
  }
1378
1378
 
1379
- // Create camera bind group layout for pool (camera-only)
1380
- private createCameraBindGroupLayout(): GPUBindGroupLayout {
1381
- return this.device.createBindGroupLayout({
1382
- label: "camera bind group layout",
1383
- entries: [
1384
- {
1385
- binding: 0,
1386
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
1387
- buffer: {
1388
- type: "uniform",
1389
- },
1390
- },
1391
- ],
1392
- })
1393
- }
1394
-
1395
- // Create camera bind group for pool
1396
- private createCameraBindGroup(layout: GPUBindGroupLayout): GPUBindGroup {
1397
- return this.device.createBindGroup({
1398
- label: "camera bind group for pool",
1399
- layout: layout,
1400
- entries: [
1401
- {
1402
- binding: 0,
1403
- resource: {
1404
- buffer: this.cameraUniformBuffer,
1405
- },
1406
- },
1407
- ],
1408
- })
1409
- }
1410
-
1411
1379
  // Step 5: Create lighting buffers
1412
1380
  private setupLighting() {
1413
1381
  this.lightUniformBuffer = this.device.createBuffer({
@@ -1454,12 +1422,32 @@ export class Engine {
1454
1422
  this.hasAnimation = true
1455
1423
  }
1456
1424
 
1457
- 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
+ }) {
1458
1429
  if (this.animationFrames.length === 0) return
1459
1430
 
1460
1431
  this.stopAnimation()
1432
+ this.stopBreathing()
1461
1433
  this.playingAnimation = true
1462
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
+
1463
1451
  const allBoneKeyFrames: BoneKeyFrame[] = []
1464
1452
  for (const keyFrame of this.animationFrames) {
1465
1453
  for (const boneFrame of keyFrame.boneFrames) {
@@ -1563,6 +1551,43 @@ export class Engine {
1563
1551
  }
1564
1552
  }
1565
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
+ }
1566
1591
  }
1567
1592
 
1568
1593
  public stopAnimation() {
@@ -1573,6 +1598,69 @@ export class Engine {
1573
1598
  this.playingAnimation = false
1574
1599
  }
1575
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
+
1576
1664
  public getStats(): EngineStats {
1577
1665
  return { ...this.stats }
1578
1666
  }
@@ -1604,6 +1692,7 @@ export class Engine {
1604
1692
  public dispose() {
1605
1693
  this.stopRenderLoop()
1606
1694
  this.stopAnimation()
1695
+ this.stopBreathing()
1607
1696
  if (this.camera) this.camera.detachControl()
1608
1697
  if (this.resizeObserver) {
1609
1698
  this.resizeObserver.disconnect()
@@ -1631,15 +1720,6 @@ export class Engine {
1631
1720
  await this.setupModelBuffers(model)
1632
1721
  }
1633
1722
 
1634
- public async addPool(options?: PoolOptions) {
1635
- if (!this.device) {
1636
- throw new Error("Engine must be initialized before adding pool")
1637
- }
1638
- const cameraLayout = this.createCameraBindGroupLayout()
1639
- this.pool = new Pool(this.device, cameraLayout, this.cameraUniformBuffer, options)
1640
- await this.pool.init()
1641
- }
1642
-
1643
1723
  public rotateBones(bones: string[], rotations: Quat[], durationMs?: number) {
1644
1724
  this.currentModel?.rotateBones(bones, rotations, durationMs)
1645
1725
  }
@@ -2067,12 +2147,6 @@ export class Engine {
2067
2147
 
2068
2148
  this.drawCallCount = 0
2069
2149
 
2070
- // Render pool first if no model
2071
- if (this.pool && !this.currentModel) {
2072
- this.pool.render(pass)
2073
- this.drawCallCount++
2074
- }
2075
-
2076
2150
  if (this.currentModel) {
2077
2151
  pass.setVertexBuffer(0, this.vertexBuffer)
2078
2152
  pass.setVertexBuffer(1, this.jointsBuffer)
@@ -2089,17 +2163,6 @@ export class Engine {
2089
2163
  }
2090
2164
  }
2091
2165
 
2092
- // Pass 1.5: Pool (water plane) - render after opaque, before eyes
2093
- if (this.pool) {
2094
- this.pool.render(pass, {
2095
- vertexBuffer: this.vertexBuffer,
2096
- jointsBuffer: this.jointsBuffer,
2097
- weightsBuffer: this.weightsBuffer,
2098
- indexBuffer: this.indexBuffer!,
2099
- })
2100
- this.drawCallCount++
2101
- }
2102
-
2103
2166
  // Pass 2: Eyes (writes stencil value for hair to test against)
2104
2167
  pass.setPipeline(this.eyePipeline)
2105
2168
  pass.setStencilReference(this.STENCIL_EYE_VALUE)
package/src/index.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export { Engine, type EngineStats } from "./engine"
2
2
  export { Vec3, Quat, Mat4 } from "./math"
3
- export { Pool, type PoolOptions } from "./pool"
package/src/pool.ts DELETED
@@ -1,483 +0,0 @@
1
- import { Vec3 } from "./math"
2
-
3
- export interface PoolOptions {
4
- y?: number // Y position (default: 12)
5
- size?: number // Plane size (default: 100)
6
- segments?: number // Subdivision count (default: 50)
7
- }
8
-
9
- export class Pool {
10
- private device!: GPUDevice
11
- private vertexBuffer!: GPUBuffer
12
- private indexBuffer!: GPUBuffer
13
- private pipeline!: GPURenderPipeline
14
- private bindGroup!: GPUBindGroup
15
- private bindGroupLayout!: GPUBindGroupLayout
16
- private uniformBuffer!: GPUBuffer
17
- private cameraBindGroupLayout!: GPUBindGroupLayout
18
- private cameraBindGroup!: GPUBindGroup
19
- private cameraUniformBuffer!: GPUBuffer
20
- private indexCount: number = 0
21
- private y: number
22
- private size: number
23
- private segments: number
24
- private seaColor: Vec3
25
- private seaLight: Vec3
26
- private startTime: number = performance.now()
27
-
28
- constructor(
29
- device: GPUDevice,
30
- cameraBindGroupLayout: GPUBindGroupLayout,
31
- cameraUniformBuffer: GPUBuffer,
32
- options?: PoolOptions
33
- ) {
34
- this.device = device
35
- this.cameraBindGroupLayout = cameraBindGroupLayout
36
- this.cameraUniformBuffer = cameraUniformBuffer
37
- this.y = options?.y ?? 15
38
- this.size = options?.size ?? 100
39
- this.segments = options?.segments ?? 50
40
- // Hardcoded dark night pool colors (not used in shader, but kept for uniform buffer)
41
- this.seaColor = new Vec3(0.02, 0.05, 0.12) // Dark night pool base
42
- this.seaLight = new Vec3(0.1, 0.3, 0.5) // Light cyan for lit areas
43
- }
44
-
45
- public async init() {
46
- this.createGeometry()
47
- this.createShader()
48
- this.createUniforms()
49
- }
50
-
51
- private createGeometry() {
52
- const segments = this.segments
53
- const size = this.size
54
- const halfSize = size / 2
55
- const step = size / segments
56
-
57
- // Generate vertices
58
- const vertices: number[] = []
59
- for (let i = 0; i <= segments; i++) {
60
- for (let j = 0; j <= segments; j++) {
61
- const x = -halfSize + j * step
62
- const z = -halfSize + i * step
63
- const y = this.y
64
- const u = j / segments
65
- const v = i / segments
66
-
67
- // Position (x, y, z) + UV (u, v)
68
- vertices.push(x, y, z, u, v)
69
- }
70
- }
71
-
72
- // Generate indices
73
- const indices: number[] = []
74
- for (let i = 0; i < segments; i++) {
75
- for (let j = 0; j < segments; j++) {
76
- const topLeft = i * (segments + 1) + j
77
- const topRight = topLeft + 1
78
- const bottomLeft = (i + 1) * (segments + 1) + j
79
- const bottomRight = bottomLeft + 1
80
-
81
- // Two triangles per quad
82
- indices.push(topLeft, bottomLeft, topRight)
83
- indices.push(topRight, bottomLeft, bottomRight)
84
- }
85
- }
86
-
87
- this.indexCount = indices.length
88
-
89
- // Create buffers
90
- this.vertexBuffer = this.device.createBuffer({
91
- label: "pool vertices",
92
- size: vertices.length * 4,
93
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
94
- })
95
- this.device.queue.writeBuffer(this.vertexBuffer, 0, new Float32Array(vertices))
96
-
97
- const indexBufferSize = indices.length * 4
98
- this.indexBuffer = this.device.createBuffer({
99
- label: "pool indices",
100
- size: indexBufferSize,
101
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
102
- })
103
- this.device.queue.writeBuffer(this.indexBuffer, 0, new Uint32Array(indices))
104
-
105
- // Verify: segments=50 should give 50*50*6 = 15000 indices = 60000 bytes
106
- if (this.indexCount !== indices.length) {
107
- console.warn(`Pool index count mismatch: expected ${indices.length}, got ${this.indexCount}`)
108
- }
109
- }
110
-
111
- private createShader() {
112
- const shaderModule = this.device.createShaderModule({
113
- label: "pool shader",
114
- code: /* wgsl */ `
115
- struct CameraUniforms {
116
- view: mat4x4f,
117
- projection: mat4x4f,
118
- viewPos: vec3f,
119
- _padding: f32,
120
- };
121
-
122
- struct PoolUniforms {
123
- time: f32,
124
- poolY: f32,
125
- seaColor: vec3f,
126
- seaLight: vec3f,
127
- };
128
-
129
- struct VertexOutput {
130
- @builtin(position) position: vec4f,
131
- @location(0) worldPos: vec3f,
132
- @location(1) uv: vec2f,
133
- };
134
-
135
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
136
- @group(1) @binding(0) var<uniform> pool: PoolUniforms;
137
-
138
- // Procedural noise function (simplified)
139
- fn hash(p: vec2f) -> f32 {
140
- var p3 = fract(vec3f(p.xyx) * vec3f(443.8975, 397.2973, 491.1871));
141
- p3 += dot(p3, p3.yzx + 19.19);
142
- return fract((p3.x + p3.y) * p3.z);
143
- }
144
-
145
- fn noise(p: vec2f) -> f32 {
146
- let i = floor(p);
147
- var f = fract(p);
148
- f = f * f * (3.0 - 2.0 * f);
149
-
150
- let a = hash(i);
151
- let b = hash(i + vec2f(1.0, 0.0));
152
- let c = hash(i + vec2f(0.0, 1.0));
153
- let d = hash(i + vec2f(1.0, 1.0));
154
-
155
- return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
156
- }
157
-
158
- // Layered noise for water height (adapted from Shadertoy - matches reference exactly)
159
- fn waterHeight(uv: vec2f, time: f32) -> f32 {
160
- var e = 0.0;
161
- // Match Shadertoy: time*mod(j,.789)*.1 - time*.05
162
- for (var j = 1.0; j < 6.0; j += 1.0) {
163
- let timeOffset = time * (j % 0.789) * 0.1 - time * 0.05;
164
- let scaledUV = uv * (j * 1.789) + j * 159.45 + timeOffset;
165
- e += noise(scaledUV) / j;
166
- }
167
- return e / 6.0;
168
- }
169
-
170
- // Calculate water normals from height gradients (matches Shadertoy reference)
171
- fn waterNormals(uv: vec2f, time: f32) -> vec3f {
172
- // Match Shadertoy: uv.x *= .25 (scale X differently for more wave detail)
173
- let scaledUV = vec2f(uv.x * 0.25, uv.y);
174
- let eps = 0.008; // Match Shadertoy epsilon
175
- let h = waterHeight(scaledUV, time);
176
- let hx = waterHeight(scaledUV + vec2f(eps, 0.0), time);
177
- let hz = waterHeight(scaledUV + vec2f(0.0, eps), time);
178
-
179
- // Match Shadertoy normal calculation exactly
180
- let n = vec3f(h - hx, 1.0, h - hz);
181
- return normalize(n);
182
- }
183
-
184
- @vertex fn vs(
185
- @location(0) position: vec3f,
186
- @location(1) uv: vec2f
187
- ) -> VertexOutput {
188
- var output: VertexOutput;
189
-
190
- // Displace Y based on water height - much higher waves
191
- let time = pool.time;
192
- // More wave detail - smaller scale for more waves
193
- // Wave direction: back-left to front-right (both U and V increase)
194
- let waveUV = uv * 12.0 + vec2f(time * 0.3, time * 0.2); // Front-right direction (both positive)
195
- let height = waterHeight(waveUV, time) * 2; // Much higher wave amplitude
196
- let displacedY = position.y + height;
197
-
198
- let worldPos = vec3f(position.x, displacedY, position.z);
199
- output.worldPos = worldPos;
200
- output.uv = uv;
201
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
202
-
203
- return output;
204
- }
205
-
206
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
207
- let time = pool.time;
208
- // More wave detail - smaller scale for more waves (matches Shadertoy approach)
209
- // Wave direction: back-left to front-right (both U and V increase)
210
- let uv = input.uv * 12.0 + vec2f(time * 0.3, time * 0.2); // Front-right direction (both positive)
211
-
212
- // Calculate water normals from height gradients (this creates the wave effect)
213
- let n = waterNormals(uv, time);
214
-
215
- // View direction
216
- let viewDir = normalize(camera.viewPos - input.worldPos);
217
-
218
- // Fresnel effect for reflection (stronger at glancing angles)
219
- var fresnel = 1.0 - max(dot(n, viewDir), 0.0);
220
- fresnel = fresnel * fresnel;
221
-
222
- // Dark night pool - very dark base
223
- let darkPoolColor = vec3f(0.01, 0.02, 0.05); // Very dark blue-black
224
-
225
- // Center spotlight effect - reflection-like bright center
226
- let centerUV = input.uv - vec2f(0.5, 0.5); // Center at (0.5, 0.5)
227
- let distFromCenter = length(centerUV);
228
- // Smaller spotlight area with very smooth, subtle gradient
229
- let spotlightFalloff = 1.0 - smoothstep(0.0, 0.12, distFromCenter); // Smaller radius (0.12)
230
-
231
- // Reflection-like bright center - brighter, balanced blue
232
- let spotlightColor = vec3f(0.2, 0.4, 0.6); // Balanced blue
233
- let spotlightCenter = vec3f(0.5, 0.65, 0.85); // More white center
234
-
235
- // Very smooth, subtle gradient mix - multiple smoothstep layers for smoother transition
236
- let falloff1 = smoothstep(0.0, 0.12, distFromCenter); // Outer edge
237
- let falloff2 = smoothstep(0.0, 0.08, distFromCenter); // Inner edge
238
- var color = mix(darkPoolColor, spotlightColor, (1.0 - falloff1) * 0.9); // Brighter outer gradient
239
- color = mix(color, spotlightCenter, (1.0 - falloff2) * (1.0 - falloff2) * 1.0); // Very bright inner reflection
240
-
241
- // Add reflection-like effect based on view angle and normals
242
- let reflectionFactor = max(dot(n, vec3f(0.0, 1.0, 0.0)), 0.0); // More reflection when looking down
243
- let reflectionBrightness = spotlightFalloff * reflectionFactor * 0.5;
244
- color += spotlightCenter * reflectionBrightness; // Add reflection-like brightness
245
-
246
- // Wave-based color variation (matches Shadertoy transparency approach)
247
- // Match Shadertoy: transparency = dot(n, vec3(0.,.2,1.5)) * 12. + 1.5
248
- var transparency = dot(n, vec3f(0.0, 0.2, 1.5));
249
- transparency = (transparency * 12.0 + 1.5);
250
-
251
- // Match Shadertoy color mixing: mix with seaColor and seaLight (brighter, balanced blue)
252
- let seaColor = vec3f(0.08, 0.18, 0.3); // Balanced blue
253
- let seaLight = vec3f(0.12, 0.25, 0.45); // Balanced blue
254
- // Only apply this mixing subtly to avoid green tint
255
- color = mix(color, seaColor, clamp(transparency, 0.0, 1.0) * 0.3);
256
- color = mix(color, seaLight, max(0.0, transparency - 1.5) * 0.2);
257
-
258
- // Enhanced wave-based color variation for more visible waves
259
- let waveHeight = waterHeight(uv, time);
260
- let waveContrast = (waveHeight - 0.5) * 0.3; // Amplify wave contrast
261
- color += vec3f(0.05, 0.08, 0.15) * waveContrast * spotlightFalloff; // Balanced blue wave highlights
262
-
263
- // Enhanced underwater glow - white/neutral glow with subtle blue tint, much brighter
264
- let glowIntensity = spotlightFalloff * 0.6 + fresnel * 0.3 + waveHeight * 0.2; // Stronger glow
265
- let glowColor = vec3f(0.35, 0.35, 0.45); // Brighter glow with subtle blue tint
266
- color += glowColor * glowIntensity;
267
-
268
- // Additional subtle white glow around the center, brighter and more white
269
- let centerGlow = spotlightFalloff * spotlightFalloff * 0.4; // Soft glow falloff
270
- let whiteGlow = vec3f(0.55, 0.55, 0.6); // Brighter white glow
271
- color += whiteGlow * centerGlow * 0.7; // Brighter white center glow
272
-
273
- // Reflection of dark night sky
274
- let nightSkyColor = vec3f(0.02, 0.04, 0.08); // Very dark night sky
275
- let reflection = mix(darkPoolColor, nightSkyColor, fresnel * 0.2);
276
- color = mix(color, reflection, fresnel * 0.3 * (1.0 - spotlightFalloff)); // Less reflection in spotlight
277
-
278
- // Specular highlights from underwater lights (bokeh-like bright spots)
279
- let lightDir1 = normalize(vec3f(0.3, -0.4, 0.6));
280
- let lightDir2 = normalize(vec3f(-0.3, -0.3, 0.7));
281
- let reflDir1 = reflect(-lightDir1, n);
282
- let reflDir2 = reflect(-lightDir2, n);
283
- var specular1 = max(dot(viewDir, reflDir1), 0.0);
284
- var specular2 = max(dot(viewDir, reflDir2), 0.0);
285
- specular1 = pow(specular1, 150.0); // Tight, bright highlights
286
- specular2 = pow(specular2, 180.0);
287
- // Subtle white/blue highlights (bokeh effect) - darker, less blue
288
- color += vec3f(0.8, 0.8, 0.9) * specular1 * 1.2 * spotlightFalloff; // Darker white
289
- color += vec3f(0.4, 0.5, 0.7) * specular2 * 0.9 * spotlightFalloff; // Darker, less blue
290
-
291
- return vec4f(color, 0.8); // Half transparent water
292
- }
293
- `,
294
- })
295
-
296
- // Create bind group layout for pool uniforms
297
- this.bindGroupLayout = this.device.createBindGroupLayout({
298
- label: "pool bind group layout",
299
- entries: [
300
- {
301
- binding: 0,
302
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
303
- buffer: {
304
- type: "uniform",
305
- },
306
- },
307
- ],
308
- })
309
-
310
- // Create render pipeline
311
- this.pipeline = this.device.createRenderPipeline({
312
- label: "pool pipeline",
313
- layout: this.device.createPipelineLayout({
314
- bindGroupLayouts: [this.cameraBindGroupLayout, this.bindGroupLayout],
315
- }),
316
- vertex: {
317
- module: shaderModule,
318
- entryPoint: "vs",
319
- buffers: [
320
- {
321
- arrayStride: 5 * 4, // 3 floats (position) + 2 floats (uv)
322
- attributes: [
323
- {
324
- shaderLocation: 0,
325
- offset: 0,
326
- format: "float32x3",
327
- },
328
- {
329
- shaderLocation: 1,
330
- offset: 3 * 4,
331
- format: "float32x2",
332
- },
333
- ],
334
- },
335
- ],
336
- },
337
- fragment: {
338
- module: shaderModule,
339
- entryPoint: "fs",
340
- targets: [
341
- {
342
- format: "bgra8unorm",
343
- blend: {
344
- color: {
345
- srcFactor: "src-alpha",
346
- dstFactor: "one-minus-src-alpha",
347
- },
348
- alpha: {
349
- srcFactor: "one",
350
- dstFactor: "one-minus-src-alpha",
351
- },
352
- },
353
- },
354
- ],
355
- },
356
- primitive: {
357
- topology: "triangle-list",
358
- cullMode: "none",
359
- },
360
- depthStencil: {
361
- depthWriteEnabled: true,
362
- depthCompare: "less-equal",
363
- format: "depth24plus-stencil8",
364
- },
365
- multisample: {
366
- count: 4,
367
- },
368
- })
369
- }
370
-
371
- private createUniforms() {
372
- // Create uniform buffer
373
- // WGSL uniform buffers require 16-byte alignment:
374
- // time: f32 (4) + poolY (4) + padding (8) = 16 bytes
375
- // seaColor: vec3f (12) + padding (4) = 16 bytes
376
- // seaLight: vec3f (12) + padding (4) = 16 bytes
377
- // Total: 48 bytes
378
- this.uniformBuffer = this.device.createBuffer({
379
- label: "pool uniforms",
380
- size: 48,
381
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
382
- })
383
-
384
- // Create bind group
385
- this.bindGroup = this.device.createBindGroup({
386
- label: "pool bind group",
387
- layout: this.bindGroupLayout,
388
- entries: [
389
- {
390
- binding: 0,
391
- resource: {
392
- buffer: this.uniformBuffer,
393
- },
394
- },
395
- ],
396
- })
397
-
398
- // Create camera bind group
399
- this.cameraBindGroup = this.device.createBindGroup({
400
- label: "pool camera bind group",
401
- layout: this.cameraBindGroupLayout,
402
- entries: [
403
- {
404
- binding: 0,
405
- resource: {
406
- buffer: this.cameraUniformBuffer,
407
- },
408
- },
409
- ],
410
- })
411
- }
412
-
413
- public updateUniforms() {
414
- const time = (performance.now() - this.startTime) / 1000.0
415
- // WGSL uniform buffer layout (16-byte aligned):
416
- // offset 0: time (f32), poolY (f32), padding (8 bytes)
417
- // offset 16: seaColor (vec3f), padding (4 bytes)
418
- // offset 32: seaLight (vec3f), padding (4 bytes)
419
- const data = new Float32Array(12)
420
- data[0] = time
421
- data[1] = this.y
422
- // data[2-3] = padding (unused)
423
- data[4] = this.seaColor.x
424
- data[5] = this.seaColor.y
425
- data[6] = this.seaColor.z
426
- // data[7] = padding (unused)
427
- data[8] = this.seaLight.x
428
- data[9] = this.seaLight.y
429
- data[10] = this.seaLight.z
430
- // data[11] = padding (unused)
431
-
432
- this.device.queue.writeBuffer(this.uniformBuffer, 0, data)
433
- }
434
-
435
- public render(
436
- pass: GPURenderPassEncoder,
437
- restoreBuffers?: {
438
- vertexBuffer: GPUBuffer
439
- jointsBuffer: GPUBuffer
440
- weightsBuffer: GPUBuffer
441
- indexBuffer: GPUBuffer
442
- }
443
- ) {
444
- this.updateUniforms()
445
-
446
- // Set pool's pipeline and bind groups FIRST
447
- pass.setPipeline(this.pipeline)
448
- pass.setBindGroup(0, this.cameraBindGroup)
449
- pass.setBindGroup(1, this.bindGroup)
450
-
451
- // IMPORTANT: Set pool's own buffers AFTER setting pipeline
452
- // Pool only needs vertex buffer 0 (position + UV), but we must keep buffers 1 and 2 set
453
- // for subsequent model rendering (eyes, hair, etc.)
454
- pass.setVertexBuffer(0, this.vertexBuffer)
455
- // Explicitly keep model's buffers 1 and 2 set - pool pipeline doesn't use them but they must stay
456
- if (restoreBuffers) {
457
- pass.setVertexBuffer(1, restoreBuffers.jointsBuffer)
458
- pass.setVertexBuffer(2, restoreBuffers.weightsBuffer)
459
- }
460
-
461
- // Set pool's index buffer - this MUST be set to override model's index buffer
462
- pass.setIndexBuffer(this.indexBuffer, "uint32")
463
-
464
- // Draw all pool indices starting from 0
465
- // Parameters: indexCount, instanceCount, firstIndex, baseVertex, firstInstance
466
- // We always draw from index 0 with all indices
467
- pass.drawIndexed(this.indexCount, 1, 0, 0, 0)
468
-
469
- // Restore model's buffers for subsequent rendering (eyes, hair, etc.)
470
- // This ensures vertex buffer 0 and index buffer are restored to model's buffers
471
- if (restoreBuffers) {
472
- pass.setVertexBuffer(0, restoreBuffers.vertexBuffer)
473
- pass.setVertexBuffer(1, restoreBuffers.jointsBuffer)
474
- pass.setVertexBuffer(2, restoreBuffers.weightsBuffer)
475
- pass.setIndexBuffer(restoreBuffers.indexBuffer, "uint32")
476
- }
477
- }
478
-
479
- public dispose() {
480
- // Buffers will be cleaned up automatically when device is lost
481
- // But we could explicitly destroy them if needed
482
- }
483
- }