murow 0.0.73 → 0.1.3

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.
Files changed (188) hide show
  1. package/README.md +15 -1
  2. package/dist/cjs/core/binary-codec/binary-codec.js +1 -1
  3. package/dist/cjs/core/clock/clock.js +1 -0
  4. package/dist/cjs/core/clock/index.js +1 -0
  5. package/dist/cjs/core/driver/driver.js +1 -1
  6. package/dist/cjs/core/driver/drivers/immediate.js +1 -1
  7. package/dist/cjs/core/driver/drivers/raf.js +1 -1
  8. package/dist/cjs/core/driver/drivers/timeout.js +1 -1
  9. package/dist/cjs/core/hitbox/hitbox-library.js +1 -0
  10. package/dist/cjs/core/hitbox/hitbox.js +1 -0
  11. package/dist/cjs/core/hitbox/index.js +1 -0
  12. package/dist/cjs/core/hitbox/test.js +1 -0
  13. package/dist/cjs/core/index.js +1 -1
  14. package/dist/cjs/core/input/index.js +1 -1
  15. package/dist/cjs/core/input/mouse-look/index.js +1 -0
  16. package/dist/cjs/core/input/mouse-look/mouse-look.js +1 -0
  17. package/dist/cjs/core/input/scroll-zoom/index.js +1 -0
  18. package/dist/cjs/core/input/scroll-zoom/scroll-zoom.js +1 -0
  19. package/dist/cjs/core/prediction/prediction.js +1 -1
  20. package/dist/cjs/core/ray/ray-3d.js +1 -1
  21. package/dist/cjs/core/raycast/hit-buffer.js +1 -0
  22. package/dist/cjs/core/raycast/index.js +1 -0
  23. package/dist/cjs/core/raycast/raycaster.js +1 -0
  24. package/dist/cjs/core/slot-map/index.js +1 -0
  25. package/dist/cjs/core/slot-map/slot-map.js +1 -0
  26. package/dist/cjs/core/state-machine/index.js +1 -0
  27. package/dist/cjs/core/state-machine/state-machine.js +1 -0
  28. package/dist/cjs/core/timeline/index.js +1 -0
  29. package/dist/cjs/core/timeline/timeline.js +1 -0
  30. package/dist/cjs/ecs/component.js +1 -1
  31. package/dist/cjs/ecs/system-builder.js +1 -1
  32. package/dist/cjs/ecs/world.js +1 -1
  33. package/dist/cjs/game/loop/loop.js +1 -1
  34. package/dist/cjs/game/loop/ticker-schedule.js +1 -0
  35. package/dist/cjs/net/adapters/bun-websocket.js +1 -1
  36. package/dist/cjs/renderer/index.js +1 -1
  37. package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -1
  38. package/dist/cjs/renderer/prefab-bucket/index.js +1 -1
  39. package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -1
  40. package/dist/cjs/renderer/prefab-bucket/specs.js +1 -1
  41. package/dist/cjs/renderer/raycast/index.js +1 -0
  42. package/dist/cjs/renderer/raycast/raycast.js +1 -0
  43. package/dist/esm/core/binary-codec/binary-codec.js +1 -1
  44. package/dist/esm/core/clock/clock.js +1 -0
  45. package/dist/esm/core/clock/index.js +1 -0
  46. package/dist/esm/core/driver/drivers/immediate.js +1 -1
  47. package/dist/esm/core/driver/drivers/raf.js +1 -1
  48. package/dist/esm/core/driver/drivers/timeout.js +1 -1
  49. package/dist/esm/core/hitbox/hitbox-library.js +1 -0
  50. package/dist/esm/core/hitbox/hitbox.js +1 -0
  51. package/dist/esm/core/hitbox/index.js +1 -0
  52. package/dist/esm/core/hitbox/test.js +1 -0
  53. package/dist/esm/core/index.js +1 -1
  54. package/dist/esm/core/input/index.js +1 -1
  55. package/dist/esm/core/input/mouse-look/index.js +1 -0
  56. package/dist/esm/core/input/mouse-look/mouse-look.js +1 -0
  57. package/dist/esm/core/input/scroll-zoom/index.js +1 -0
  58. package/dist/esm/core/input/scroll-zoom/scroll-zoom.js +1 -0
  59. package/dist/esm/core/prediction/prediction.js +1 -1
  60. package/dist/esm/core/ray/ray-3d.js +1 -1
  61. package/dist/esm/core/raycast/hit-buffer.js +1 -0
  62. package/dist/esm/core/raycast/index.js +1 -0
  63. package/dist/esm/core/raycast/raycaster.js +1 -0
  64. package/dist/esm/core/slot-map/index.js +1 -0
  65. package/dist/esm/core/slot-map/slot-map.js +1 -0
  66. package/dist/esm/core/state-machine/index.js +1 -0
  67. package/dist/esm/core/state-machine/state-machine.js +1 -0
  68. package/dist/esm/core/timeline/index.js +1 -0
  69. package/dist/esm/core/timeline/timeline.js +1 -0
  70. package/dist/esm/ecs/component.js +1 -1
  71. package/dist/esm/ecs/system-builder.js +1 -1
  72. package/dist/esm/ecs/world.js +1 -1
  73. package/dist/esm/game/loop/loop.js +1 -1
  74. package/dist/esm/game/loop/ticker-schedule.js +1 -0
  75. package/dist/esm/net/adapters/bun-websocket.js +1 -1
  76. package/dist/esm/renderer/index.js +1 -1
  77. package/dist/esm/renderer/prefab-bucket/concrete.js +1 -1
  78. package/dist/esm/renderer/prefab-bucket/index.js +1 -1
  79. package/dist/esm/renderer/prefab-bucket/parsers.js +1 -1
  80. package/dist/esm/renderer/raycast/index.js +1 -0
  81. package/dist/esm/renderer/raycast/raycast.js +1 -0
  82. package/dist/netcode/cjs/index.js +1556 -0
  83. package/dist/netcode/esm/index.js +1534 -0
  84. package/dist/netcode/types/client/game-client.d.ts +139 -0
  85. package/dist/netcode/types/client/index.d.ts +1 -0
  86. package/dist/netcode/types/client/strategies/snapshot-interpolation.d.ts +33 -0
  87. package/dist/netcode/types/client/strategies/snapshot-interpolation.test.d.ts +1 -0
  88. package/dist/netcode/types/codec/delta-codec.d.ts +17 -0
  89. package/dist/netcode/types/codec/delta-codec.test.d.ts +1 -0
  90. package/dist/netcode/types/codec/index.d.ts +1 -0
  91. package/dist/netcode/types/components/index.d.ts +1 -0
  92. package/dist/netcode/types/components/sync-spec.d.ts +49 -0
  93. package/dist/netcode/types/components/sync-spec.test.d.ts +1 -0
  94. package/dist/netcode/types/ctx.d.ts +105 -0
  95. package/dist/netcode/types/ctx.test.d.ts +1 -0
  96. package/dist/netcode/types/handlers/define-handlers.d.ts +47 -0
  97. package/dist/netcode/types/handlers/index.d.ts +1 -0
  98. package/dist/netcode/types/index.d.ts +11 -0
  99. package/dist/netcode/types/integration.test.d.ts +1 -0
  100. package/dist/netcode/types/intents/define-intents.d.ts +53 -0
  101. package/dist/netcode/types/intents/define-intents.test.d.ts +1 -0
  102. package/dist/netcode/types/intents/index.d.ts +1 -0
  103. package/dist/netcode/types/network/base.d.ts +120 -0
  104. package/dist/netcode/types/network/index.d.ts +2 -0
  105. package/dist/netcode/types/network/transport.d.ts +1 -0
  106. package/dist/netcode/types/packets/convergence.test.d.ts +1 -0
  107. package/dist/netcode/types/packets/harness.d.ts +103 -0
  108. package/dist/netcode/types/packets/index.d.ts +2 -0
  109. package/dist/netcode/types/packets/intermittent-intents.test.d.ts +1 -0
  110. package/dist/netcode/types/packets/pathological.test.d.ts +1 -0
  111. package/dist/netcode/types/packets/peer-interpolation.test.d.ts +1 -0
  112. package/dist/netcode/types/packets/reordering.test.d.ts +1 -0
  113. package/dist/netcode/types/packets/virtual-network.d.ts +65 -0
  114. package/dist/netcode/types/predictions/define-predictions.d.ts +45 -0
  115. package/dist/netcode/types/predictions/define-predictions.test.d.ts +1 -0
  116. package/dist/netcode/types/predictions/index.d.ts +1 -0
  117. package/dist/netcode/types/reconciliation.test.d.ts +1 -0
  118. package/dist/netcode/types/rpcs/define-rpcs.d.ts +44 -0
  119. package/dist/netcode/types/rpcs/define-rpcs.test.d.ts +1 -0
  120. package/dist/netcode/types/rpcs/index.d.ts +1 -0
  121. package/dist/netcode/types/server/game-server.d.ts +77 -0
  122. package/dist/netcode/types/server/index.d.ts +2 -0
  123. package/dist/netcode/types/server/plugins/aoi-grid.d.ts +34 -0
  124. package/dist/netcode/types/server/plugins/index.d.ts +3 -0
  125. package/dist/netcode/types/server/plugins/lag-compensation.d.ts +34 -0
  126. package/dist/netcode/types/server/plugins/plugin.d.ts +24 -0
  127. package/dist/netcode/types/tick-rate.test.d.ts +1 -0
  128. package/dist/netcode/types/transports/index.d.ts +1 -0
  129. package/dist/netcode/types/transports/memory-transport.d.ts +51 -0
  130. package/dist/netcode/types/types.test.d.ts +1 -0
  131. package/dist/types/core/binary-codec/binary-codec.d.ts +89 -31
  132. package/dist/types/core/clock/clock.d.ts +37 -0
  133. package/dist/types/core/clock/index.d.ts +1 -0
  134. package/dist/types/core/driver/driver.d.ts +8 -8
  135. package/dist/types/core/driver/drivers/immediate.d.ts +4 -4
  136. package/dist/types/core/driver/drivers/raf.d.ts +6 -6
  137. package/dist/types/core/driver/drivers/timeout.d.ts +4 -4
  138. package/dist/types/core/hitbox/hitbox-library.d.ts +29 -0
  139. package/dist/types/core/hitbox/hitbox.d.ts +50 -0
  140. package/dist/types/core/hitbox/index.d.ts +3 -0
  141. package/dist/types/core/hitbox/test.d.ts +44 -0
  142. package/dist/types/core/index.d.ts +6 -0
  143. package/dist/types/core/input/index.d.ts +2 -0
  144. package/dist/types/core/input/mouse-look/index.d.ts +1 -0
  145. package/dist/types/core/input/mouse-look/mouse-look.d.ts +139 -0
  146. package/dist/types/core/input/scroll-zoom/index.d.ts +1 -0
  147. package/dist/types/core/input/scroll-zoom/scroll-zoom.d.ts +38 -0
  148. package/dist/types/core/prediction/prediction.d.ts +35 -58
  149. package/dist/types/core/ray/ray-3d.d.ts +21 -1
  150. package/dist/types/core/raycast/hit-buffer.d.ts +43 -0
  151. package/dist/types/core/raycast/index.d.ts +2 -0
  152. package/dist/types/core/raycast/raycaster.d.ts +54 -0
  153. package/dist/types/core/slot-map/index.d.ts +1 -0
  154. package/dist/types/core/slot-map/slot-map.d.ts +109 -0
  155. package/dist/types/core/state-machine/index.d.ts +1 -0
  156. package/dist/types/core/state-machine/state-machine.d.ts +114 -0
  157. package/dist/types/core/timeline/index.d.ts +1 -0
  158. package/dist/types/core/timeline/timeline.d.ts +34 -0
  159. package/dist/types/ecs/component.d.ts +67 -11
  160. package/dist/types/ecs/entity-handle.d.ts +5 -5
  161. package/dist/types/ecs/system-builder.d.ts +13 -0
  162. package/dist/types/ecs/world.d.ts +72 -4
  163. package/dist/types/game/loop/loop.d.ts +51 -2
  164. package/dist/types/game/loop/ticker-schedule.d.ts +52 -0
  165. package/dist/types/net/adapters/bun-websocket.d.ts +19 -3
  166. package/dist/types/renderer/index.d.ts +1 -0
  167. package/dist/types/renderer/prefab-bucket/concrete.d.ts +16 -6
  168. package/dist/types/renderer/prefab-bucket/index.d.ts +11 -7
  169. package/dist/types/renderer/prefab-bucket/specs.d.ts +10 -0
  170. package/dist/types/renderer/raycast/index.d.ts +1 -0
  171. package/dist/types/renderer/raycast/raycast.d.ts +24 -0
  172. package/dist/types/renderer/types.d.ts +1 -0
  173. package/dist/webgpu/cjs/index.js +1897 -592
  174. package/dist/webgpu/esm/index.js +1889 -578
  175. package/dist/webgpu/types/2d/raycast.d.ts +45 -0
  176. package/dist/webgpu/types/2d/renderer.d.ts +11 -0
  177. package/dist/webgpu/types/2d/sprite-accessor.d.ts +3 -1
  178. package/dist/webgpu/types/3d/hitbox.d.ts +32 -0
  179. package/dist/webgpu/types/3d/lights.d.ts +113 -0
  180. package/dist/webgpu/types/3d/lights.test.d.ts +1 -0
  181. package/dist/webgpu/types/3d/raycast.d.ts +44 -0
  182. package/dist/webgpu/types/3d/renderer.d.ts +88 -1
  183. package/dist/webgpu/types/3d/shader.d.ts +88 -5
  184. package/dist/webgpu/types/core/types.d.ts +55 -0
  185. package/dist/webgpu/types/geometry/geometry-builder.d.ts +1 -4
  186. package/dist/webgpu/types/index.d.ts +1 -0
  187. package/dist/webgpu/types/shaders/utils.d.ts +24 -0
  188. package/package.json +6 -1
@@ -128,8 +128,8 @@ var std = { ..._std };
128
128
  // src/2d/renderer.ts
129
129
  var import_typegpu6 = __toESM(require("typegpu"), 1);
130
130
  var d4 = __toESM(require("typegpu/data"), 1);
131
- var import_free_list2 = require("murow/core/free-list");
132
- var import_renderer2 = require("murow/renderer");
131
+ var import_free_list = require("murow/core/free-list");
132
+ var import_renderer3 = require("murow/renderer");
133
133
 
134
134
  // src/core/constants.ts
135
135
  var DYNAMIC_OFFSET_PREV_X = 0;
@@ -283,8 +283,46 @@ var MeshUniforms = d2.struct({
283
283
  alpha: d2.f32,
284
284
  lightDirX: d2.f32,
285
285
  lightDirY: d2.f32,
286
- lightDirZ: d2.f32
286
+ lightDirZ: d2.f32,
287
+ lightDirR: d2.f32,
288
+ lightDirG: d2.f32,
289
+ lightDirB: d2.f32,
290
+ lightDirIntensity: d2.f32,
291
+ ambientR: d2.f32,
292
+ ambientG: d2.f32,
293
+ ambientB: d2.f32,
294
+ lightCount: d2.u32
295
+ });
296
+ var MESH_UNIFORM_ALPHA_OFFSET = 16;
297
+ var MESH_UNIFORM_LIGHT_OFFSET = 17;
298
+ var MESH_UNIFORM_FLOATS = 28;
299
+ var Light = d2.struct({
300
+ kind: d2.f32,
301
+ currPosX: d2.f32,
302
+ currPosY: d2.f32,
303
+ currPosZ: d2.f32,
304
+ prevPosX: d2.f32,
305
+ prevPosY: d2.f32,
306
+ prevPosZ: d2.f32,
307
+ currDirX: d2.f32,
308
+ currDirY: d2.f32,
309
+ currDirZ: d2.f32,
310
+ prevDirX: d2.f32,
311
+ prevDirY: d2.f32,
312
+ prevDirZ: d2.f32,
313
+ colorR: d2.f32,
314
+ colorG: d2.f32,
315
+ colorB: d2.f32,
316
+ intensity: d2.f32,
317
+ range: d2.f32,
318
+ innerCos: d2.f32,
319
+ outerCos: d2.f32,
320
+ castsShadow: d2.f32,
321
+ shadowMapIndex: d2.f32
287
322
  });
323
+ var LIGHT_FLOATS = 22;
324
+ var LIGHT_KIND_POINT = 1;
325
+ var LIGHT_KIND_SPOT = 2;
288
326
  var InstanceAnimStateGPU = d2.struct({
289
327
  clipId: d2.i32,
290
328
  time: d2.f32,
@@ -307,15 +345,19 @@ var import_sparse_batcher = require("murow/core/sparse-batcher");
307
345
 
308
346
  // src/2d/sprite-accessor.ts
309
347
  var SpriteAccessor = class {
310
- constructor(dynamicData, staticData, slot, sheetId, onStaticDirty) {
348
+ constructor(dynamicData, staticData, id, slot, sheetId, onStaticDirty) {
311
349
  this.dynamicData = dynamicData;
312
350
  this.staticData = staticData;
351
+ this._id = id;
313
352
  this._slot = slot;
314
353
  this._sheetId = sheetId;
315
354
  this.dynamicBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
316
355
  this.staticBase = slot * STATIC_FLOATS_PER_SPRITE;
317
356
  this._onStaticDirty = onStaticDirty;
318
357
  }
358
+ get id() {
359
+ return this._id;
360
+ }
319
361
  get slot() {
320
362
  return this._slot;
321
363
  }
@@ -439,6 +481,88 @@ var SpriteAccessor = class {
439
481
  }
440
482
  };
441
483
 
484
+ // src/2d/raycast.ts
485
+ var import_raycast = require("murow/core/raycast");
486
+ var import_renderer = require("murow/renderer");
487
+ var WebGPURaycast2D = class extends import_renderer.Raycast {
488
+ constructor(renderer) {
489
+ super();
490
+ this.renderer = renderer;
491
+ this.state = new import_raycast.HitBuffer(2);
492
+ this.resultBuffer = [];
493
+ this.memos = /* @__PURE__ */ new Set();
494
+ }
495
+ update(input) {
496
+ this.state.reset();
497
+ this.renderer._collectRaycastHitsInto(input.mouse.position.x, input.mouse.position.y, this.state);
498
+ for (const m of this.memos)
499
+ m._invalidate();
500
+ }
501
+ /**
502
+ * Topmost sprite under the cursor, or null. The returned object is
503
+ * pool-backed and valid only until the next `update()` -- copy what
504
+ * you need, or use `memo` for results that persist across frames.
505
+ */
506
+ hit(opts) {
507
+ return this.state.nearest(opts?.filter, opts?.maxDistance ?? Infinity);
508
+ }
509
+ /**
510
+ * Every sprite under the cursor, topmost first. The array and its
511
+ * entries are reused and overwritten by the next `update()`.
512
+ */
513
+ hitAll(opts) {
514
+ this.state.collectInto(this.resultBuffer, opts?.filter, opts?.maxDistance ?? Infinity);
515
+ return this.resultBuffer;
516
+ }
517
+ memo(opts) {
518
+ const m = new WebGPURaycastMemo2D(this.state, opts, () => this.memos.delete(m));
519
+ this.memos.add(m);
520
+ return m;
521
+ }
522
+ clearMemos() {
523
+ for (const m of this.memos)
524
+ m._detach();
525
+ this.memos.clear();
526
+ }
527
+ };
528
+ var WebGPURaycastMemo2D = class extends import_renderer.RaycastMemo {
529
+ constructor(state, opts, onDispose) {
530
+ super();
531
+ this.state = state;
532
+ this.opts = opts;
533
+ this.onDispose = onDispose;
534
+ this.dirty = true;
535
+ this.detached = false;
536
+ this.cached = [];
537
+ }
538
+ get hits() {
539
+ if (this.detached)
540
+ return this.cached;
541
+ if (this.dirty) {
542
+ this.state.collectInto(this.cached, this.opts.filter, this.opts.maxDistance ?? Infinity);
543
+ this.dirty = false;
544
+ }
545
+ return this.cached;
546
+ }
547
+ get first() {
548
+ const arr = this.hits;
549
+ return arr.length > 0 ? arr[0] : null;
550
+ }
551
+ dispose() {
552
+ if (this.detached)
553
+ return;
554
+ this.onDispose();
555
+ this._detach();
556
+ }
557
+ _invalidate() {
558
+ this.dirty = true;
559
+ }
560
+ _detach() {
561
+ this.detached = true;
562
+ this.cached.length = 0;
563
+ }
564
+ };
565
+
442
566
  // src/camera/camera-2d.ts
443
567
  var import_lerp = require("murow/core/lerp");
444
568
  var Camera2D = class {
@@ -648,7 +772,7 @@ function createSpriteFragment(_spriteLayout, textureLayout) {
648
772
  }
649
773
 
650
774
  // src/spritesheet/spritesheet.ts
651
- var import_renderer = require("murow/renderer");
775
+ var import_renderer2 = require("murow/renderer");
652
776
  var Spritesheet = class {
653
777
  constructor(id, texture, textureView, sampler, uvs, width, height) {
654
778
  this.id = id;
@@ -688,7 +812,7 @@ function createTextureFromBitmap(device, bitmap) {
688
812
  }
689
813
 
690
814
  // src/2d/renderer.ts
691
- var import_renderer3 = require("murow/renderer");
815
+ var import_renderer4 = require("murow/renderer");
692
816
 
693
817
  // src/geometry/geometry-builder.ts
694
818
  var import_typegpu2 = __toESM(require("typegpu"), 1);
@@ -1201,7 +1325,7 @@ function resolveBuiltInGeometry(name) {
1201
1325
  }
1202
1326
 
1203
1327
  // src/geometry/geometry-builder.ts
1204
- var import_free_list = require("murow/core/free-list");
1328
+ var import_slot_map = require("murow/core/slot-map");
1205
1329
 
1206
1330
  // src/shaders/runtime-transpile.ts
1207
1331
  var acorn = __toESM(require("acorn"), 1);
@@ -1379,7 +1503,6 @@ var CustomGeometry = class {
1379
1503
  this._uniformDirty = true;
1380
1504
  this._isComputeSourced = false;
1381
1505
  this._drawCount = 0;
1382
- this.activeCount = 0;
1383
1506
  this.name = name;
1384
1507
  this.root = root;
1385
1508
  this.maxInstances = maxInstances;
@@ -1393,7 +1516,7 @@ var CustomGeometry = class {
1393
1516
  this.staticFloatsPerInstance = 0;
1394
1517
  for (const n of this.staticFieldNames)
1395
1518
  this.staticFloatsPerInstance += getFieldFloats(layoutConfig.static[n]);
1396
- this.freeList = new import_free_list.FreeList(maxInstances);
1519
+ this.slots = new import_slot_map.SlotMap(maxInstances);
1397
1520
  this.dynamicData = new Float32Array(maxInstances * this.dynamicFloatsPerInstance);
1398
1521
  this.staticData = new Float32Array(maxInstances * this.staticFloatsPerInstance);
1399
1522
  this.uniformValues = { ...uniformValues };
@@ -1404,8 +1527,6 @@ var CustomGeometry = class {
1404
1527
  this.dataBindGroup = dataBindGroup;
1405
1528
  this.canvas = canvas;
1406
1529
  this.clearColor = clearColor;
1407
- this.activeSlots = new Uint32Array(maxInstances);
1408
- this.slotToActive = new Int32Array(maxInstances).fill(-1);
1409
1530
  this._ctx = new InstanceContext();
1410
1531
  this._isComputeSourced = isComputeSourced;
1411
1532
  if (isComputeSourced)
@@ -1419,34 +1540,20 @@ var CustomGeometry = class {
1419
1540
  this._drawCount = count;
1420
1541
  }
1421
1542
  addInstance(data) {
1422
- const slot = this.freeList.allocate();
1543
+ const slot = this.slots.add();
1423
1544
  if (slot === -1)
1424
1545
  throw new Error(`Max instances (${this.maxInstances}) reached for "${this.name}"`);
1425
1546
  this.setInstanceData(slot, data);
1426
- this.activeSlots[this.activeCount] = slot;
1427
- this.slotToActive[slot] = this.activeCount;
1428
- this.activeCount++;
1429
1547
  return slot;
1430
1548
  }
1431
1549
  removeInstance(slot) {
1432
- this.freeList.free(slot);
1433
1550
  const dynBase = slot * this.dynamicFloatsPerInstance;
1434
1551
  const statBase = slot * this.staticFloatsPerInstance;
1435
1552
  this.dynamicData.fill(0, dynBase, dynBase + this.dynamicFloatsPerInstance);
1436
1553
  this.staticData.fill(0, statBase, statBase + this.staticFloatsPerInstance);
1437
1554
  this._dynamicDirty = true;
1438
1555
  this._staticDirty = true;
1439
- const activeIdx = this.slotToActive[slot];
1440
- if (activeIdx !== -1) {
1441
- const lastIdx = this.activeCount - 1;
1442
- if (activeIdx !== lastIdx) {
1443
- const lastSlot = this.activeSlots[lastIdx];
1444
- this.activeSlots[activeIdx] = lastSlot;
1445
- this.slotToActive[lastSlot] = activeIdx;
1446
- }
1447
- this.slotToActive[slot] = -1;
1448
- this.activeCount--;
1449
- }
1556
+ this.slots.remove(slot);
1450
1557
  }
1451
1558
  setInstanceData(slot, data) {
1452
1559
  const rawData = data;
@@ -1519,7 +1626,7 @@ var CustomGeometry = class {
1519
1626
  this._uniformDirty = true;
1520
1627
  }
1521
1628
  getActiveCount() {
1522
- return this.freeList.getAllocatedCount();
1629
+ return this.slots.size;
1523
1630
  }
1524
1631
  updateAll(callback) {
1525
1632
  this._ctx._bind(
@@ -1531,8 +1638,8 @@ var CustomGeometry = class {
1531
1638
  this.staticFieldNames,
1532
1639
  this.layoutConfig
1533
1640
  );
1534
- const count = this.activeCount;
1535
- const slots = this.activeSlots;
1641
+ const count = this.slots.size;
1642
+ const slots = this.slots.activeSlots;
1536
1643
  for (let i = 0; i < count; i++) {
1537
1644
  const slot = slots[i];
1538
1645
  this._ctx._setSlot(slot);
@@ -1543,7 +1650,7 @@ var CustomGeometry = class {
1543
1650
  }
1544
1651
  render() {
1545
1652
  const device = this.root.device;
1546
- const count = this._isComputeSourced ? this._drawCount : this.freeList.getAllocatedCount();
1653
+ const count = this._isComputeSourced ? this._drawCount : this.slots.size;
1547
1654
  if (count === 0)
1548
1655
  return;
1549
1656
  const context = this.canvas.getContext("webgpu");
@@ -2168,6 +2275,7 @@ var ComputeBuilder = class {
2168
2275
  };
2169
2276
 
2170
2277
  // src/2d/renderer.ts
2278
+ var import_hitbox = require("murow/core/hitbox");
2171
2279
  var prefab2DHandles = /* @__PURE__ */ new WeakMap();
2172
2280
  function isPrefab2D(value) {
2173
2281
  return value.type === "spritesheet";
@@ -2181,7 +2289,7 @@ function resolveSpritePrefabHandle(prefab) {
2181
2289
  }
2182
2290
  return h;
2183
2291
  }
2184
- var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
2292
+ var WebGPU2DRenderer = class extends import_renderer3.Base2DRenderer {
2185
2293
  constructor(canvas, options) {
2186
2294
  const resolvedMaxSprites = options.maxSprites ?? options.maxInstances ?? 1024;
2187
2295
  super(canvas, { ...options, maxSprites: resolvedMaxSprites });
@@ -2191,15 +2299,19 @@ var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
2191
2299
  this.sheets = /* @__PURE__ */ new Map();
2192
2300
  this.nextSheetId = 0;
2193
2301
  this.uniformData = new Float32Array(20);
2302
+ this.nextSpriteId = 0;
2194
2303
  this.resizeObserver = null;
2195
2304
  this.resizeCallbacks = [];
2196
2305
  this._prefabs = options.prefabs ?? null;
2197
2306
  this.camera = new Camera2D(canvas.width || 800, canvas.height || 600);
2198
- this.freeList = new import_free_list2.FreeList(resolvedMaxSprites);
2307
+ this.raycast = new WebGPURaycast2D(this);
2308
+ this.freeList = new import_free_list.FreeList(resolvedMaxSprites);
2199
2309
  this.batcher = new import_sparse_batcher.SparseBatcher(resolvedMaxSprites);
2200
2310
  this.dynamicData = new Float32Array(resolvedMaxSprites * DYNAMIC_FLOATS_PER_SPRITE);
2201
2311
  this.staticData = new Float32Array(resolvedMaxSprites * STATIC_FLOATS_PER_SPRITE);
2202
2312
  this.slotIndexData = new Uint32Array(resolvedMaxSprites);
2313
+ this.spriteHandles = new Array(resolvedMaxSprites).fill(null);
2314
+ this.spriteHitboxes = new Array(resolvedMaxSprites).fill(null);
2203
2315
  }
2204
2316
  get device() {
2205
2317
  return this._device;
@@ -2325,7 +2437,7 @@ var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
2325
2437
  this.resizeCallbacks.push(callback);
2326
2438
  }
2327
2439
  async loadSpritesheet(source) {
2328
- const parsed = await (0, import_renderer3.parseSpritesheet)(source);
2440
+ const parsed = await (0, import_renderer4.parseSpritesheet)(source);
2329
2441
  return this.uploadParsedSpritesheet(parsed);
2330
2442
  }
2331
2443
  /**
@@ -2359,6 +2471,9 @@ var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
2359
2471
  const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
2360
2472
  const statBase = slot * STATIC_FLOATS_PER_SPRITE;
2361
2473
  const sheet = isPrefab2D(opts.sheet) ? resolveSpritePrefabHandle(opts.sheet) : opts.sheet;
2474
+ const hitboxName = isPrefab2D(opts.sheet) ? opts.sheet.hitbox : void 0;
2475
+ const lib = this._prefabs?.hitboxLibrary ?? null;
2476
+ const hitbox = hitboxName && lib ? lib.get(hitboxName) : null;
2362
2477
  const [px, py] = opts.position ?? [0, 0];
2363
2478
  this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] = px;
2364
2479
  this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] = py;
@@ -2387,15 +2502,19 @@ var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
2387
2502
  this.staticData[statBase + STATIC_OFFSET_TINT_A] = tint[3];
2388
2503
  this.staticDirty = true;
2389
2504
  this.batcher.add(opts.layer ?? 0, sheet.id, slot);
2390
- return new SpriteAccessor(
2505
+ const accessor = new SpriteAccessor(
2391
2506
  this.dynamicData,
2392
2507
  this.staticData,
2508
+ this.nextSpriteId++,
2393
2509
  slot,
2394
2510
  sheet.id,
2395
2511
  () => {
2396
2512
  this.staticDirty = true;
2397
2513
  }
2398
2514
  );
2515
+ this.spriteHandles[slot] = accessor;
2516
+ this.spriteHitboxes[slot] = hitbox;
2517
+ return accessor;
2399
2518
  }
2400
2519
  removeSprite(sprite) {
2401
2520
  const accessor = sprite;
@@ -2405,8 +2524,47 @@ var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
2405
2524
  const statBase = accessor.slot * STATIC_FLOATS_PER_SPRITE;
2406
2525
  this.dynamicData.fill(0, dynBase, dynBase + DYNAMIC_FLOATS_PER_SPRITE);
2407
2526
  this.staticData.fill(0, statBase, statBase + STATIC_FLOATS_PER_SPRITE);
2527
+ this.spriteHandles[accessor.slot] = null;
2528
+ this.spriteHitboxes[accessor.slot] = void 0;
2408
2529
  this.staticDirty = true;
2409
2530
  }
2531
+ /**
2532
+ * Point-test every sprite against the unprojected cursor and push the
2533
+ * hits into the buffer. Sort key is `-layer` so the topmost sprite is
2534
+ * "nearest". A declared hitbox overrides the default rendered quad.
2535
+ */
2536
+ _collectRaycastHitsInto(screenX, screenY, rc) {
2537
+ const [wx, wy] = this.camera.screenToWorld(screenX, screenY);
2538
+ const dyn = this.dynamicData;
2539
+ const stat = this.staticData;
2540
+ this.batcher.each((_sheetId, instances, count) => {
2541
+ for (let i = 0; i < count; i++) {
2542
+ const slot = instances[i];
2543
+ const handle = this.spriteHandles[slot];
2544
+ if (handle === null)
2545
+ continue;
2546
+ const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
2547
+ const statBase = slot * STATIC_FLOATS_PER_SPRITE;
2548
+ const cx = dyn[dynBase + DYNAMIC_OFFSET_CURR_X];
2549
+ const cy = dyn[dynBase + DYNAMIC_OFFSET_CURR_Y];
2550
+ const rot = dyn[dynBase + DYNAMIC_OFFSET_CURR_ROTATION];
2551
+ const sx = stat[statBase + STATIC_OFFSET_SCALE_X];
2552
+ const sy = stat[statBase + STATIC_OFFSET_SCALE_Y];
2553
+ const layer = stat[statBase + STATIC_OFFSET_LAYER];
2554
+ const hb = this.spriteHitboxes[slot];
2555
+ let part = null;
2556
+ if (hb) {
2557
+ const hit = (0, import_hitbox.testHitbox2D)(hb, cx, cy, sx, sy, rot, wx, wy);
2558
+ if (!hit)
2559
+ continue;
2560
+ part = hit.part;
2561
+ } else if (!(0, import_hitbox.pointInQuad2D)(cx, cy, sx, sy, rot, wx, wy)) {
2562
+ continue;
2563
+ }
2564
+ rc.push(handle, -layer, wx, wy, 0, layer, part);
2565
+ }
2566
+ });
2567
+ }
2410
2568
  storePreviousState() {
2411
2569
  this.camera.storePrevious();
2412
2570
  const dyn = this.dynamicData;
@@ -2617,311 +2775,127 @@ var AnimationController = class {
2617
2775
  };
2618
2776
 
2619
2777
  // src/3d/renderer.ts
2620
- var import_typegpu9 = __toESM(require("typegpu"), 1);
2621
- var import_renderer4 = require("murow/renderer");
2622
- var import_free_list3 = require("murow/core/free-list");
2778
+ var import_typegpu10 = __toESM(require("typegpu"), 1);
2779
+ var import_renderer6 = require("murow/renderer");
2780
+ var import_free_list2 = require("murow/core/free-list");
2623
2781
  var import_sparse_batcher2 = require("murow/core/sparse-batcher");
2624
2782
 
2625
- // src/camera/camera-3d.ts
2626
- var import_lerp2 = require("murow/core/lerp");
2627
- var import_ray = require("murow/core/ray");
2628
- var Camera3D = class {
2629
- constructor() {
2630
- this.position = [0, 5, -10];
2631
- this.target = [0, 0, 0];
2632
- this.up = [0, 1, 0];
2633
- this.fov = 60;
2634
- this.near = 0.1;
2635
- this.far = 1e3;
2636
- this.aspect = 1;
2637
- this.movement = "local";
2638
- // Previous state for interpolation (stored before each tick)
2639
- this._prevPosition = [0, 5, -10];
2640
- this._prevTarget = [0, 0, 0];
2641
- // Interpolated state used for rendering
2642
- this._renderPosition = [0, 5, -10];
2643
- this._renderTarget = [0, 0, 0];
2644
- this._viewMatrix = new Float32Array(16);
2645
- this._projMatrix = new Float32Array(16);
2646
- this._vpMatrix = new Float32Array(16);
2647
- this._ray = new import_ray.Ray3D();
2648
- this._width = 1;
2649
- this._height = 1;
2650
- }
2651
- /**
2652
- * Store current position/target as previous. Call before each tick.
2653
- */
2654
- storePrevious() {
2655
- this._prevPosition[0] = this.position[0];
2656
- this._prevPosition[1] = this.position[1];
2657
- this._prevPosition[2] = this.position[2];
2658
- this._prevTarget[0] = this.target[0];
2659
- this._prevTarget[1] = this.target[1];
2660
- this._prevTarget[2] = this.target[2];
2661
- }
2662
- /**
2663
- * Smoothly move the camera toward a target point.
2664
- * Call each tick. The camera and its look-at target lerp toward the given position.
2665
- * @param targetX World X to follow
2666
- * @param targetY World Y to follow
2667
- * @param targetZ World Z to follow
2668
- * @param smoothing 0-1. 1 = snap instantly, 0.1 = lazy follow. Default 1.
2669
- */
2670
- follow(targetX, targetY, targetZ, smoothing = 1) {
2671
- const dx = targetX - this.target[0];
2672
- const dy = targetY - this.target[1];
2673
- const dz = targetZ - this.target[2];
2674
- const mx = dx * smoothing;
2675
- const my = dy * smoothing;
2676
- const mz = dz * smoothing;
2677
- this.target[0] += mx;
2678
- this.target[1] += my;
2679
- this.target[2] += mz;
2680
- this.position[0] += mx;
2681
- this.position[1] += my;
2682
- this.position[2] += mz;
2683
- }
2684
- /**
2685
- * Interpolate between previous and current state. Call before rendering.
2686
- */
2687
- interpolate(alpha) {
2688
- this._renderPosition[0] = (0, import_lerp2.lerp)(this._prevPosition[0], this.position[0], alpha);
2689
- this._renderPosition[1] = (0, import_lerp2.lerp)(this._prevPosition[1], this.position[1], alpha);
2690
- this._renderPosition[2] = (0, import_lerp2.lerp)(this._prevPosition[2], this.position[2], alpha);
2691
- this._renderTarget[0] = (0, import_lerp2.lerp)(this._prevTarget[0], this.target[0], alpha);
2692
- this._renderTarget[1] = (0, import_lerp2.lerp)(this._prevTarget[1], this.target[1], alpha);
2693
- this._renderTarget[2] = (0, import_lerp2.lerp)(this._prevTarget[2], this.target[2], alpha);
2694
- }
2695
- /**
2696
- * Build the view matrix (lookAt) using interpolated state.
2697
- */
2698
- getViewMatrix() {
2699
- lookAt(this._viewMatrix, this._renderPosition, this._renderTarget, this.up);
2700
- return this._viewMatrix;
2701
- }
2702
- /**
2703
- * Build the perspective projection matrix.
2704
- */
2705
- getProjectionMatrix() {
2706
- perspective(this._projMatrix, this.fov * (Math.PI / 180), this.aspect, this.near, this.far);
2707
- return this._projMatrix;
2783
+ // src/3d/shader.ts
2784
+ var import_typegpu8 = __toESM(require("typegpu"), 1);
2785
+ var d6 = __toESM(require("typegpu/data"), 1);
2786
+ var std4 = __toESM(require("typegpu/std"), 1);
2787
+
2788
+ // src/shaders/utils.ts
2789
+ var import_typegpu7 = __toESM(require("typegpu"), 1);
2790
+ var d5 = __toESM(require("typegpu/data"), 1);
2791
+ var std3 = __toESM(require("typegpu/std"), 1);
2792
+ var rotate2d = import_typegpu7.default.fn([d5.vec2f, d5.f32], d5.vec2f)(
2793
+ function rotate2d2(point, angle) {
2794
+ "use gpu";
2795
+ const c = std3.cos(angle);
2796
+ const s = std3.sin(angle);
2797
+ return d5.vec2f(
2798
+ point.x * c - point.y * s,
2799
+ point.x * s + point.y * c
2800
+ );
2708
2801
  }
2709
- /**
2710
- * Build the combined view-projection matrix.
2711
- */
2712
- getViewProjectionMatrix() {
2713
- this.getViewMatrix();
2714
- this.getProjectionMatrix();
2715
- mat4Multiply(this._vpMatrix, this._projMatrix, this._viewMatrix);
2716
- return this._vpMatrix;
2802
+ );
2803
+ var worldToClip2d = import_typegpu7.default.fn([d5.vec2f, d5.mat3x3f], d5.vec4f)(
2804
+ function worldToClip2d2(worldPos, cameraMatrix) {
2805
+ "use gpu";
2806
+ const clip = cameraMatrix * d5.vec3f(worldPos.x, worldPos.y, 1);
2807
+ return d5.vec4f(clip.x, clip.y, 0, 1);
2717
2808
  }
2718
- setAspect(width, height) {
2719
- this.aspect = width / height;
2720
- this._width = width;
2721
- this._height = height;
2809
+ );
2810
+ var worldToClip3d = import_typegpu7.default.fn([d5.vec3f, d5.mat4x4f], d5.vec4f)(
2811
+ function worldToClip3d2(worldPos, vpMatrix) {
2812
+ "use gpu";
2813
+ return vpMatrix * d5.vec4f(worldPos.x, worldPos.y, worldPos.z, 1);
2722
2814
  }
2723
- setPosition(x, y, z) {
2724
- this.position[0] = x;
2725
- this.position[1] = y;
2726
- this.position[2] = z;
2815
+ );
2816
+ var remap = import_typegpu7.default.fn([d5.f32, d5.f32, d5.f32, d5.f32, d5.f32], d5.f32)(
2817
+ function remap2(value, inMin, inMax, outMin, outMax) {
2818
+ "use gpu";
2819
+ const t = (value - inMin) / (inMax - inMin);
2820
+ return outMin + t * (outMax - outMin);
2727
2821
  }
2728
- /**
2729
- * Unproject a screen coordinate into a world-space ray.
2730
- * Requires `setAspect(width, height)` to have been called first.
2731
- * Returns a pre-allocated Ray3D — copy origin/direction if you need to store it.
2732
- */
2733
- screenToRay(screenX, screenY) {
2734
- this.getViewMatrix();
2735
- const m = this._viewMatrix;
2736
- const ndcX = 2 * screenX / this._width - 1;
2737
- const ndcY = 1 - 2 * screenY / this._height;
2738
- const t = Math.tan(this.fov * Math.PI / 180 * 0.5);
2739
- const rightX = m[0], rightY = m[4], rightZ = m[8];
2740
- const upX = m[1], upY = m[5], upZ = m[9];
2741
- const fwdX = -m[2], fwdY = -m[6], fwdZ = -m[10];
2742
- const dx = fwdX + rightX * ndcX * t * this.aspect + upX * ndcY * t;
2743
- const dy = fwdY + rightY * ndcX * t * this.aspect + upY * ndcY * t;
2744
- const dz = fwdZ + rightZ * ndcX * t * this.aspect + upZ * ndcY * t;
2745
- this._ray.set(this.position[0], this.position[1], this.position[2], dx, dy, dz);
2746
- return this._ray;
2822
+ );
2823
+ var scaleRotate2d = import_typegpu7.default.fn([d5.vec2f, d5.f32], d5.mat2x2f)(
2824
+ function scaleRotate2d2(scale, angle) {
2825
+ "use gpu";
2826
+ const c = std3.cos(angle);
2827
+ const s = std3.sin(angle);
2828
+ return d5.mat2x2f(
2829
+ scale.x * c,
2830
+ scale.x * s,
2831
+ -(scale.y * s),
2832
+ scale.y * c
2833
+ );
2747
2834
  }
2748
- setTarget(x, y, z) {
2749
- this.target[0] = x;
2750
- this.target[1] = y;
2751
- this.target[2] = z;
2835
+ );
2836
+ var inverseLerp = import_typegpu7.default.fn([d5.f32, d5.f32, d5.f32], d5.f32)(
2837
+ function inverseLerp2(min, max3, value) {
2838
+ "use gpu";
2839
+ return std3.saturate((value - min) / (max3 - min));
2752
2840
  }
2753
- /**
2754
- * Move the camera. Behaviour is determined by the `movement` property:
2755
- * - `'local'` — along camera axes, pitch included (free-fly / spectator)
2756
- * - `'grounded'` — yaw-projected XZ + world Y (FPS)
2757
- * - `'global'` — world axes directly (isometric / platformer)
2758
- */
2759
- move(right, up, forward) {
2760
- if (this.movement === "grounded")
2761
- this._moveGrounded(right, up, forward);
2762
- else if (this.movement === "global")
2763
- this._moveGlobal(right, up, forward);
2764
- else
2765
- this._moveLocal(right, up, forward);
2841
+ );
2842
+ var lightContribution = import_typegpu7.default.fn(
2843
+ [d5.vec3f, d5.vec3f, d5.vec3f, d5.vec4f, d5.vec3f, d5.vec3f],
2844
+ d5.vec3f
2845
+ )(
2846
+ function lightContribution2(pos, axis, color, params, normal, worldPos) {
2847
+ "use gpu";
2848
+ const intensity = params.x;
2849
+ const range = params.y;
2850
+ const innerCos = params.z;
2851
+ const outerCos = params.w;
2852
+ const toLight = d5.vec3f(pos.x - worldPos.x, pos.y - worldPos.y, pos.z - worldPos.z);
2853
+ const dist = std3.length(toLight);
2854
+ const dir = std3.normalize(toLight);
2855
+ const lambert = std3.max(std3.dot(normal, dir), 0);
2856
+ const atten = std3.saturate(1 - dist / std3.max(range, 1e-4));
2857
+ const falloff = atten * atten;
2858
+ const axisLen = std3.length(axis);
2859
+ const isSpot = axisLen > 1e-4;
2860
+ const safeAxis = std3.select(d5.vec3f(0, 0, 1), axis, isSpot);
2861
+ const cosAngle = std3.dot(std3.normalize(safeAxis), d5.vec3f(-dir.x, -dir.y, -dir.z));
2862
+ const spotCone = std3.smoothstep(outerCos, innerCos, cosAngle);
2863
+ const cone = std3.select(1, spotCone, isSpot);
2864
+ const shadowFactor = 1;
2865
+ const scale = lambert * falloff * cone * intensity * shadowFactor;
2866
+ return d5.vec3f(color.x * scale, color.y * scale, color.z * scale);
2766
2867
  }
2767
- _moveLocal(right, up, forward) {
2768
- this.getViewMatrix();
2769
- const m = this._viewMatrix;
2770
- const dx = m[0] * right + m[1] * up - m[2] * forward;
2771
- const dy = m[4] * right + m[5] * up - m[6] * forward;
2772
- const dz = m[8] * right + m[9] * up - m[10] * forward;
2773
- this.position[0] += dx;
2774
- this.position[1] += dy;
2775
- this.position[2] += dz;
2776
- this.target[0] += dx;
2777
- this.target[1] += dy;
2778
- this.target[2] += dz;
2868
+ );
2869
+ var tonemap = import_typegpu7.default.fn([d5.vec3f], d5.vec3f)(
2870
+ function tonemap2(c) {
2871
+ "use gpu";
2872
+ return d5.vec3f(c.x / (1 + c.x), c.y / (1 + c.y), c.z / (1 + c.z));
2779
2873
  }
2780
- _moveGrounded(right, up, forward) {
2781
- const yaw = Math.atan2(this.target[0] - this.position[0], this.target[2] - this.position[2]);
2782
- const dx = Math.sin(yaw) * forward + Math.cos(yaw) * right;
2783
- const dz = Math.cos(yaw) * forward - Math.sin(yaw) * right;
2784
- this.position[0] += dx;
2785
- this.position[1] += up;
2786
- this.position[2] += dz;
2787
- this.target[0] += dx;
2788
- this.target[1] += up;
2789
- this.target[2] += dz;
2790
- }
2791
- _moveGlobal(right, up, forward) {
2792
- this.position[0] += right;
2793
- this.position[1] += up;
2794
- this.position[2] += forward;
2795
- this.target[0] += right;
2796
- this.target[1] += up;
2797
- this.target[2] += forward;
2798
- }
2799
- /**
2800
- * Orbit around the target point. Zero allocations.
2801
- * @param yawDelta Horizontal rotation in radians (positive = rotate right)
2802
- * @param pitchDelta Vertical rotation in radians (positive = rotate up)
2803
- */
2804
- orbit(yawDelta, pitchDelta) {
2805
- let ox = this.position[0] - this.target[0];
2806
- let oy = this.position[1] - this.target[1];
2807
- let oz = this.position[2] - this.target[2];
2808
- const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
2809
- let yaw = Math.atan2(ox, oz);
2810
- let pitch = Math.asin(oy / dist);
2811
- yaw += yawDelta;
2812
- pitch += pitchDelta;
2813
- pitch = Math.max(-Math.PI * 0.49, Math.min(Math.PI * 0.49, pitch));
2814
- this.position[0] = this.target[0] + Math.sin(yaw) * Math.cos(pitch) * dist;
2815
- this.position[1] = this.target[1] + Math.sin(pitch) * dist;
2816
- this.position[2] = this.target[2] + Math.cos(yaw) * Math.cos(pitch) * dist;
2817
- }
2818
- /**
2819
- * Zoom by adjusting distance to target. Zero allocations.
2820
- * @param delta Positive = zoom in, negative = zoom out
2821
- */
2822
- zoom(delta) {
2823
- let ox = this.position[0] - this.target[0];
2824
- let oy = this.position[1] - this.target[1];
2825
- let oz = this.position[2] - this.target[2];
2826
- const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
2827
- const newDist = Math.max(0.1, dist - delta);
2828
- const scale = newDist / dist;
2829
- this.position[0] = this.target[0] + ox * scale;
2830
- this.position[1] = this.target[1] + oy * scale;
2831
- this.position[2] = this.target[2] + oz * scale;
2832
- }
2833
- };
2834
- function lookAt(out, eye, center, up) {
2835
- let fx = center[0] - eye[0];
2836
- let fy = center[1] - eye[1];
2837
- let fz = center[2] - eye[2];
2838
- let len = 1 / Math.sqrt(fx * fx + fy * fy + fz * fz);
2839
- fx *= len;
2840
- fy *= len;
2841
- fz *= len;
2842
- let sx = fy * up[2] - fz * up[1];
2843
- let sy = fz * up[0] - fx * up[2];
2844
- let sz = fx * up[1] - fy * up[0];
2845
- len = Math.sqrt(sx * sx + sy * sy + sz * sz);
2846
- if (len > 0) {
2847
- len = 1 / len;
2848
- sx *= len;
2849
- sy *= len;
2850
- sz *= len;
2851
- }
2852
- const ux = sy * fz - sz * fy;
2853
- const uy = sz * fx - sx * fz;
2854
- const uz = sx * fy - sy * fx;
2855
- out[0] = sx;
2856
- out[1] = ux;
2857
- out[2] = -fx;
2858
- out[3] = 0;
2859
- out[4] = sy;
2860
- out[5] = uy;
2861
- out[6] = -fy;
2862
- out[7] = 0;
2863
- out[8] = sz;
2864
- out[9] = uz;
2865
- out[10] = -fz;
2866
- out[11] = 0;
2867
- out[12] = -(sx * eye[0] + sy * eye[1] + sz * eye[2]);
2868
- out[13] = -(ux * eye[0] + uy * eye[1] + uz * eye[2]);
2869
- out[14] = fx * eye[0] + fy * eye[1] + fz * eye[2];
2870
- out[15] = 1;
2871
- }
2872
- function perspective(out, fovRad, aspect, near, far) {
2873
- const f = 1 / Math.tan(fovRad * 0.5);
2874
- const rangeInv = 1 / (near - far);
2875
- out[0] = f / aspect;
2876
- out[1] = 0;
2877
- out[2] = 0;
2878
- out[3] = 0;
2879
- out[4] = 0;
2880
- out[5] = f;
2881
- out[6] = 0;
2882
- out[7] = 0;
2883
- out[8] = 0;
2884
- out[9] = 0;
2885
- out[10] = (near + far) * rangeInv;
2886
- out[11] = -1;
2887
- out[12] = 0;
2888
- out[13] = 0;
2889
- out[14] = 2 * near * far * rangeInv;
2890
- out[15] = 0;
2891
- }
2892
- function mat4Multiply(out, a, b) {
2893
- for (let i = 0; i < 4; i++) {
2894
- const ai0 = a[i], ai1 = a[i + 4], ai2 = a[i + 8], ai3 = a[i + 12];
2895
- out[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3];
2896
- out[i + 4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7];
2897
- out[i + 8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11];
2898
- out[i + 12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15];
2899
- }
2900
- }
2874
+ );
2901
2875
 
2902
2876
  // src/3d/shader.ts
2903
- var import_typegpu7 = __toESM(require("typegpu"), 1);
2904
- var d5 = __toESM(require("typegpu/data"), 1);
2905
- var std3 = __toESM(require("typegpu/std"), 1);
2877
+ var MAX_LIGHTS = 64;
2906
2878
  function createMeshLayout(maxInstances) {
2907
- return import_typegpu7.default.bindGroupLayout({
2879
+ return import_typegpu8.default.bindGroupLayout({
2908
2880
  uniforms: { uniform: MeshUniforms },
2909
- dynamicInstances: { storage: d5.arrayOf(DynamicMesh, maxInstances) },
2910
- staticInstances: { storage: d5.arrayOf(StaticMesh, maxInstances) },
2911
- slotIndices: { storage: d5.arrayOf(d5.u32, maxInstances) }
2881
+ dynamicInstances: { storage: d6.arrayOf(DynamicMesh, maxInstances) },
2882
+ staticInstances: { storage: d6.arrayOf(StaticMesh, maxInstances) },
2883
+ slotIndices: { storage: d6.arrayOf(d6.u32, maxInstances) },
2884
+ lights: { storage: d6.arrayOf(Light, MAX_LIGHTS) }
2912
2885
  });
2913
2886
  }
2914
2887
  function createMeshVertex(meshLayout) {
2915
- return import_typegpu7.default.vertexFn({
2888
+ return import_typegpu8.default.vertexFn({
2916
2889
  in: {
2917
- position: d5.location(0, d5.vec3f),
2918
- normal: d5.location(1, d5.vec3f),
2919
- instanceIndex: d5.builtin.instanceIndex
2890
+ position: d6.location(0, d6.vec3f),
2891
+ normal: d6.location(1, d6.vec3f),
2892
+ instanceIndex: d6.builtin.instanceIndex
2920
2893
  },
2921
2894
  out: {
2922
- pos: d5.builtin.position,
2923
- vNormal: d5.vec3f,
2924
- vColor: d5.vec3f
2895
+ pos: d6.builtin.position,
2896
+ vNormal: d6.vec3f,
2897
+ vColor: d6.vec3f,
2898
+ vWorldPos: d6.vec3f
2925
2899
  }
2926
2900
  })(function(input) {
2927
2901
  const instanceIndex = input.instanceIndex;
@@ -2929,115 +2903,144 @@ function createMeshVertex(meshLayout) {
2929
2903
  const dyn = meshLayout.$.dynamicInstances[slot];
2930
2904
  const stat = meshLayout.$.staticInstances[slot];
2931
2905
  const alpha = meshLayout.$.uniforms.alpha;
2932
- const px = std3.mix(dyn.prevPosX, dyn.currPosX, alpha);
2933
- const py = std3.mix(dyn.prevPosY, dyn.currPosY, alpha);
2934
- const pz = std3.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
2935
- const rx = std3.mix(dyn.prevRotX, dyn.currRotX, alpha);
2936
- const ry = std3.mix(dyn.prevRotY, dyn.currRotY, alpha);
2937
- const rz = std3.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
2906
+ const px = std4.mix(dyn.prevPosX, dyn.currPosX, alpha);
2907
+ const py = std4.mix(dyn.prevPosY, dyn.currPosY, alpha);
2908
+ const pz = std4.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
2909
+ const rx = std4.mix(dyn.prevRotX, dyn.currRotX, alpha);
2910
+ const ry = std4.mix(dyn.prevRotY, dyn.currRotY, alpha);
2911
+ const rz = std4.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
2938
2912
  const sx = stat.scaleX;
2939
2913
  const sy = stat.scaleY;
2940
2914
  const sz = stat.scaleZ;
2941
- const scaled = d5.vec3f(
2942
- std3.mul(input.position.x, sx),
2943
- std3.mul(input.position.y, sy),
2944
- std3.mul(input.position.z, sz)
2915
+ const scaled = d6.vec3f(
2916
+ std4.mul(input.position.x, sx),
2917
+ std4.mul(input.position.y, sy),
2918
+ std4.mul(input.position.z, sz)
2945
2919
  );
2946
- const czr = std3.cos(rz);
2947
- const szr = std3.sin(rz);
2948
- const rz1 = d5.vec3f(
2949
- std3.sub(std3.mul(scaled.x, czr), std3.mul(scaled.y, szr)),
2950
- std3.add(std3.mul(scaled.x, szr), std3.mul(scaled.y, czr)),
2920
+ const czr = std4.cos(rz);
2921
+ const szr = std4.sin(rz);
2922
+ const rz1 = d6.vec3f(
2923
+ std4.sub(std4.mul(scaled.x, czr), std4.mul(scaled.y, szr)),
2924
+ std4.add(std4.mul(scaled.x, szr), std4.mul(scaled.y, czr)),
2951
2925
  scaled.z
2952
2926
  );
2953
- const cyr = std3.cos(ry);
2954
- const syr = std3.sin(ry);
2955
- const ry1 = d5.vec3f(
2956
- std3.add(std3.mul(rz1.x, cyr), std3.mul(rz1.z, syr)),
2927
+ const cyr = std4.cos(ry);
2928
+ const syr = std4.sin(ry);
2929
+ const ry1 = d6.vec3f(
2930
+ std4.add(std4.mul(rz1.x, cyr), std4.mul(rz1.z, syr)),
2957
2931
  rz1.y,
2958
- std3.sub(std3.mul(rz1.z, cyr), std3.mul(rz1.x, syr))
2932
+ std4.sub(std4.mul(rz1.z, cyr), std4.mul(rz1.x, syr))
2959
2933
  );
2960
- const cxr = std3.cos(rx);
2961
- const sxr = std3.sin(rx);
2962
- const rx1 = d5.vec3f(
2934
+ const cxr = std4.cos(rx);
2935
+ const sxr = std4.sin(rx);
2936
+ const rx1 = d6.vec3f(
2963
2937
  ry1.x,
2964
- std3.sub(std3.mul(ry1.y, cxr), std3.mul(ry1.z, sxr)),
2965
- std3.add(std3.mul(ry1.y, sxr), std3.mul(ry1.z, cxr))
2938
+ std4.sub(std4.mul(ry1.y, cxr), std4.mul(ry1.z, sxr)),
2939
+ std4.add(std4.mul(ry1.y, sxr), std4.mul(ry1.z, cxr))
2966
2940
  );
2967
- const worldPos = d5.vec4f(
2968
- std3.add(rx1.x, px),
2969
- std3.add(rx1.y, py),
2970
- std3.add(rx1.z, pz),
2941
+ const worldPos = d6.vec4f(
2942
+ std4.add(rx1.x, px),
2943
+ std4.add(rx1.y, py),
2944
+ std4.add(rx1.z, pz),
2971
2945
  1
2972
2946
  );
2973
2947
  const nScaled = input.normal;
2974
- const nRz = d5.vec3f(
2975
- std3.sub(std3.mul(nScaled.x, czr), std3.mul(nScaled.y, szr)),
2976
- std3.add(std3.mul(nScaled.x, szr), std3.mul(nScaled.y, czr)),
2948
+ const nRz = d6.vec3f(
2949
+ std4.sub(std4.mul(nScaled.x, czr), std4.mul(nScaled.y, szr)),
2950
+ std4.add(std4.mul(nScaled.x, szr), std4.mul(nScaled.y, czr)),
2977
2951
  nScaled.z
2978
2952
  );
2979
- const nRy = d5.vec3f(
2980
- std3.add(std3.mul(nRz.x, cyr), std3.mul(nRz.z, syr)),
2953
+ const nRy = d6.vec3f(
2954
+ std4.add(std4.mul(nRz.x, cyr), std4.mul(nRz.z, syr)),
2981
2955
  nRz.y,
2982
- std3.sub(std3.mul(nRz.z, cyr), std3.mul(nRz.x, syr))
2956
+ std4.sub(std4.mul(nRz.z, cyr), std4.mul(nRz.x, syr))
2983
2957
  );
2984
- const nRx = d5.vec3f(
2958
+ const nRx = d6.vec3f(
2985
2959
  nRy.x,
2986
- std3.sub(std3.mul(nRy.y, cxr), std3.mul(nRy.z, sxr)),
2987
- std3.add(std3.mul(nRy.y, sxr), std3.mul(nRy.z, cxr))
2960
+ std4.sub(std4.mul(nRy.y, cxr), std4.mul(nRy.z, sxr)),
2961
+ std4.add(std4.mul(nRy.y, sxr), std4.mul(nRy.z, cxr))
2988
2962
  );
2989
- const clipPos = std3.mul(meshLayout.$.uniforms.viewProjection, worldPos);
2963
+ const clipPos = std4.mul(meshLayout.$.uniforms.viewProjection, worldPos);
2990
2964
  return {
2991
2965
  pos: clipPos,
2992
2966
  vNormal: nRx,
2993
- vColor: d5.vec3f(stat.colorR, stat.colorG, stat.colorB)
2967
+ vColor: d6.vec3f(stat.colorR, stat.colorG, stat.colorB),
2968
+ vWorldPos: d6.vec3f(worldPos.x, worldPos.y, worldPos.z)
2994
2969
  };
2995
2970
  });
2996
2971
  }
2997
2972
  function createMeshFragment(meshLayout) {
2998
- return import_typegpu7.default.fragmentFn({
2973
+ return import_typegpu8.default.fragmentFn({
2999
2974
  in: {
3000
- vNormal: d5.vec3f,
3001
- vColor: d5.vec3f
2975
+ vNormal: d6.vec3f,
2976
+ vColor: d6.vec3f,
2977
+ vWorldPos: d6.vec3f
3002
2978
  },
3003
- out: d5.vec4f
2979
+ out: d6.vec4f
3004
2980
  })(function(input) {
3005
- const normal = std3.normalize(input.vNormal);
3006
- const lightDir = std3.normalize(d5.vec3f(
3007
- meshLayout.$.uniforms.lightDirX,
3008
- meshLayout.$.uniforms.lightDirY,
3009
- meshLayout.$.uniforms.lightDirZ
3010
- ));
3011
- const diff = std3.max(std3.dot(normal, lightDir), 0);
3012
- const ambient = std3.mul(input.vColor, 0.3);
3013
- const diffuse = std3.mul(input.vColor, diff);
3014
- return d5.vec4f(
3015
- std3.add(ambient.x, diffuse.x),
3016
- std3.add(ambient.y, diffuse.y),
3017
- std3.add(ambient.z, diffuse.z),
3018
- 1
2981
+ const u = meshLayout.$.uniforms;
2982
+ const baseColor = input.vColor;
2983
+ const worldPos = input.vWorldPos;
2984
+ const normal = std4.normalize(input.vNormal);
2985
+ const lightDir = std4.normalize(d6.vec3f(u.lightDirX, u.lightDirY, u.lightDirZ));
2986
+ const diff = std4.max(std4.dot(normal, lightDir), 0) * u.lightDirIntensity;
2987
+ let acc = d6.vec3f(
2988
+ baseColor.x * (u.ambientR + u.lightDirR * diff),
2989
+ baseColor.y * (u.ambientG + u.lightDirG * diff),
2990
+ baseColor.z * (u.ambientB + u.lightDirB * diff)
3019
2991
  );
2992
+ const count = u.lightCount;
2993
+ const a = u.alpha;
2994
+ for (let i = d6.u32(0); i < count; i++) {
2995
+ const L = meshLayout.$.lights[i];
2996
+ const pos = d6.vec3f(
2997
+ std4.mix(L.prevPosX, L.currPosX, a),
2998
+ std4.mix(L.prevPosY, L.currPosY, a),
2999
+ std4.mix(L.prevPosZ, L.currPosZ, a)
3000
+ );
3001
+ const axis = d6.vec3f(
3002
+ std4.mix(L.prevDirX, L.currDirX, a),
3003
+ std4.mix(L.prevDirY, L.currDirY, a),
3004
+ std4.mix(L.prevDirZ, L.currDirZ, a)
3005
+ );
3006
+ const c = lightContribution(
3007
+ pos,
3008
+ axis,
3009
+ d6.vec3f(L.colorR, L.colorG, L.colorB),
3010
+ d6.vec4f(L.intensity, L.range, L.innerCos, L.outerCos),
3011
+ normal,
3012
+ worldPos
3013
+ );
3014
+ acc = d6.vec3f(
3015
+ acc.x + baseColor.x * c.x,
3016
+ acc.y + baseColor.y * c.y,
3017
+ acc.z + baseColor.z * c.z
3018
+ );
3019
+ }
3020
+ const mapped = tonemap(acc);
3021
+ return d6.vec4f(mapped.x, mapped.y, mapped.z, 1);
3020
3022
  });
3021
3023
  }
3022
3024
  function createTextureBindGroupLayout() {
3023
- return import_typegpu7.default.bindGroupLayout({
3025
+ return import_typegpu8.default.bindGroupLayout({
3024
3026
  modelTexture: { texture: "float" },
3025
3027
  modelSampler: { sampler: "filtering" }
3026
3028
  });
3027
3029
  }
3028
3030
  function createTexturedMeshVertex(meshLayout) {
3029
- return import_typegpu7.default.vertexFn({
3031
+ return import_typegpu8.default.vertexFn({
3030
3032
  in: {
3031
- position: d5.location(0, d5.vec3f),
3032
- normal: d5.location(1, d5.vec3f),
3033
- uv: d5.location(2, d5.vec2f),
3034
- instanceIndex: d5.builtin.instanceIndex
3033
+ position: d6.location(0, d6.vec3f),
3034
+ normal: d6.location(1, d6.vec3f),
3035
+ uv: d6.location(2, d6.vec2f),
3036
+ instanceIndex: d6.builtin.instanceIndex
3035
3037
  },
3036
3038
  out: {
3037
- pos: d5.builtin.position,
3038
- vNormal: d5.vec3f,
3039
- vColor: d5.vec3f,
3040
- vUV: d5.vec2f
3039
+ pos: d6.builtin.position,
3040
+ vNormal: d6.vec3f,
3041
+ vColor: d6.vec3f,
3042
+ vUV: d6.vec2f,
3043
+ vWorldPos: d6.vec3f
3041
3044
  }
3042
3045
  })(function(input) {
3043
3046
  const instanceIndex = input.instanceIndex;
@@ -3045,128 +3048,157 @@ function createTexturedMeshVertex(meshLayout) {
3045
3048
  const dyn = meshLayout.$.dynamicInstances[slot];
3046
3049
  const stat = meshLayout.$.staticInstances[slot];
3047
3050
  const alpha = meshLayout.$.uniforms.alpha;
3048
- const px = std3.mix(dyn.prevPosX, dyn.currPosX, alpha);
3049
- const py = std3.mix(dyn.prevPosY, dyn.currPosY, alpha);
3050
- const pz = std3.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
3051
- const rx = std3.mix(dyn.prevRotX, dyn.currRotX, alpha);
3052
- const ry = std3.mix(dyn.prevRotY, dyn.currRotY, alpha);
3053
- const rz = std3.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
3051
+ const px = std4.mix(dyn.prevPosX, dyn.currPosX, alpha);
3052
+ const py = std4.mix(dyn.prevPosY, dyn.currPosY, alpha);
3053
+ const pz = std4.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
3054
+ const rx = std4.mix(dyn.prevRotX, dyn.currRotX, alpha);
3055
+ const ry = std4.mix(dyn.prevRotY, dyn.currRotY, alpha);
3056
+ const rz = std4.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
3054
3057
  const sx = stat.scaleX;
3055
3058
  const sy = stat.scaleY;
3056
3059
  const sz = stat.scaleZ;
3057
- const scaled = d5.vec3f(
3058
- std3.mul(input.position.x, sx),
3059
- std3.mul(input.position.y, sy),
3060
- std3.mul(input.position.z, sz)
3060
+ const scaled = d6.vec3f(
3061
+ std4.mul(input.position.x, sx),
3062
+ std4.mul(input.position.y, sy),
3063
+ std4.mul(input.position.z, sz)
3061
3064
  );
3062
- const czr = std3.cos(rz);
3063
- const szr = std3.sin(rz);
3064
- const rz1 = d5.vec3f(
3065
- std3.sub(std3.mul(scaled.x, czr), std3.mul(scaled.y, szr)),
3066
- std3.add(std3.mul(scaled.x, szr), std3.mul(scaled.y, czr)),
3065
+ const czr = std4.cos(rz);
3066
+ const szr = std4.sin(rz);
3067
+ const rz1 = d6.vec3f(
3068
+ std4.sub(std4.mul(scaled.x, czr), std4.mul(scaled.y, szr)),
3069
+ std4.add(std4.mul(scaled.x, szr), std4.mul(scaled.y, czr)),
3067
3070
  scaled.z
3068
3071
  );
3069
- const cyr = std3.cos(ry);
3070
- const syr = std3.sin(ry);
3071
- const ry1 = d5.vec3f(
3072
- std3.add(std3.mul(rz1.x, cyr), std3.mul(rz1.z, syr)),
3072
+ const cyr = std4.cos(ry);
3073
+ const syr = std4.sin(ry);
3074
+ const ry1 = d6.vec3f(
3075
+ std4.add(std4.mul(rz1.x, cyr), std4.mul(rz1.z, syr)),
3073
3076
  rz1.y,
3074
- std3.sub(std3.mul(rz1.z, cyr), std3.mul(rz1.x, syr))
3077
+ std4.sub(std4.mul(rz1.z, cyr), std4.mul(rz1.x, syr))
3075
3078
  );
3076
- const cxr = std3.cos(rx);
3077
- const sxr = std3.sin(rx);
3078
- const rx1 = d5.vec3f(
3079
+ const cxr = std4.cos(rx);
3080
+ const sxr = std4.sin(rx);
3081
+ const rx1 = d6.vec3f(
3079
3082
  ry1.x,
3080
- std3.sub(std3.mul(ry1.y, cxr), std3.mul(ry1.z, sxr)),
3081
- std3.add(std3.mul(ry1.y, sxr), std3.mul(ry1.z, cxr))
3083
+ std4.sub(std4.mul(ry1.y, cxr), std4.mul(ry1.z, sxr)),
3084
+ std4.add(std4.mul(ry1.y, sxr), std4.mul(ry1.z, cxr))
3082
3085
  );
3083
- const worldPos = d5.vec4f(
3084
- std3.add(rx1.x, px),
3085
- std3.add(rx1.y, py),
3086
- std3.add(rx1.z, pz),
3086
+ const worldPos = d6.vec4f(
3087
+ std4.add(rx1.x, px),
3088
+ std4.add(rx1.y, py),
3089
+ std4.add(rx1.z, pz),
3087
3090
  1
3088
3091
  );
3089
3092
  const nScaled = input.normal;
3090
- const nRz = d5.vec3f(
3091
- std3.sub(std3.mul(nScaled.x, czr), std3.mul(nScaled.y, szr)),
3092
- std3.add(std3.mul(nScaled.x, szr), std3.mul(nScaled.y, czr)),
3093
+ const nRz = d6.vec3f(
3094
+ std4.sub(std4.mul(nScaled.x, czr), std4.mul(nScaled.y, szr)),
3095
+ std4.add(std4.mul(nScaled.x, szr), std4.mul(nScaled.y, czr)),
3093
3096
  nScaled.z
3094
3097
  );
3095
- const nRy = d5.vec3f(
3096
- std3.add(std3.mul(nRz.x, cyr), std3.mul(nRz.z, syr)),
3098
+ const nRy = d6.vec3f(
3099
+ std4.add(std4.mul(nRz.x, cyr), std4.mul(nRz.z, syr)),
3097
3100
  nRz.y,
3098
- std3.sub(std3.mul(nRz.z, cyr), std3.mul(nRz.x, syr))
3101
+ std4.sub(std4.mul(nRz.z, cyr), std4.mul(nRz.x, syr))
3099
3102
  );
3100
- const nRx = d5.vec3f(
3103
+ const nRx = d6.vec3f(
3101
3104
  nRy.x,
3102
- std3.sub(std3.mul(nRy.y, cxr), std3.mul(nRy.z, sxr)),
3103
- std3.add(std3.mul(nRy.y, sxr), std3.mul(nRy.z, cxr))
3105
+ std4.sub(std4.mul(nRy.y, cxr), std4.mul(nRy.z, sxr)),
3106
+ std4.add(std4.mul(nRy.y, sxr), std4.mul(nRy.z, cxr))
3104
3107
  );
3105
- const clipPos = std3.mul(meshLayout.$.uniforms.viewProjection, worldPos);
3108
+ const clipPos = std4.mul(meshLayout.$.uniforms.viewProjection, worldPos);
3106
3109
  return {
3107
3110
  pos: clipPos,
3108
3111
  vNormal: nRx,
3109
- vColor: d5.vec3f(stat.colorR, stat.colorG, stat.colorB),
3110
- vUV: input.uv
3112
+ vColor: d6.vec3f(stat.colorR, stat.colorG, stat.colorB),
3113
+ vUV: input.uv,
3114
+ vWorldPos: d6.vec3f(worldPos.x, worldPos.y, worldPos.z)
3111
3115
  };
3112
3116
  });
3113
3117
  }
3114
3118
  function createTexturedMeshFragment(meshLayout, texLayout) {
3115
- return import_typegpu7.default.fragmentFn({
3119
+ return import_typegpu8.default.fragmentFn({
3116
3120
  in: {
3117
- vNormal: d5.vec3f,
3118
- vColor: d5.vec3f,
3119
- vUV: d5.vec2f
3121
+ vNormal: d6.vec3f,
3122
+ vColor: d6.vec3f,
3123
+ vUV: d6.vec2f,
3124
+ vWorldPos: d6.vec3f
3120
3125
  },
3121
- out: d5.vec4f
3126
+ out: d6.vec4f
3122
3127
  })(function(input) {
3123
- const normal = std3.normalize(input.vNormal);
3124
- const lightDir = std3.normalize(d5.vec3f(
3125
- meshLayout.$.uniforms.lightDirX,
3126
- meshLayout.$.uniforms.lightDirY,
3127
- meshLayout.$.uniforms.lightDirZ
3128
- ));
3129
- const texColor = std3.textureSample(texLayout.$.modelTexture, texLayout.$.modelSampler, input.vUV);
3130
- const baseColor = d5.vec3f(
3131
- std3.mul(texColor.x, input.vColor.x),
3132
- std3.mul(texColor.y, input.vColor.y),
3133
- std3.mul(texColor.z, input.vColor.z)
3128
+ const u = meshLayout.$.uniforms;
3129
+ const worldPos = input.vWorldPos;
3130
+ const normal = std4.normalize(input.vNormal);
3131
+ const texColor = std4.textureSample(texLayout.$.modelTexture, texLayout.$.modelSampler, input.vUV);
3132
+ const baseColor = d6.vec3f(
3133
+ std4.mul(texColor.x, input.vColor.x),
3134
+ std4.mul(texColor.y, input.vColor.y),
3135
+ std4.mul(texColor.z, input.vColor.z)
3134
3136
  );
3135
- const diff = std3.max(std3.dot(normal, lightDir), 0);
3136
- const ambient = std3.mul(baseColor, 0.3);
3137
- const diffuse = std3.mul(baseColor, diff);
3138
- return d5.vec4f(
3139
- std3.add(ambient.x, diffuse.x),
3140
- std3.add(ambient.y, diffuse.y),
3141
- std3.add(ambient.z, diffuse.z),
3142
- std3.mul(texColor.w, 1)
3137
+ const lightDir = std4.normalize(d6.vec3f(u.lightDirX, u.lightDirY, u.lightDirZ));
3138
+ const diff = std4.max(std4.dot(normal, lightDir), 0) * u.lightDirIntensity;
3139
+ let acc = d6.vec3f(
3140
+ baseColor.x * (u.ambientR + u.lightDirR * diff),
3141
+ baseColor.y * (u.ambientG + u.lightDirG * diff),
3142
+ baseColor.z * (u.ambientB + u.lightDirB * diff)
3143
3143
  );
3144
+ const count = u.lightCount;
3145
+ const a = u.alpha;
3146
+ for (let i = d6.u32(0); i < count; i++) {
3147
+ const L = meshLayout.$.lights[i];
3148
+ const pos = d6.vec3f(
3149
+ std4.mix(L.prevPosX, L.currPosX, a),
3150
+ std4.mix(L.prevPosY, L.currPosY, a),
3151
+ std4.mix(L.prevPosZ, L.currPosZ, a)
3152
+ );
3153
+ const axis = d6.vec3f(
3154
+ std4.mix(L.prevDirX, L.currDirX, a),
3155
+ std4.mix(L.prevDirY, L.currDirY, a),
3156
+ std4.mix(L.prevDirZ, L.currDirZ, a)
3157
+ );
3158
+ const c = lightContribution(
3159
+ pos,
3160
+ axis,
3161
+ d6.vec3f(L.colorR, L.colorG, L.colorB),
3162
+ d6.vec4f(L.intensity, L.range, L.innerCos, L.outerCos),
3163
+ normal,
3164
+ worldPos
3165
+ );
3166
+ acc = d6.vec3f(
3167
+ acc.x + baseColor.x * c.x,
3168
+ acc.y + baseColor.y * c.y,
3169
+ acc.z + baseColor.z * c.z
3170
+ );
3171
+ }
3172
+ const mapped = tonemap(acc);
3173
+ return d6.vec4f(mapped.x, mapped.y, mapped.z, texColor.w);
3144
3174
  });
3145
3175
  }
3146
3176
  function createSkinnedMeshLayout(maxInstances, maxBones) {
3147
- return import_typegpu7.default.bindGroupLayout({
3177
+ return import_typegpu8.default.bindGroupLayout({
3148
3178
  uniforms: { uniform: MeshUniforms },
3149
- dynamicInstances: { storage: d5.arrayOf(DynamicMesh, maxInstances) },
3150
- staticInstances: { storage: d5.arrayOf(SkinnedStaticMesh, maxInstances) },
3151
- slotIndices: { storage: d5.arrayOf(d5.u32, maxInstances) },
3152
- boneMatrices: { storage: d5.arrayOf(d5.mat4x4f, maxBones) }
3179
+ dynamicInstances: { storage: d6.arrayOf(DynamicMesh, maxInstances) },
3180
+ staticInstances: { storage: d6.arrayOf(SkinnedStaticMesh, maxInstances) },
3181
+ slotIndices: { storage: d6.arrayOf(d6.u32, maxInstances) },
3182
+ boneMatrices: { storage: d6.arrayOf(d6.mat4x4f, maxBones) },
3183
+ lights: { storage: d6.arrayOf(Light, MAX_LIGHTS) }
3153
3184
  });
3154
3185
  }
3155
3186
  function createSkinnedMeshVertex(layout) {
3156
- return import_typegpu7.default.vertexFn({
3187
+ return import_typegpu8.default.vertexFn({
3157
3188
  in: {
3158
- position: d5.location(0, d5.vec3f),
3159
- normal: d5.location(1, d5.vec3f),
3160
- uv: d5.location(2, d5.vec2f),
3161
- joints: d5.location(3, d5.vec4u),
3162
- weights: d5.location(4, d5.vec4f),
3163
- instanceIndex: d5.builtin.instanceIndex
3189
+ position: d6.location(0, d6.vec3f),
3190
+ normal: d6.location(1, d6.vec3f),
3191
+ uv: d6.location(2, d6.vec2f),
3192
+ joints: d6.location(3, d6.vec4u),
3193
+ weights: d6.location(4, d6.vec4f),
3194
+ instanceIndex: d6.builtin.instanceIndex
3164
3195
  },
3165
3196
  out: {
3166
- pos: d5.builtin.position,
3167
- vNormal: d5.vec3f,
3168
- vColor: d5.vec3f,
3169
- vUV: d5.vec2f
3197
+ pos: d6.builtin.position,
3198
+ vNormal: d6.vec3f,
3199
+ vColor: d6.vec3f,
3200
+ vUV: d6.vec2f,
3201
+ vWorldPos: d6.vec3f
3170
3202
  }
3171
3203
  })(function(input) {
3172
3204
  const slot = layout.$.slotIndices[input.instanceIndex];
@@ -3186,69 +3218,69 @@ function createSkinnedMeshVertex(layout) {
3186
3218
  const m1 = layout.$.boneMatrices[boneOffset + j1];
3187
3219
  const m2 = layout.$.boneMatrices[boneOffset + j2];
3188
3220
  const m3 = layout.$.boneMatrices[boneOffset + j3];
3189
- const p = d5.vec4f(input.position.x, input.position.y, input.position.z, 1);
3221
+ const p = d6.vec4f(input.position.x, input.position.y, input.position.z, 1);
3190
3222
  const sp0 = m0 * p;
3191
3223
  const sp1 = m1 * p;
3192
3224
  const sp2 = m2 * p;
3193
3225
  const sp3 = m3 * p;
3194
- const skinnedPos = d5.vec3f(
3226
+ const skinnedPos = d6.vec3f(
3195
3227
  sp0.x * w0 + sp1.x * w1 + sp2.x * w2 + sp3.x * w3,
3196
3228
  sp0.y * w0 + sp1.y * w1 + sp2.y * w2 + sp3.y * w3,
3197
3229
  sp0.z * w0 + sp1.z * w1 + sp2.z * w2 + sp3.z * w3
3198
3230
  );
3199
- const n = d5.vec4f(input.normal.x, input.normal.y, input.normal.z, 0);
3231
+ const n = d6.vec4f(input.normal.x, input.normal.y, input.normal.z, 0);
3200
3232
  const sn0 = m0 * n;
3201
3233
  const sn1 = m1 * n;
3202
3234
  const sn2 = m2 * n;
3203
3235
  const sn3 = m3 * n;
3204
- const skinnedNormal = d5.vec3f(
3236
+ const skinnedNormal = d6.vec3f(
3205
3237
  sn0.x * w0 + sn1.x * w1 + sn2.x * w2 + sn3.x * w3,
3206
3238
  sn0.y * w0 + sn1.y * w1 + sn2.y * w2 + sn3.y * w3,
3207
3239
  sn0.z * w0 + sn1.z * w1 + sn2.z * w2 + sn3.z * w3
3208
3240
  );
3209
- const px = std3.mix(dyn.prevPosX, dyn.currPosX, alpha);
3210
- const py = std3.mix(dyn.prevPosY, dyn.currPosY, alpha);
3211
- const pz = std3.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
3212
- const rx = std3.mix(dyn.prevRotX, dyn.currRotX, alpha);
3213
- const ry = std3.mix(dyn.prevRotY, dyn.currRotY, alpha);
3214
- const rz = std3.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
3241
+ const px = std4.mix(dyn.prevPosX, dyn.currPosX, alpha);
3242
+ const py = std4.mix(dyn.prevPosY, dyn.currPosY, alpha);
3243
+ const pz = std4.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
3244
+ const rx = std4.mix(dyn.prevRotX, dyn.currRotX, alpha);
3245
+ const ry = std4.mix(dyn.prevRotY, dyn.currRotY, alpha);
3246
+ const rz = std4.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
3215
3247
  const sx = stat.scaleX;
3216
3248
  const sy = stat.scaleY;
3217
3249
  const sz = stat.scaleZ;
3218
- const scaled = d5.vec3f(skinnedPos.x * sx, skinnedPos.y * sy, skinnedPos.z * sz);
3219
- const czr = std3.cos(rz);
3220
- const szr = std3.sin(rz);
3221
- const rz1 = d5.vec3f(
3250
+ const scaled = d6.vec3f(skinnedPos.x * sx, skinnedPos.y * sy, skinnedPos.z * sz);
3251
+ const czr = std4.cos(rz);
3252
+ const szr = std4.sin(rz);
3253
+ const rz1 = d6.vec3f(
3222
3254
  scaled.x * czr - scaled.y * szr,
3223
3255
  scaled.x * szr + scaled.y * czr,
3224
3256
  scaled.z
3225
3257
  );
3226
- const cyr = std3.cos(ry);
3227
- const syr = std3.sin(ry);
3228
- const ry1 = d5.vec3f(
3258
+ const cyr = std4.cos(ry);
3259
+ const syr = std4.sin(ry);
3260
+ const ry1 = d6.vec3f(
3229
3261
  rz1.x * cyr + rz1.z * syr,
3230
3262
  rz1.y,
3231
3263
  rz1.z * cyr - rz1.x * syr
3232
3264
  );
3233
- const cxr = std3.cos(rx);
3234
- const sxr = std3.sin(rx);
3235
- const rx1 = d5.vec3f(
3265
+ const cxr = std4.cos(rx);
3266
+ const sxr = std4.sin(rx);
3267
+ const rx1 = d6.vec3f(
3236
3268
  ry1.x,
3237
3269
  ry1.y * cxr - ry1.z * sxr,
3238
3270
  ry1.y * sxr + ry1.z * cxr
3239
3271
  );
3240
- const worldPos = d5.vec4f(rx1.x + px, rx1.y + py, rx1.z + pz, 1);
3241
- const nRz = d5.vec3f(
3272
+ const worldPos = d6.vec4f(rx1.x + px, rx1.y + py, rx1.z + pz, 1);
3273
+ const nRz = d6.vec3f(
3242
3274
  skinnedNormal.x * czr - skinnedNormal.y * szr,
3243
3275
  skinnedNormal.x * szr + skinnedNormal.y * czr,
3244
3276
  skinnedNormal.z
3245
3277
  );
3246
- const nRy = d5.vec3f(
3278
+ const nRy = d6.vec3f(
3247
3279
  nRz.x * cyr + nRz.z * syr,
3248
3280
  nRz.y,
3249
3281
  nRz.z * cyr - nRz.x * syr
3250
3282
  );
3251
- const nRx = d5.vec3f(
3283
+ const nRx = d6.vec3f(
3252
3284
  nRy.x,
3253
3285
  nRy.y * cxr - nRy.z * sxr,
3254
3286
  nRy.y * sxr + nRy.z * cxr
@@ -3257,14 +3289,629 @@ function createSkinnedMeshVertex(layout) {
3257
3289
  return {
3258
3290
  pos: clipPos,
3259
3291
  vNormal: nRx,
3260
- vColor: d5.vec3f(stat.colorR, stat.colorG, stat.colorB),
3261
- vUV: input.uv
3292
+ vColor: d6.vec3f(stat.colorR, stat.colorG, stat.colorB),
3293
+ vUV: input.uv,
3294
+ vWorldPos: d6.vec3f(worldPos.x, worldPos.y, worldPos.z)
3262
3295
  };
3263
3296
  });
3264
3297
  }
3298
+ function createSkinnedMeshFragment(meshLayout) {
3299
+ return import_typegpu8.default.fragmentFn({
3300
+ in: {
3301
+ vNormal: d6.vec3f,
3302
+ vColor: d6.vec3f,
3303
+ vUV: d6.vec2f,
3304
+ vWorldPos: d6.vec3f
3305
+ },
3306
+ out: d6.vec4f
3307
+ })(function(input) {
3308
+ const u = meshLayout.$.uniforms;
3309
+ const baseColor = input.vColor;
3310
+ const worldPos = input.vWorldPos;
3311
+ const normal = std4.normalize(input.vNormal);
3312
+ const lightDir = std4.normalize(d6.vec3f(u.lightDirX, u.lightDirY, u.lightDirZ));
3313
+ const diff = std4.max(std4.dot(normal, lightDir), 0) * u.lightDirIntensity;
3314
+ let acc = d6.vec3f(
3315
+ baseColor.x * (u.ambientR + u.lightDirR * diff),
3316
+ baseColor.y * (u.ambientG + u.lightDirG * diff),
3317
+ baseColor.z * (u.ambientB + u.lightDirB * diff)
3318
+ );
3319
+ const count = u.lightCount;
3320
+ const a = u.alpha;
3321
+ for (let i = d6.u32(0); i < count; i++) {
3322
+ const L = meshLayout.$.lights[i];
3323
+ const pos = d6.vec3f(
3324
+ std4.mix(L.prevPosX, L.currPosX, a),
3325
+ std4.mix(L.prevPosY, L.currPosY, a),
3326
+ std4.mix(L.prevPosZ, L.currPosZ, a)
3327
+ );
3328
+ const axis = d6.vec3f(
3329
+ std4.mix(L.prevDirX, L.currDirX, a),
3330
+ std4.mix(L.prevDirY, L.currDirY, a),
3331
+ std4.mix(L.prevDirZ, L.currDirZ, a)
3332
+ );
3333
+ const c = lightContribution(
3334
+ pos,
3335
+ axis,
3336
+ d6.vec3f(L.colorR, L.colorG, L.colorB),
3337
+ d6.vec4f(L.intensity, L.range, L.innerCos, L.outerCos),
3338
+ normal,
3339
+ worldPos
3340
+ );
3341
+ acc = d6.vec3f(
3342
+ acc.x + baseColor.x * c.x,
3343
+ acc.y + baseColor.y * c.y,
3344
+ acc.z + baseColor.z * c.z
3345
+ );
3346
+ }
3347
+ const mapped = tonemap(acc);
3348
+ return d6.vec4f(mapped.x, mapped.y, mapped.z, 1);
3349
+ });
3350
+ }
3351
+
3352
+ // src/3d/lights.ts
3353
+ var import_slot_map2 = require("murow/core/slot-map");
3354
+ var KIND = 0;
3355
+ var CURR_POS_X = 1;
3356
+ var CURR_POS_Y = 2;
3357
+ var CURR_POS_Z = 3;
3358
+ var PREV_POS_X = 4;
3359
+ var PREV_POS_Y = 5;
3360
+ var PREV_POS_Z = 6;
3361
+ var CURR_DIR_X = 7;
3362
+ var CURR_DIR_Y = 8;
3363
+ var CURR_DIR_Z = 9;
3364
+ var PREV_DIR_X = 10;
3365
+ var PREV_DIR_Y = 11;
3366
+ var PREV_DIR_Z = 12;
3367
+ var COL_R = 13;
3368
+ var COL_G = 14;
3369
+ var COL_B = 15;
3370
+ var INTENSITY = 16;
3371
+ var RANGE = 17;
3372
+ var INNER_COS = 18;
3373
+ var OUTER_COS = 19;
3374
+ var CASTS_SHADOW = 20;
3375
+ var SHADOW_INDEX = 21;
3376
+ var LightSystem = class {
3377
+ constructor(maxLights) {
3378
+ this.maxLights = maxLights;
3379
+ // Global directional + ambient terms (the classic fixed look; now configurable).
3380
+ this.dirDir = [0.3, 0.8, 0.5];
3381
+ this.dirColor = [1, 1, 1];
3382
+ this.dirIntensity = 1;
3383
+ this.ambient = [0.3, 0.3, 0.3];
3384
+ this.data = new Float32Array(maxLights * LIGHT_FLOATS);
3385
+ this.slots = new import_slot_map2.SlotMap(maxLights);
3386
+ this.enabled = new Uint8Array(maxLights).fill(1);
3387
+ this.handles = new Array(maxLights).fill(null);
3388
+ this.uploadData = new Float32Array(maxLights * LIGHT_FLOATS);
3389
+ this.angle = new Float32Array(maxLights);
3390
+ this.smoothness = new Float32Array(maxLights);
3391
+ }
3392
+ /** Add a dynamic point or spot light. Throws past `maxLights`. */
3393
+ add(spec) {
3394
+ const slot = this.slots.add();
3395
+ if (slot === -1)
3396
+ throw new Error(`Max lights (${this.maxLights}) reached`);
3397
+ this.enabled[slot] = 1;
3398
+ this.writeSlot(slot, spec);
3399
+ const data = this.data;
3400
+ const enabledArr = this.enabled;
3401
+ const slots = this.slots;
3402
+ const handles = this.handles;
3403
+ const angleArr = this.angle;
3404
+ const smoothArr = this.smoothness;
3405
+ const base = slot * LIGHT_FLOATS;
3406
+ let destroyed = false;
3407
+ const posOut = [0, 0, 0];
3408
+ const dirOut = [0, 0, 0];
3409
+ const colOut = [0, 0, 0];
3410
+ const handle = {
3411
+ slot,
3412
+ setPosition(x, y, z) {
3413
+ data[base + CURR_POS_X] = x;
3414
+ data[base + CURR_POS_Y] = y;
3415
+ data[base + CURR_POS_Z] = z;
3416
+ },
3417
+ setDirection(x, y, z) {
3418
+ data[base + CURR_DIR_X] = x;
3419
+ data[base + CURR_DIR_Y] = y;
3420
+ data[base + CURR_DIR_Z] = z;
3421
+ },
3422
+ teleport(x, y, z) {
3423
+ data[base + CURR_POS_X] = x;
3424
+ data[base + CURR_POS_Y] = y;
3425
+ data[base + CURR_POS_Z] = z;
3426
+ data[base + PREV_POS_X] = x;
3427
+ data[base + PREV_POS_Y] = y;
3428
+ data[base + PREV_POS_Z] = z;
3429
+ },
3430
+ setColor(r, g, b) {
3431
+ data[base + COL_R] = r;
3432
+ data[base + COL_G] = g;
3433
+ data[base + COL_B] = b;
3434
+ },
3435
+ get position() {
3436
+ posOut[0] = data[base + CURR_POS_X];
3437
+ posOut[1] = data[base + CURR_POS_Y];
3438
+ posOut[2] = data[base + CURR_POS_Z];
3439
+ return posOut;
3440
+ },
3441
+ get direction() {
3442
+ dirOut[0] = data[base + CURR_DIR_X];
3443
+ dirOut[1] = data[base + CURR_DIR_Y];
3444
+ dirOut[2] = data[base + CURR_DIR_Z];
3445
+ return dirOut;
3446
+ },
3447
+ get color() {
3448
+ colOut[0] = data[base + COL_R];
3449
+ colOut[1] = data[base + COL_G];
3450
+ colOut[2] = data[base + COL_B];
3451
+ return colOut;
3452
+ },
3453
+ get intensity() {
3454
+ return data[base + INTENSITY];
3455
+ },
3456
+ set intensity(v) {
3457
+ data[base + INTENSITY] = v;
3458
+ },
3459
+ get range() {
3460
+ return data[base + RANGE];
3461
+ },
3462
+ set range(v) {
3463
+ data[base + RANGE] = v;
3464
+ },
3465
+ get angle() {
3466
+ return angleArr[slot];
3467
+ },
3468
+ set angle(v) {
3469
+ angleArr[slot] = v;
3470
+ const { innerCos, outerCos } = coneCosines(v, smoothArr[slot]);
3471
+ data[base + INNER_COS] = innerCos;
3472
+ data[base + OUTER_COS] = outerCos;
3473
+ },
3474
+ get smoothness() {
3475
+ return smoothArr[slot];
3476
+ },
3477
+ set smoothness(v) {
3478
+ const s = v < 0 ? 0 : v > 1 ? 1 : v;
3479
+ smoothArr[slot] = s;
3480
+ const { innerCos, outerCos } = coneCosines(angleArr[slot], s);
3481
+ data[base + INNER_COS] = innerCos;
3482
+ data[base + OUTER_COS] = outerCos;
3483
+ },
3484
+ get enabled() {
3485
+ return enabledArr[slot] === 1;
3486
+ },
3487
+ set enabled(v) {
3488
+ enabledArr[slot] = v ? 1 : 0;
3489
+ },
3490
+ destroy() {
3491
+ if (destroyed)
3492
+ return;
3493
+ destroyed = true;
3494
+ data.fill(0, base, base + LIGHT_FLOATS);
3495
+ handles[slot] = null;
3496
+ slots.remove(slot);
3497
+ }
3498
+ };
3499
+ this.handles[slot] = handle;
3500
+ return handle;
3501
+ }
3502
+ /**
3503
+ * Set the global directional light (the "sun"). `direction` points from the
3504
+ * surface toward the light. Defaults to `(0.3, 0.8, 0.5)`, white, intensity 1.
3505
+ */
3506
+ setDirectional(direction, color = [1, 1, 1], intensity = 1) {
3507
+ this.dirDir = [direction[0], direction[1], direction[2]];
3508
+ this.dirColor = [color[0], color[1], color[2]];
3509
+ this.dirIntensity = intensity;
3510
+ }
3511
+ /** Set the global ambient term. Defaults to `(0.3, 0.3, 0.3)`. */
3512
+ setAmbient(color) {
3513
+ this.ambient = [color[0], color[1], color[2]];
3514
+ }
3515
+ /** Number of live dynamic lights. */
3516
+ get count() {
3517
+ return this.slots.size;
3518
+ }
3519
+ /**
3520
+ * Pack enabled lights into a dense run for upload. Disabled lights are
3521
+ * skipped so the shader loop only walks contributing lights. Returns the
3522
+ * shared scratch buffer, the light count, and the byte length to upload
3523
+ * (so the caller never needs the record layout).
3524
+ */
3525
+ pack() {
3526
+ const active = this.slots.activeSlots;
3527
+ const size = this.slots.size;
3528
+ const src = this.data;
3529
+ const dst = this.uploadData;
3530
+ let count = 0;
3531
+ for (let i = 0; i < size; i++) {
3532
+ const slot = active[i];
3533
+ if (this.enabled[slot] === 0)
3534
+ continue;
3535
+ const sBase = slot * LIGHT_FLOATS;
3536
+ dst.set(src.subarray(sBase, sBase + LIGHT_FLOATS), count * LIGHT_FLOATS);
3537
+ count++;
3538
+ }
3539
+ return { data: dst, count, byteLength: count * LIGHT_FLOATS * 4 };
3540
+ }
3541
+ /**
3542
+ * Stamp the directional + ambient terms and the light count into the
3543
+ * renderer's uniform array, starting at `offset` (the float index after the
3544
+ * VP matrix + alpha). Layout: lightDir(3), dirColor(3), dirIntensity(1),
3545
+ * ambient(3), then lightCount as a u32 reinterpret at `offset + 10`.
3546
+ */
3547
+ writeUniforms(uniformData, offset, count) {
3548
+ uniformData[offset + 0] = this.dirDir[0];
3549
+ uniformData[offset + 1] = this.dirDir[1];
3550
+ uniformData[offset + 2] = this.dirDir[2];
3551
+ uniformData[offset + 3] = this.dirColor[0];
3552
+ uniformData[offset + 4] = this.dirColor[1];
3553
+ uniformData[offset + 5] = this.dirColor[2];
3554
+ uniformData[offset + 6] = this.dirIntensity;
3555
+ uniformData[offset + 7] = this.ambient[0];
3556
+ uniformData[offset + 8] = this.ambient[1];
3557
+ uniformData[offset + 9] = this.ambient[2];
3558
+ new Uint32Array(uniformData.buffer)[offset + 10] = count;
3559
+ }
3560
+ /**
3561
+ * Snapshot every live light's current position/direction into its prev
3562
+ * slot. Called from the renderer's `storePreviousState()` in `pre-tick`, so
3563
+ * the shader can `mix(prev, curr, alpha)` and moving lights interpolate at
3564
+ * render rate instead of snapping at the tick boundary.
3565
+ */
3566
+ storePrevious() {
3567
+ const active = this.slots.activeSlots;
3568
+ const size = this.slots.size;
3569
+ const data = this.data;
3570
+ for (let i = 0; i < size; i++) {
3571
+ const base = active[i] * LIGHT_FLOATS;
3572
+ data[base + PREV_POS_X] = data[base + CURR_POS_X];
3573
+ data[base + PREV_POS_Y] = data[base + CURR_POS_Y];
3574
+ data[base + PREV_POS_Z] = data[base + CURR_POS_Z];
3575
+ data[base + PREV_DIR_X] = data[base + CURR_DIR_X];
3576
+ data[base + PREV_DIR_Y] = data[base + CURR_DIR_Y];
3577
+ data[base + PREV_DIR_Z] = data[base + CURR_DIR_Z];
3578
+ }
3579
+ }
3580
+ /** Write a light spec into its CPU slot. Prev is seeded to curr (no spawn lerp). */
3581
+ writeSlot(slot, spec) {
3582
+ const base = slot * LIGHT_FLOATS;
3583
+ const data = this.data;
3584
+ const color = spec.color ?? [1, 1, 1];
3585
+ const [px, py, pz] = spec.position;
3586
+ data[base + KIND] = spec.type === "spot" ? LIGHT_KIND_SPOT : LIGHT_KIND_POINT;
3587
+ data[base + CURR_POS_X] = px;
3588
+ data[base + PREV_POS_X] = px;
3589
+ data[base + CURR_POS_Y] = py;
3590
+ data[base + PREV_POS_Y] = py;
3591
+ data[base + CURR_POS_Z] = pz;
3592
+ data[base + PREV_POS_Z] = pz;
3593
+ data[base + COL_R] = color[0];
3594
+ data[base + COL_G] = color[1];
3595
+ data[base + COL_B] = color[2];
3596
+ data[base + INTENSITY] = spec.intensity ?? 1;
3597
+ data[base + RANGE] = spec.range ?? 10;
3598
+ data[base + CASTS_SHADOW] = 0;
3599
+ data[base + SHADOW_INDEX] = -1;
3600
+ if (spec.type === "spot") {
3601
+ const [dx, dy, dz] = spec.direction;
3602
+ data[base + CURR_DIR_X] = dx;
3603
+ data[base + PREV_DIR_X] = dx;
3604
+ data[base + CURR_DIR_Y] = dy;
3605
+ data[base + PREV_DIR_Y] = dy;
3606
+ data[base + CURR_DIR_Z] = dz;
3607
+ data[base + PREV_DIR_Z] = dz;
3608
+ const angle = spec.angle ?? 0.5;
3609
+ const smoothness = Math.min(1, Math.max(0, spec.smoothness ?? 0.5));
3610
+ this.angle[slot] = angle;
3611
+ this.smoothness[slot] = smoothness;
3612
+ const { innerCos, outerCos } = coneCosines(angle, smoothness);
3613
+ data[base + INNER_COS] = innerCos;
3614
+ data[base + OUTER_COS] = outerCos;
3615
+ } else {
3616
+ data[base + CURR_DIR_X] = 0;
3617
+ data[base + PREV_DIR_X] = 0;
3618
+ data[base + CURR_DIR_Y] = 0;
3619
+ data[base + PREV_DIR_Y] = 0;
3620
+ data[base + CURR_DIR_Z] = 0;
3621
+ data[base + PREV_DIR_Z] = 0;
3622
+ this.angle[slot] = 0;
3623
+ this.smoothness[slot] = 0;
3624
+ data[base + INNER_COS] = 1;
3625
+ data[base + OUTER_COS] = -1;
3626
+ }
3627
+ }
3628
+ };
3629
+ function coneCosines(angle, smoothness) {
3630
+ const outerCos = Math.cos(angle);
3631
+ const innerCos = Math.cos(angle * (1 - smoothness));
3632
+ return { innerCos, outerCos };
3633
+ }
3634
+
3635
+ // src/camera/camera-3d.ts
3636
+ var import_lerp2 = require("murow/core/lerp");
3637
+ var import_ray = require("murow/core/ray");
3638
+ var Camera3D = class {
3639
+ constructor() {
3640
+ this.position = [0, 5, -10];
3641
+ this.target = [0, 0, 0];
3642
+ this.up = [0, 1, 0];
3643
+ this.fov = 60;
3644
+ this.near = 0.1;
3645
+ this.far = 1e3;
3646
+ this.aspect = 1;
3647
+ this.movement = "local";
3648
+ // Previous state for interpolation (stored before each tick)
3649
+ this._prevPosition = [0, 5, -10];
3650
+ this._prevTarget = [0, 0, 0];
3651
+ // Interpolated state used for rendering
3652
+ this._renderPosition = [0, 5, -10];
3653
+ this._renderTarget = [0, 0, 0];
3654
+ this._viewMatrix = new Float32Array(16);
3655
+ this._projMatrix = new Float32Array(16);
3656
+ this._vpMatrix = new Float32Array(16);
3657
+ this._ray = new import_ray.Ray3D();
3658
+ this._width = 1;
3659
+ this._height = 1;
3660
+ }
3661
+ /**
3662
+ * Store current position/target as previous. Call before each tick.
3663
+ */
3664
+ storePrevious() {
3665
+ this._prevPosition[0] = this.position[0];
3666
+ this._prevPosition[1] = this.position[1];
3667
+ this._prevPosition[2] = this.position[2];
3668
+ this._prevTarget[0] = this.target[0];
3669
+ this._prevTarget[1] = this.target[1];
3670
+ this._prevTarget[2] = this.target[2];
3671
+ }
3672
+ /**
3673
+ * Smoothly move the camera toward a target point.
3674
+ * Call each tick. The camera and its look-at target lerp toward the given position.
3675
+ * @param targetX World X to follow
3676
+ * @param targetY World Y to follow
3677
+ * @param targetZ World Z to follow
3678
+ * @param smoothing 0-1. 1 = snap instantly, 0.1 = lazy follow. Default 1.
3679
+ */
3680
+ follow(targetX, targetY, targetZ, smoothing = 1) {
3681
+ const dx = targetX - this.target[0];
3682
+ const dy = targetY - this.target[1];
3683
+ const dz = targetZ - this.target[2];
3684
+ const mx = dx * smoothing;
3685
+ const my = dy * smoothing;
3686
+ const mz = dz * smoothing;
3687
+ this.target[0] += mx;
3688
+ this.target[1] += my;
3689
+ this.target[2] += mz;
3690
+ this.position[0] += mx;
3691
+ this.position[1] += my;
3692
+ this.position[2] += mz;
3693
+ }
3694
+ /**
3695
+ * Interpolate between previous and current state. Call before rendering.
3696
+ */
3697
+ interpolate(alpha) {
3698
+ this._renderPosition[0] = (0, import_lerp2.lerp)(this._prevPosition[0], this.position[0], alpha);
3699
+ this._renderPosition[1] = (0, import_lerp2.lerp)(this._prevPosition[1], this.position[1], alpha);
3700
+ this._renderPosition[2] = (0, import_lerp2.lerp)(this._prevPosition[2], this.position[2], alpha);
3701
+ this._renderTarget[0] = (0, import_lerp2.lerp)(this._prevTarget[0], this.target[0], alpha);
3702
+ this._renderTarget[1] = (0, import_lerp2.lerp)(this._prevTarget[1], this.target[1], alpha);
3703
+ this._renderTarget[2] = (0, import_lerp2.lerp)(this._prevTarget[2], this.target[2], alpha);
3704
+ }
3705
+ /**
3706
+ * Build the view matrix (lookAt) using interpolated state.
3707
+ */
3708
+ getViewMatrix() {
3709
+ lookAt(this._viewMatrix, this._renderPosition, this._renderTarget, this.up);
3710
+ return this._viewMatrix;
3711
+ }
3712
+ /**
3713
+ * Build the perspective projection matrix.
3714
+ */
3715
+ getProjectionMatrix() {
3716
+ perspective(this._projMatrix, this.fov * (Math.PI / 180), this.aspect, this.near, this.far);
3717
+ return this._projMatrix;
3718
+ }
3719
+ /**
3720
+ * Build the combined view-projection matrix.
3721
+ */
3722
+ getViewProjectionMatrix() {
3723
+ this.getViewMatrix();
3724
+ this.getProjectionMatrix();
3725
+ mat4Multiply(this._vpMatrix, this._projMatrix, this._viewMatrix);
3726
+ return this._vpMatrix;
3727
+ }
3728
+ setAspect(width, height) {
3729
+ this.aspect = width / height;
3730
+ this._width = width;
3731
+ this._height = height;
3732
+ }
3733
+ setPosition(x, y, z) {
3734
+ this.position[0] = x;
3735
+ this.position[1] = y;
3736
+ this.position[2] = z;
3737
+ }
3738
+ /**
3739
+ * Unproject a screen coordinate into a world-space ray.
3740
+ * Requires `setAspect(width, height)` to have been called first.
3741
+ * Returns a pre-allocated Ray3D — copy origin/direction if you need to store it.
3742
+ */
3743
+ screenToRay(screenX, screenY) {
3744
+ this.getViewMatrix();
3745
+ const m = this._viewMatrix;
3746
+ const ndcX = 2 * screenX / this._width - 1;
3747
+ const ndcY = 1 - 2 * screenY / this._height;
3748
+ const t = Math.tan(this.fov * Math.PI / 180 * 0.5);
3749
+ const rightX = m[0], rightY = m[4], rightZ = m[8];
3750
+ const upX = m[1], upY = m[5], upZ = m[9];
3751
+ const fwdX = -m[2], fwdY = -m[6], fwdZ = -m[10];
3752
+ const dx = fwdX + rightX * ndcX * t * this.aspect + upX * ndcY * t;
3753
+ const dy = fwdY + rightY * ndcX * t * this.aspect + upY * ndcY * t;
3754
+ const dz = fwdZ + rightZ * ndcX * t * this.aspect + upZ * ndcY * t;
3755
+ this._ray.set(this.position[0], this.position[1], this.position[2], dx, dy, dz);
3756
+ return this._ray;
3757
+ }
3758
+ setTarget(x, y, z) {
3759
+ this.target[0] = x;
3760
+ this.target[1] = y;
3761
+ this.target[2] = z;
3762
+ }
3763
+ /**
3764
+ * Move the camera. Behaviour is determined by the `movement` property:
3765
+ * - `'local'` — along camera axes, pitch included (free-fly / spectator)
3766
+ * - `'grounded'` — yaw-projected XZ + world Y (FPS)
3767
+ * - `'global'` — world axes directly (isometric / platformer)
3768
+ */
3769
+ move(right, up, forward) {
3770
+ if (this.movement === "grounded")
3771
+ this._moveGrounded(right, up, forward);
3772
+ else if (this.movement === "global")
3773
+ this._moveGlobal(right, up, forward);
3774
+ else
3775
+ this._moveLocal(right, up, forward);
3776
+ }
3777
+ _moveLocal(right, up, forward) {
3778
+ this.getViewMatrix();
3779
+ const m = this._viewMatrix;
3780
+ const dx = m[0] * right + m[1] * up - m[2] * forward;
3781
+ const dy = m[4] * right + m[5] * up - m[6] * forward;
3782
+ const dz = m[8] * right + m[9] * up - m[10] * forward;
3783
+ this.position[0] += dx;
3784
+ this.position[1] += dy;
3785
+ this.position[2] += dz;
3786
+ this.target[0] += dx;
3787
+ this.target[1] += dy;
3788
+ this.target[2] += dz;
3789
+ }
3790
+ _moveGrounded(right, up, forward) {
3791
+ const yaw = Math.atan2(this.target[0] - this.position[0], this.target[2] - this.position[2]);
3792
+ const dx = Math.sin(yaw) * forward + Math.cos(yaw) * right;
3793
+ const dz = Math.cos(yaw) * forward - Math.sin(yaw) * right;
3794
+ this.position[0] += dx;
3795
+ this.position[1] += up;
3796
+ this.position[2] += dz;
3797
+ this.target[0] += dx;
3798
+ this.target[1] += up;
3799
+ this.target[2] += dz;
3800
+ }
3801
+ _moveGlobal(right, up, forward) {
3802
+ this.position[0] += right;
3803
+ this.position[1] += up;
3804
+ this.position[2] += forward;
3805
+ this.target[0] += right;
3806
+ this.target[1] += up;
3807
+ this.target[2] += forward;
3808
+ }
3809
+ /**
3810
+ * Orbit around the target point. Zero allocations.
3811
+ * @param yawDelta Horizontal rotation in radians (positive = rotate right)
3812
+ * @param pitchDelta Vertical rotation in radians (positive = rotate up)
3813
+ */
3814
+ orbit(yawDelta, pitchDelta) {
3815
+ let ox = this.position[0] - this.target[0];
3816
+ let oy = this.position[1] - this.target[1];
3817
+ let oz = this.position[2] - this.target[2];
3818
+ const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
3819
+ let yaw = Math.atan2(ox, oz);
3820
+ let pitch = Math.asin(oy / dist);
3821
+ yaw += yawDelta;
3822
+ pitch += pitchDelta;
3823
+ pitch = Math.max(-Math.PI * 0.49, Math.min(Math.PI * 0.49, pitch));
3824
+ this.position[0] = this.target[0] + Math.sin(yaw) * Math.cos(pitch) * dist;
3825
+ this.position[1] = this.target[1] + Math.sin(pitch) * dist;
3826
+ this.position[2] = this.target[2] + Math.cos(yaw) * Math.cos(pitch) * dist;
3827
+ }
3828
+ /**
3829
+ * Zoom by adjusting distance to target. Zero allocations.
3830
+ * @param delta Positive = zoom in, negative = zoom out
3831
+ */
3832
+ zoom(delta) {
3833
+ let ox = this.position[0] - this.target[0];
3834
+ let oy = this.position[1] - this.target[1];
3835
+ let oz = this.position[2] - this.target[2];
3836
+ const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
3837
+ const newDist = Math.max(0.1, dist - delta);
3838
+ const scale = newDist / dist;
3839
+ this.position[0] = this.target[0] + ox * scale;
3840
+ this.position[1] = this.target[1] + oy * scale;
3841
+ this.position[2] = this.target[2] + oz * scale;
3842
+ }
3843
+ };
3844
+ function lookAt(out, eye, center, up) {
3845
+ let fx = center[0] - eye[0];
3846
+ let fy = center[1] - eye[1];
3847
+ let fz = center[2] - eye[2];
3848
+ let len = 1 / Math.sqrt(fx * fx + fy * fy + fz * fz);
3849
+ fx *= len;
3850
+ fy *= len;
3851
+ fz *= len;
3852
+ let sx = fy * up[2] - fz * up[1];
3853
+ let sy = fz * up[0] - fx * up[2];
3854
+ let sz = fx * up[1] - fy * up[0];
3855
+ len = Math.sqrt(sx * sx + sy * sy + sz * sz);
3856
+ if (len > 0) {
3857
+ len = 1 / len;
3858
+ sx *= len;
3859
+ sy *= len;
3860
+ sz *= len;
3861
+ }
3862
+ const ux = sy * fz - sz * fy;
3863
+ const uy = sz * fx - sx * fz;
3864
+ const uz = sx * fy - sy * fx;
3865
+ out[0] = sx;
3866
+ out[1] = ux;
3867
+ out[2] = -fx;
3868
+ out[3] = 0;
3869
+ out[4] = sy;
3870
+ out[5] = uy;
3871
+ out[6] = -fy;
3872
+ out[7] = 0;
3873
+ out[8] = sz;
3874
+ out[9] = uz;
3875
+ out[10] = -fz;
3876
+ out[11] = 0;
3877
+ out[12] = -(sx * eye[0] + sy * eye[1] + sz * eye[2]);
3878
+ out[13] = -(ux * eye[0] + uy * eye[1] + uz * eye[2]);
3879
+ out[14] = fx * eye[0] + fy * eye[1] + fz * eye[2];
3880
+ out[15] = 1;
3881
+ }
3882
+ function perspective(out, fovRad, aspect, near, far) {
3883
+ const f = 1 / Math.tan(fovRad * 0.5);
3884
+ const rangeInv = 1 / (near - far);
3885
+ out[0] = f / aspect;
3886
+ out[1] = 0;
3887
+ out[2] = 0;
3888
+ out[3] = 0;
3889
+ out[4] = 0;
3890
+ out[5] = f;
3891
+ out[6] = 0;
3892
+ out[7] = 0;
3893
+ out[8] = 0;
3894
+ out[9] = 0;
3895
+ out[10] = (near + far) * rangeInv;
3896
+ out[11] = -1;
3897
+ out[12] = 0;
3898
+ out[13] = 0;
3899
+ out[14] = 2 * near * far * rangeInv;
3900
+ out[15] = 0;
3901
+ }
3902
+ function mat4Multiply(out, a, b) {
3903
+ for (let i = 0; i < 4; i++) {
3904
+ const ai0 = a[i], ai1 = a[i + 4], ai2 = a[i + 8], ai3 = a[i + 12];
3905
+ out[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3];
3906
+ out[i + 4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7];
3907
+ out[i + 8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11];
3908
+ out[i + 12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15];
3909
+ }
3910
+ }
3265
3911
 
3266
3912
  // src/3d/renderer.ts
3267
- var import_renderer5 = require("murow/renderer");
3913
+ var import_renderer7 = require("murow/renderer");
3914
+ var import_hitbox3 = require("murow/core/hitbox");
3268
3915
 
3269
3916
  // src/3d/skeletal-animation-compute/packer.ts
3270
3917
  function packAnimationData(packed) {
@@ -3464,8 +4111,8 @@ function buildAnimationKernel(root, packed, maxInstances, maxTotalBones, budgets
3464
4111
  let by = animF32[offB + 1];
3465
4112
  let bz = animF32[offB + 2];
3466
4113
  let bw = animF32[offB + 3];
3467
- const dot2 = ax * bx + ay * by + az * bz + aw * bw;
3468
- if (dot2 < 0) {
4114
+ const dot3 = ax * bx + ay * by + az * bz + aw * bw;
4115
+ if (dot3 < 0) {
3469
4116
  bx = -bx;
3470
4117
  by = -by;
3471
4118
  bz = -bz;
@@ -3740,6 +4387,327 @@ var GltfClipResyncCoordinator = class {
3740
4387
  }
3741
4388
  };
3742
4389
 
4390
+ // src/3d/raycast.ts
4391
+ var import_raycast3 = require("murow/core/raycast");
4392
+ var import_renderer5 = require("murow/renderer");
4393
+ var WebGPURaycast3D = class extends import_renderer5.Raycast {
4394
+ constructor(renderer) {
4395
+ super();
4396
+ this.renderer = renderer;
4397
+ this.state = new import_raycast3.HitBuffer(3);
4398
+ this.resultBuffer = [];
4399
+ this.memos = /* @__PURE__ */ new Set();
4400
+ }
4401
+ update(input) {
4402
+ this.state.reset();
4403
+ this.renderer._collectRaycastHitsInto(input.mouse.position.x, input.mouse.position.y, this.state);
4404
+ for (const m of this.memos)
4405
+ m._invalidate();
4406
+ }
4407
+ /**
4408
+ * Nearest hit, or null. The returned object is pool-backed and valid
4409
+ * only until the next `update()` -- copy what you need, or use `memo`
4410
+ * for results that persist across frames.
4411
+ */
4412
+ hit(opts) {
4413
+ return this.state.nearest(opts?.filter, opts?.maxDistance ?? Infinity);
4414
+ }
4415
+ /**
4416
+ * All hits, nearest first. The array and its entries are reused across
4417
+ * calls and overwritten by the next `update()`; do not retain them.
4418
+ */
4419
+ hitAll(opts) {
4420
+ this.state.collectInto(this.resultBuffer, opts?.filter, opts?.maxDistance ?? Infinity);
4421
+ return this.resultBuffer;
4422
+ }
4423
+ memo(opts) {
4424
+ const m = new WebGPURaycastMemo3D(this.state, opts, () => this.memos.delete(m));
4425
+ this.memos.add(m);
4426
+ return m;
4427
+ }
4428
+ clearMemos() {
4429
+ for (const m of this.memos)
4430
+ m._detach();
4431
+ this.memos.clear();
4432
+ }
4433
+ };
4434
+ var WebGPURaycastMemo3D = class extends import_renderer5.RaycastMemo {
4435
+ constructor(state, opts, onDispose) {
4436
+ super();
4437
+ this.state = state;
4438
+ this.opts = opts;
4439
+ this.onDispose = onDispose;
4440
+ this.dirty = true;
4441
+ this.detached = false;
4442
+ this.cached = [];
4443
+ }
4444
+ get hits() {
4445
+ if (this.detached)
4446
+ return this.cached;
4447
+ if (this.dirty) {
4448
+ this.state.collectInto(this.cached, this.opts.filter, this.opts.maxDistance ?? Infinity);
4449
+ this.dirty = false;
4450
+ }
4451
+ return this.cached;
4452
+ }
4453
+ get first() {
4454
+ const arr = this.hits;
4455
+ return arr.length > 0 ? arr[0] : null;
4456
+ }
4457
+ dispose() {
4458
+ if (this.detached)
4459
+ return;
4460
+ this.onDispose();
4461
+ this._detach();
4462
+ }
4463
+ _invalidate() {
4464
+ this.dirty = true;
4465
+ }
4466
+ _detach() {
4467
+ this.detached = true;
4468
+ this.cached.length = 0;
4469
+ }
4470
+ };
4471
+
4472
+ // src/3d/hitbox.ts
4473
+ var import_hitbox2 = require("murow/core/hitbox");
4474
+ function buildUnitSphereWireframe(segments = 16) {
4475
+ const out = [];
4476
+ const step2 = Math.PI * 2 / segments;
4477
+ for (let axis = 0; axis < 3; axis++) {
4478
+ for (let i = 0; i < segments; i++) {
4479
+ const a = i * step2;
4480
+ const b = (i + 1) * step2;
4481
+ const ca = Math.cos(a), sa = Math.sin(a);
4482
+ const cb = Math.cos(b), sb = Math.sin(b);
4483
+ if (axis === 0) {
4484
+ out.push(0, ca, sa, 0, cb, sb);
4485
+ } else if (axis === 1) {
4486
+ out.push(ca, 0, sa, cb, 0, sb);
4487
+ } else {
4488
+ out.push(ca, sa, 0, cb, sb, 0);
4489
+ }
4490
+ }
4491
+ }
4492
+ return new Float32Array(out);
4493
+ }
4494
+ function buildUnitBoxWireframe() {
4495
+ const h = 0.5;
4496
+ const corners = [
4497
+ [-h, -h, -h],
4498
+ [h, -h, -h],
4499
+ [h, h, -h],
4500
+ [-h, h, -h],
4501
+ [-h, -h, h],
4502
+ [h, -h, h],
4503
+ [h, h, h],
4504
+ [-h, h, h]
4505
+ ];
4506
+ const edges = [
4507
+ [0, 1],
4508
+ [1, 2],
4509
+ [2, 3],
4510
+ [3, 0],
4511
+ [4, 5],
4512
+ [5, 6],
4513
+ [6, 7],
4514
+ [7, 4],
4515
+ [0, 4],
4516
+ [1, 5],
4517
+ [2, 6],
4518
+ [3, 7]
4519
+ ];
4520
+ const out = [];
4521
+ for (const [a, b] of edges) {
4522
+ out.push(...corners[a], ...corners[b]);
4523
+ }
4524
+ return new Float32Array(out);
4525
+ }
4526
+ function buildUnitCylinderWireframe(segments = 24) {
4527
+ const h = 0.5;
4528
+ const step2 = Math.PI * 2 / segments;
4529
+ const out = [];
4530
+ for (let i = 0; i < segments; i++) {
4531
+ const a = i * step2, b = (i + 1) * step2;
4532
+ const ca = Math.cos(a), sa = Math.sin(a);
4533
+ const cb = Math.cos(b), sb = Math.sin(b);
4534
+ out.push(ca, -h, sa, cb, -h, sb);
4535
+ out.push(ca, h, sa, cb, h, sb);
4536
+ }
4537
+ for (let i = 0; i < 4; i++) {
4538
+ const a = i * (Math.PI * 0.5);
4539
+ const ca = Math.cos(a), sa = Math.sin(a);
4540
+ out.push(ca, -h, sa, ca, h, sa);
4541
+ }
4542
+ return new Float32Array(out);
4543
+ }
4544
+ var IDLE = [1, 0, 1, 1];
4545
+ var HOVERED = [0.2, 1, 0.4, 1];
4546
+ var UNIFORM_STRIDE = 256;
4547
+ var MIN_BINDING_SIZE = 80;
4548
+ var CAPACITY = 4096;
4549
+ var HitboxDebugRenderer = class {
4550
+ constructor() {
4551
+ this.device = null;
4552
+ this.pipeline = null;
4553
+ this.bindGroup = null;
4554
+ this.uniformBuffer = null;
4555
+ this.sphereBuffer = null;
4556
+ this.sphereVertexCount = 0;
4557
+ this.boxBuffer = null;
4558
+ this.boxVertexCount = 0;
4559
+ this.cylinderBuffer = null;
4560
+ this.cylinderVertexCount = 0;
4561
+ this.stage = new Float32Array(0);
4562
+ this.entries = [];
4563
+ this.vp = new Float32Array(16);
4564
+ }
4565
+ init(device, format) {
4566
+ this.device = device;
4567
+ const upload = (data) => {
4568
+ const buf = device.createBuffer({
4569
+ size: data.byteLength,
4570
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4571
+ });
4572
+ device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
4573
+ return buf;
4574
+ };
4575
+ const sphereData = buildUnitSphereWireframe();
4576
+ const boxData = buildUnitBoxWireframe();
4577
+ const cylinderData = buildUnitCylinderWireframe();
4578
+ this.sphereBuffer = upload(sphereData);
4579
+ this.sphereVertexCount = sphereData.length / 3;
4580
+ this.boxBuffer = upload(boxData);
4581
+ this.boxVertexCount = boxData.length / 3;
4582
+ this.cylinderBuffer = upload(cylinderData);
4583
+ this.cylinderVertexCount = cylinderData.length / 3;
4584
+ this.uniformBuffer = device.createBuffer({
4585
+ size: UNIFORM_STRIDE * CAPACITY,
4586
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
4587
+ });
4588
+ this.stage = new Float32Array(UNIFORM_STRIDE * CAPACITY / 4);
4589
+ const bindGroupLayout = device.createBindGroupLayout({
4590
+ entries: [{
4591
+ binding: 0,
4592
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4593
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: MIN_BINDING_SIZE }
4594
+ }]
4595
+ });
4596
+ this.bindGroup = device.createBindGroup({
4597
+ layout: bindGroupLayout,
4598
+ entries: [{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: MIN_BINDING_SIZE } }]
4599
+ });
4600
+ const shaderModule = device.createShaderModule({
4601
+ code: `
4602
+ struct Uniforms {
4603
+ mvp: mat4x4<f32>,
4604
+ color: vec4<f32>,
4605
+ };
4606
+ @group(0) @binding(0) var<uniform> u: Uniforms;
4607
+ @vertex
4608
+ fn vs(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
4609
+ return u.mvp * vec4<f32>(p, 1.0);
4610
+ }
4611
+ @fragment
4612
+ fn fs() -> @location(0) vec4<f32> {
4613
+ return u.color;
4614
+ }
4615
+ `
4616
+ });
4617
+ this.pipeline = device.createRenderPipeline({
4618
+ layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
4619
+ vertex: {
4620
+ module: shaderModule,
4621
+ entryPoint: "vs",
4622
+ buffers: [{
4623
+ arrayStride: 12,
4624
+ attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }]
4625
+ }]
4626
+ },
4627
+ fragment: { module: shaderModule, entryPoint: "fs", targets: [{ format }] },
4628
+ primitive: { topology: "line-list" },
4629
+ depthStencil: {
4630
+ format: "depth24plus",
4631
+ depthWriteEnabled: false,
4632
+ depthCompare: "less-equal"
4633
+ }
4634
+ });
4635
+ }
4636
+ begin(vp) {
4637
+ this.entries.length = 0;
4638
+ this.vp = vp;
4639
+ }
4640
+ emit(hb, hovered, px, py, pz, sx, sy, sz) {
4641
+ const p = (0, import_hitbox2.placePart3D)(hb, px, py, pz, sx, sy, sz);
4642
+ const color = hovered ? HOVERED : IDLE;
4643
+ if (hb.shape === "sphere") {
4644
+ this.collect(this.sphereBuffer, this.sphereVertexCount, p.cx, p.cy, p.cz, p.hx, p.hx, p.hx, color);
4645
+ } else if (hb.shape === "box") {
4646
+ this.collect(this.boxBuffer, this.boxVertexCount, p.cx, p.cy, p.cz, p.hx * 2, p.hy * 2, p.hz * 2, color);
4647
+ } else {
4648
+ this.collect(this.cylinderBuffer, this.cylinderVertexCount, p.cx, p.cy, p.cz, p.hx, p.hy * 2, p.hz, color);
4649
+ }
4650
+ }
4651
+ flush(pass) {
4652
+ const { pipeline, bindGroup, uniformBuffer, entries } = this;
4653
+ if (!pipeline || !bindGroup || !uniformBuffer || entries.length === 0)
4654
+ return;
4655
+ this.device.queue.writeBuffer(
4656
+ uniformBuffer,
4657
+ 0,
4658
+ this.stage.buffer,
4659
+ 0,
4660
+ entries.length * UNIFORM_STRIDE
4661
+ );
4662
+ pass.setPipeline(pipeline);
4663
+ let currentVbo = null;
4664
+ for (const e of entries) {
4665
+ if (e.vbo !== currentVbo) {
4666
+ pass.setVertexBuffer(0, e.vbo);
4667
+ currentVbo = e.vbo;
4668
+ }
4669
+ pass.setBindGroup(0, bindGroup, [e.offset]);
4670
+ pass.draw(e.vertexCount, 1, 0, 0);
4671
+ }
4672
+ }
4673
+ /**
4674
+ * The model matrix is pure scale-then-translate, so `MVP = VP * M`
4675
+ * collapses to scaling VP's first three columns by the extents and
4676
+ * replacing the fourth with `VP * (center, 1)` -- no matrix multiply.
4677
+ */
4678
+ collect(vbo, vertexCount, cx, cy, cz, ex, ey, ez, color) {
4679
+ if (!vbo || vertexCount === 0)
4680
+ return;
4681
+ if (this.entries.length >= CAPACITY)
4682
+ return;
4683
+ const idx = this.entries.length;
4684
+ const base = idx * UNIFORM_STRIDE >>> 2;
4685
+ const f324 = this.stage;
4686
+ const vp = this.vp;
4687
+ f324[base + 0] = vp[0] * ex;
4688
+ f324[base + 1] = vp[1] * ex;
4689
+ f324[base + 2] = vp[2] * ex;
4690
+ f324[base + 3] = vp[3] * ex;
4691
+ f324[base + 4] = vp[4] * ey;
4692
+ f324[base + 5] = vp[5] * ey;
4693
+ f324[base + 6] = vp[6] * ey;
4694
+ f324[base + 7] = vp[7] * ey;
4695
+ f324[base + 8] = vp[8] * ez;
4696
+ f324[base + 9] = vp[9] * ez;
4697
+ f324[base + 10] = vp[10] * ez;
4698
+ f324[base + 11] = vp[11] * ez;
4699
+ f324[base + 12] = vp[0] * cx + vp[4] * cy + vp[8] * cz + vp[12];
4700
+ f324[base + 13] = vp[1] * cx + vp[5] * cy + vp[9] * cz + vp[13];
4701
+ f324[base + 14] = vp[2] * cx + vp[6] * cy + vp[10] * cz + vp[14];
4702
+ f324[base + 15] = vp[3] * cx + vp[7] * cy + vp[11] * cz + vp[15];
4703
+ f324[base + 16] = color[0];
4704
+ f324[base + 17] = color[1];
4705
+ f324[base + 18] = color[2];
4706
+ f324[base + 19] = color[3];
4707
+ this.entries.push({ vbo, vertexCount, offset: idx * UNIFORM_STRIDE });
4708
+ }
4709
+ };
4710
+
3743
4711
  // src/3d/renderer.ts
3744
4712
  var DYN_PREV_PX = 0;
3745
4713
  var DYN_PREV_PY = 1;
@@ -3801,7 +4769,7 @@ function computeBucketStats(bucket) {
3801
4769
  }
3802
4770
  return { maxSkinnedParts, maxJointCount };
3803
4771
  }
3804
- var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4772
+ var WebGPU3DRenderer = class extends import_renderer6.Base3DRenderer {
3805
4773
  constructor(canvas, options) {
3806
4774
  const resolvedMaxInstances = options.maxInstances ?? (options.prefabs ? options.prefabs.size + 16 : 32);
3807
4775
  super(canvas, { ...options, maxInstances: resolvedMaxInstances });
@@ -3809,6 +4777,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3809
4777
  this.resizeCallbacks = [];
3810
4778
  // layer=0, sheetId=modelId
3811
4779
  this.staticDirty = false;
4780
+ this.nextInstanceId = 0;
3812
4781
  // Models (vertex + index buffers)
3813
4782
  this.models = [];
3814
4783
  this.nextModelId = 0;
@@ -3818,7 +4787,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3818
4787
  this.clipResync = null;
3819
4788
  this.boneMatrixDirty = true;
3820
4789
  // GPU animation compute
3821
- this.packedAnimData = (0, import_renderer5.createPackedAnimationData)();
4790
+ this.packedAnimData = (0, import_renderer7.createPackedAnimationData)();
3822
4791
  this.animComputeKernel = null;
3823
4792
  this.animComputeNeedsRebuild = false;
3824
4793
  // Capacities the active kernel was built with — used to gate the in-place upload path on resync.
@@ -3834,10 +4803,15 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3834
4803
  this.freedBoneOffsets = /* @__PURE__ */ new Map();
3835
4804
  // Frustum planes (6 planes × 4 floats each), extracted from VP matrix
3836
4805
  this.frustumPlanes = new Float32Array(24);
3837
- this.uniformData = new Float32Array(24);
3838
- // mat4x4 (16) + alpha (1) + lightDir (3) + padding (4)
4806
+ // Dynamic lights — CPU state (SoA, slots, globals) lives in LightSystem;
4807
+ // the renderer owns only the GPU buffer it packs into each frame.
4808
+ this.lights = new LightSystem(MAX_LIGHTS);
4809
+ this.uniformData = new Float32Array(MESH_UNIFORM_FLOATS);
3839
4810
  this.lastRenderTime = 0;
4811
+ this.debug = { hitboxes: false };
4812
+ this.hitboxDebug = new HitboxDebugRenderer();
3840
4813
  this.camera = new Camera3D();
4814
+ this.raycast = new WebGPURaycast3D(this);
3841
4815
  this._prefabs = options.prefabs ?? null;
3842
4816
  const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
3843
4817
  const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
@@ -3849,14 +4823,15 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3849
4823
  this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
3850
4824
  this.boneOffsetRefcount = new Uint32Array(this.maxTotalBones);
3851
4825
  this.boneOffsetSkinIndex = new Uint32Array(this.maxTotalBones);
3852
- this.freeList = new import_free_list3.FreeList(resolvedMaxInstances);
4826
+ this.freeList = new import_free_list2.FreeList(resolvedMaxInstances);
3853
4827
  this.batcher = new import_sparse_batcher2.SparseBatcher(resolvedMaxInstances);
3854
4828
  this.dynamicData = new Float32Array(resolvedMaxInstances * DYNAMIC_MESH_FLOATS);
3855
4829
  this.staticData = new Float32Array(resolvedMaxInstances * STATIC_MESH_FLOATS);
3856
4830
  this.slotIndexData = new Uint32Array(resolvedMaxInstances);
3857
4831
  this.instanceModelIds = new Uint8Array(resolvedMaxInstances);
4832
+ this.instanceHandles = new Array(resolvedMaxInstances).fill(null);
3858
4833
  const msi = this.maxSkinnedInstances;
3859
- this.skinnedFreeList = new import_free_list3.FreeList(msi);
4834
+ this.skinnedFreeList = new import_free_list2.FreeList(msi);
3860
4835
  this.skinnedBatcher = new import_sparse_batcher2.SparseBatcher(msi);
3861
4836
  this.skinnedDynamicData = new Float32Array(msi * DYNAMIC_MESH_FLOATS);
3862
4837
  this.skinnedStaticData = new Float32Array(msi * SKINNED_STATIC_MESH_FLOATS);
@@ -3865,6 +4840,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3865
4840
  this.skinnedInstanceModelIds = new Uint8Array(msi);
3866
4841
  this.skinnedInstanceBoneOffsets = new Uint32Array(msi);
3867
4842
  this.skinnedAnimStates = new Array(msi).fill(null);
4843
+ this.skinnedInstanceHandles = new Array(msi).fill(null);
3868
4844
  this.boneMatrixData = new Float32Array(this.maxTotalBones * 16);
3869
4845
  const instBufSize = msi * 8;
3870
4846
  this.gpuInstData = new Float32Array(instBufSize);
@@ -3883,7 +4859,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3883
4859
  maxComputeInvocationsPerWorkgroup: a.maxComputeInvocationsPerWorkgroup
3884
4860
  };
3885
4861
  const device = await adapter.requestDevice({ requiredLimits });
3886
- this.root = import_typegpu9.default.initFromDevice({ device });
4862
+ this.root = import_typegpu10.default.initFromDevice({ device });
3887
4863
  this.device = this.root.device;
3888
4864
  this.context = this.canvas.getContext("webgpu");
3889
4865
  this.format = navigator.gpu.getPreferredCanvasFormat();
@@ -3894,7 +4870,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3894
4870
  });
3895
4871
  this._width = this.canvas.width;
3896
4872
  this._height = this.canvas.height;
3897
- this.camera.aspect = this._width / this._height;
4873
+ this.camera.setAspect(this.canvas.clientWidth || this._width, this.canvas.clientHeight || this._height);
3898
4874
  this.depthTexture = this.device.createTexture({
3899
4875
  size: [this._width, this._height],
3900
4876
  format: "depth24plus",
@@ -3924,7 +4900,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3924
4900
  };
3925
4901
  const vertex = createMeshVertex(this.meshLayout);
3926
4902
  const fragment = createMeshFragment(this.meshLayout);
3927
- const { code: wgslCode } = import_typegpu9.default.resolveWithContext([vertex, fragment]);
4903
+ const { code: wgslCode } = import_typegpu10.default.resolveWithContext([vertex, fragment]);
3928
4904
  const shaderModule = this.device.createShaderModule({ code: wgslCode });
3929
4905
  const rawBGL = this.root.unwrap(this.meshLayout);
3930
4906
  this.rawPipeline = this.device.createRenderPipeline({
@@ -3937,7 +4913,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3937
4913
  const texLayout = createTextureBindGroupLayout();
3938
4914
  const texVertex = createTexturedMeshVertex(this.meshLayout);
3939
4915
  const texFragment = createTexturedMeshFragment(this.meshLayout, texLayout);
3940
- const { code: texWgslCode } = import_typegpu9.default.resolveWithContext([texVertex, texFragment]);
4916
+ const { code: texWgslCode } = import_typegpu10.default.resolveWithContext([texVertex, texFragment]);
3941
4917
  const texShaderModule = this.device.createShaderModule({ code: texWgslCode });
3942
4918
  const rawTexBGL = this.root.unwrap(texLayout);
3943
4919
  this.rawTexturedPipeline = this.device.createRenderPipeline({
@@ -3951,17 +4927,20 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3951
4927
  this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxInstances)).$usage("storage");
3952
4928
  this.uniformBuffer = this.root.createBuffer(MeshUniforms).$usage("uniform");
3953
4929
  this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxInstances)).$usage("storage");
4930
+ this.lightBuffer = this.root.createBuffer(d.arrayOf(Light, MAX_LIGHTS)).$usage("storage");
3954
4931
  this.rawDynamicBuffer = this.root.unwrap(this.dynamicBuffer);
3955
4932
  this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
3956
4933
  this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
3957
4934
  this.rawSlotIndexBuffer = this.root.unwrap(this.slotIndexBuffer);
4935
+ this.rawLightBuffer = this.root.unwrap(this.lightBuffer);
3958
4936
  this.rawBindGroup = this.device.createBindGroup({
3959
4937
  layout: rawBGL,
3960
4938
  entries: [
3961
4939
  { binding: 0, resource: { buffer: this.rawUniformBuffer } },
3962
4940
  { binding: 1, resource: { buffer: this.rawDynamicBuffer } },
3963
4941
  { binding: 2, resource: { buffer: this.rawStaticBuffer } },
3964
- { binding: 3, resource: { buffer: this.rawSlotIndexBuffer } }
4942
+ { binding: 3, resource: { buffer: this.rawSlotIndexBuffer } },
4943
+ { binding: 4, resource: { buffer: this.rawLightBuffer } }
3965
4944
  ]
3966
4945
  });
3967
4946
  const msi = this.maxSkinnedInstances;
@@ -3983,8 +4962,8 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3983
4962
  ]
3984
4963
  };
3985
4964
  const skinnedVertex = createSkinnedMeshVertex(this.skinnedMeshLayout);
3986
- const skinnedFragment = createMeshFragment(this.skinnedMeshLayout);
3987
- const { code: skinnedWgsl } = import_typegpu9.default.resolveWithContext([skinnedVertex, skinnedFragment]);
4965
+ const skinnedFragment = createSkinnedMeshFragment(this.skinnedMeshLayout);
4966
+ const { code: skinnedWgsl } = import_typegpu10.default.resolveWithContext([skinnedVertex, skinnedFragment]);
3988
4967
  const skinnedShaderModule = this.device.createShaderModule({ code: skinnedWgsl });
3989
4968
  const rawSkinnedBGL = this.root.unwrap(this.skinnedMeshLayout);
3990
4969
  this.rawSkinnedPipeline = this.device.createRenderPipeline({
@@ -3996,7 +4975,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3996
4975
  });
3997
4976
  const skinnedTexVertex = createSkinnedMeshVertex(this.skinnedMeshLayout);
3998
4977
  const skinnedTexFragment = createTexturedMeshFragment(this.skinnedMeshLayout, texLayout);
3999
- const { code: skinnedTexWgsl } = import_typegpu9.default.resolveWithContext([skinnedTexVertex, skinnedTexFragment]);
4978
+ const { code: skinnedTexWgsl } = import_typegpu10.default.resolveWithContext([skinnedTexVertex, skinnedTexFragment]);
4000
4979
  const skinnedTexShaderModule = this.device.createShaderModule({ code: skinnedTexWgsl });
4001
4980
  this.rawSkinnedTexturedPipeline = this.device.createRenderPipeline({
4002
4981
  layout: this.device.createPipelineLayout({ bindGroupLayouts: [rawSkinnedBGL, rawTexBGL] }),
@@ -4020,12 +4999,14 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4020
4999
  { binding: 1, resource: { buffer: this.rawSkinnedDynamicBuffer } },
4021
5000
  { binding: 2, resource: { buffer: this.rawSkinnedStaticBuffer } },
4022
5001
  { binding: 3, resource: { buffer: this.rawSkinnedSlotIndexBuffer } },
4023
- { binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } }
5002
+ { binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } },
5003
+ { binding: 5, resource: { buffer: this.rawLightBuffer } }
4024
5004
  ]
4025
5005
  });
4026
5006
  if (this._prefabs) {
4027
5007
  this.uploadPrefabBucket(this._prefabs);
4028
5008
  }
5009
+ this.hitboxDebug.init(this.device, this.format);
4029
5010
  this.setupResizeObserver();
4030
5011
  this._initialized = true;
4031
5012
  }
@@ -4095,7 +5076,10 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4095
5076
  alphaMode: "premultiplied"
4096
5077
  });
4097
5078
  }
4098
- this.camera.aspect = w / h;
5079
+ const cssBox = entry.contentBoxSize?.[0];
5080
+ const cssW = cssBox ? cssBox.inlineSize : w;
5081
+ const cssH = cssBox ? cssBox.blockSize : h;
5082
+ this.camera.setAspect(cssW, cssH);
4099
5083
  this.depthTexture.destroy();
4100
5084
  this.depthTexture = this.device.createTexture({
4101
5085
  size: [w, h],
@@ -4149,6 +5133,33 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4149
5133
  get maxSkinned() {
4150
5134
  return this.maxSkinnedInstances;
4151
5135
  }
5136
+ /**
5137
+ * Add a dynamic point or spot light. Returns a live handle whose position,
5138
+ * color, intensity, range, and enabled state can all be changed every frame.
5139
+ * Up to `MAX_LIGHTS` (64) lights may be live at once; throws past that.
5140
+ *
5141
+ * The global directional + ambient terms are separate — see
5142
+ * `setDirectionalLight` / `setAmbient`.
5143
+ */
5144
+ addLight(spec) {
5145
+ return this.lights.add(spec);
5146
+ }
5147
+ /**
5148
+ * Set the global directional light (the "sun"). `direction` points from the
5149
+ * surface toward the light. Defaults to `(0.3, 0.8, 0.5)`, white, intensity 1
5150
+ * — the engine's classic fixed look.
5151
+ */
5152
+ setDirectionalLight(direction, color = [1, 1, 1], intensity = 1) {
5153
+ this.lights.setDirectional(direction, color, intensity);
5154
+ }
5155
+ /** Set the global ambient term. Defaults to `(0.3, 0.3, 0.3)`. */
5156
+ setAmbient(color) {
5157
+ this.lights.setAmbient(color);
5158
+ }
5159
+ /** Number of live dynamic lights. */
5160
+ get lightCount() {
5161
+ return this.lights.count;
5162
+ }
4152
5163
  /**
4153
5164
  * Create a flat grid mesh on the XZ plane at Y=0.
4154
5165
  *
@@ -4439,13 +5450,30 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4439
5450
  const uvs = data.uvs ?? new Float32Array(vertexCount * 2);
4440
5451
  const hasTexture = !!texture;
4441
5452
  let maxRadiusSq = 0;
5453
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
5454
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
4442
5455
  for (let i = 0; i < vertexCount; i++) {
4443
5456
  const px = positions[i * 3], py = positions[i * 3 + 1], pz = positions[i * 3 + 2];
4444
5457
  const rSq = px * px + py * py + pz * pz;
4445
5458
  if (rSq > maxRadiusSq)
4446
5459
  maxRadiusSq = rSq;
5460
+ if (px < minX)
5461
+ minX = px;
5462
+ if (px > maxX)
5463
+ maxX = px;
5464
+ if (py < minY)
5465
+ minY = py;
5466
+ if (py > maxY)
5467
+ maxY = py;
5468
+ if (pz < minZ)
5469
+ minZ = pz;
5470
+ if (pz > maxZ)
5471
+ maxZ = pz;
4447
5472
  }
4448
5473
  const boundingRadius = Math.sqrt(maxRadiusSq);
5474
+ const halfX = vertexCount ? (maxX - minX) * 0.5 : 0;
5475
+ const halfY = vertexCount ? (maxY - minY) * 0.5 : 0;
5476
+ const halfZ = vertexCount ? (maxZ - minZ) * 0.5 : 0;
4449
5477
  const interleaved = new Float32Array(vertexCount * 8);
4450
5478
  for (let i = 0; i < vertexCount; i++) {
4451
5479
  const o = i * 8;
@@ -4506,6 +5534,9 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4506
5534
  indexCount,
4507
5535
  indexFormat: indices instanceof Uint32Array ? "uint32" : "uint16",
4508
5536
  boundingRadius,
5537
+ halfX,
5538
+ halfY,
5539
+ halfZ,
4509
5540
  hasTexture,
4510
5541
  textureBindGroup,
4511
5542
  skinned: false,
@@ -4524,13 +5555,30 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4524
5555
  const uvs = data.uvs ?? new Float32Array(vertexCount * 2);
4525
5556
  const hasTexture = !!texture;
4526
5557
  let maxRadiusSq = 0;
5558
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
5559
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
4527
5560
  for (let i = 0; i < vertexCount; i++) {
4528
5561
  const px = positions[i * 3], py = positions[i * 3 + 1], pz = positions[i * 3 + 2];
4529
5562
  const rSq = px * px + py * py + pz * pz;
4530
5563
  if (rSq > maxRadiusSq)
4531
5564
  maxRadiusSq = rSq;
5565
+ if (px < minX)
5566
+ minX = px;
5567
+ if (px > maxX)
5568
+ maxX = px;
5569
+ if (py < minY)
5570
+ minY = py;
5571
+ if (py > maxY)
5572
+ maxY = py;
5573
+ if (pz < minZ)
5574
+ minZ = pz;
5575
+ if (pz > maxZ)
5576
+ maxZ = pz;
4532
5577
  }
4533
5578
  const boundingRadius = Math.sqrt(maxRadiusSq);
5579
+ const halfX = vertexCount ? (maxX - minX) * 0.5 : 0;
5580
+ const halfY = vertexCount ? (maxY - minY) * 0.5 : 0;
5581
+ const halfZ = vertexCount ? (maxZ - minZ) * 0.5 : 0;
4534
5582
  const buf = new ArrayBuffer(vertexCount * 56);
4535
5583
  const floatView = new Float32Array(buf);
4536
5584
  const u16View = new Uint16Array(buf);
@@ -4602,6 +5650,9 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4602
5650
  indexCount,
4603
5651
  indexFormat: indices instanceof Uint32Array ? "uint32" : "uint16",
4604
5652
  boundingRadius,
5653
+ halfX,
5654
+ halfY,
5655
+ halfZ,
4605
5656
  hasTexture,
4606
5657
  textureBindGroup,
4607
5658
  skinned: true,
@@ -4660,7 +5711,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4660
5711
  * ```
4661
5712
  */
4662
5713
  async loadGltf(url, opts) {
4663
- const parsed = await (0, import_renderer5.parseGltf)(url, opts);
5714
+ const parsed = await (0, import_renderer7.parseGltf)(url, opts);
4664
5715
  return this.uploadParsedGltf(parsed);
4665
5716
  }
4666
5717
  /**
@@ -4673,7 +5724,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4673
5724
  let skinnedModelSkinIndex = -1;
4674
5725
  if (parsed.skin) {
4675
5726
  const { data: skinData, animClips } = parsed.skin;
4676
- const animation = new import_renderer5.SkeletalAnimation(skinData, animClips);
5727
+ const animation = new import_renderer7.SkeletalAnimation(skinData, animClips);
4677
5728
  const ibm = skinData.inverseBindMatrices;
4678
5729
  let maxRadSq = 0;
4679
5730
  for (let j = 0; j < skinData.jointCount; j++) {
@@ -4690,7 +5741,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4690
5741
  boundingRadius: skinnedRadius,
4691
5742
  parsedSkin: parsed.skin
4692
5743
  });
4693
- (0, import_renderer5.packSkinAndAnimations)(this.packedAnimData, skinData, animClips);
5744
+ (0, import_renderer7.packSkinAndAnimations)(this.packedAnimData, skinData, animClips);
4694
5745
  this.animComputeNeedsRebuild = true;
4695
5746
  }
4696
5747
  const handles = [];
@@ -4734,17 +5785,18 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4734
5785
  * with another instance (e.g., when spawning all parts of a character).
4735
5786
  */
4736
5787
  addInstance(opts) {
5788
+ const userPrefabId = isPrefab3D(opts.model) ? opts.model.id : null;
4737
5789
  if (isPrefab3D(opts.model) && opts.model.type === "composite") {
4738
5790
  return this.addCompositeInstance(opts, opts.model);
4739
5791
  }
4740
5792
  const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
4741
5793
  if ("parts" in modelOrGltf) {
4742
- return this.addGltfInstance(opts, modelOrGltf);
5794
+ return this.addGltfInstance(opts, modelOrGltf, userPrefabId);
4743
5795
  }
4744
5796
  const modelHandle = modelOrGltf;
4745
5797
  const model = this.models[modelHandle.id];
4746
5798
  if (model?.skinned) {
4747
- return this.addSkinnedInstance(opts, modelHandle, model.skinIndex);
5799
+ return this.addSkinnedInstance(opts, modelHandle, model.skinIndex, void 0, userPrefabId);
4748
5800
  }
4749
5801
  const slot = this.freeList.allocate();
4750
5802
  if (slot === -1)
@@ -4777,10 +5829,16 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4777
5829
  const staticData = this.staticData;
4778
5830
  const self = this;
4779
5831
  let destroyed = false;
5832
+ const posOut = [0, 0, 0];
5833
+ const rotOut = [0, 0, 0];
5834
+ const sclOut = [0, 0, 0];
5835
+ const id = ++this.nextInstanceId;
4780
5836
  const handle = {
5837
+ id,
4781
5838
  slot,
4782
5839
  modelId: modelHandle.id,
4783
5840
  skinned: false,
5841
+ prefabId: userPrefabId,
4784
5842
  setPosition(nx, ny, nz) {
4785
5843
  dynamicData[dynBase + DYN_CURR_PX] = nx;
4786
5844
  dynamicData[dynBase + DYN_CURR_PY] = ny;
@@ -4796,6 +5854,32 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4796
5854
  staticData[statBase + STAT_SY] = ny;
4797
5855
  staticData[statBase + STAT_SZ] = nz;
4798
5856
  },
5857
+ teleport(nx, ny, nz) {
5858
+ dynamicData[dynBase + DYN_PREV_PX] = nx;
5859
+ dynamicData[dynBase + DYN_PREV_PY] = ny;
5860
+ dynamicData[dynBase + DYN_PREV_PZ] = nz;
5861
+ dynamicData[dynBase + DYN_CURR_PX] = nx;
5862
+ dynamicData[dynBase + DYN_CURR_PY] = ny;
5863
+ dynamicData[dynBase + DYN_CURR_PZ] = nz;
5864
+ },
5865
+ get position() {
5866
+ posOut[0] = dynamicData[dynBase + DYN_CURR_PX];
5867
+ posOut[1] = dynamicData[dynBase + DYN_CURR_PY];
5868
+ posOut[2] = dynamicData[dynBase + DYN_CURR_PZ];
5869
+ return posOut;
5870
+ },
5871
+ get rotation() {
5872
+ rotOut[0] = dynamicData[dynBase + DYN_CURR_RX];
5873
+ rotOut[1] = dynamicData[dynBase + DYN_CURR_RY];
5874
+ rotOut[2] = dynamicData[dynBase + DYN_CURR_RZ];
5875
+ return rotOut;
5876
+ },
5877
+ get scale() {
5878
+ sclOut[0] = staticData[statBase + STAT_SX];
5879
+ sclOut[1] = staticData[statBase + STAT_SY];
5880
+ sclOut[2] = staticData[statBase + STAT_SZ];
5881
+ return sclOut;
5882
+ },
4799
5883
  destroy() {
4800
5884
  if (destroyed)
4801
5885
  return;
@@ -4804,12 +5888,14 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4804
5888
  self.freeList.free(slot);
4805
5889
  dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4806
5890
  staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
5891
+ self.instanceHandles[slot] = null;
4807
5892
  self.staticDirty = true;
4808
5893
  }
4809
5894
  };
5895
+ this.instanceHandles[slot] = handle;
4810
5896
  return handle;
4811
5897
  }
4812
- addGltfInstance(opts, gltf) {
5898
+ addGltfInstance(opts, gltf, prefabId) {
4813
5899
  const childHandles = [];
4814
5900
  let firstSkinnedSlot;
4815
5901
  for (const part of gltf.parts) {
@@ -4817,7 +5903,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4817
5903
  const model = this.models[part.id];
4818
5904
  let handle;
4819
5905
  if (model?.skinned) {
4820
- handle = this.addSkinnedInstance(partOpts, part, model.skinIndex, firstSkinnedSlot);
5906
+ handle = this.addSkinnedInstance(partOpts, part, model.skinIndex, firstSkinnedSlot, prefabId);
4821
5907
  if (firstSkinnedSlot === void 0)
4822
5908
  firstSkinnedSlot = handle.slot;
4823
5909
  } else {
@@ -4826,8 +5912,11 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4826
5912
  childHandles.push(handle);
4827
5913
  }
4828
5914
  const skinnedHandle = childHandles.find((h) => h.skinned);
5915
+ const lead = childHandles[0];
4829
5916
  return {
5917
+ id: lead.id,
4830
5918
  skinned: gltf.skinned,
5919
+ prefabId,
4831
5920
  setPosition(x, y, z) {
4832
5921
  for (const h of childHandles)
4833
5922
  h.setPosition(x, y, z);
@@ -4840,6 +5929,19 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4840
5929
  for (const h of childHandles)
4841
5930
  h.setScale(x, y, z);
4842
5931
  },
5932
+ teleport(x, y, z) {
5933
+ for (const h of childHandles)
5934
+ h.teleport(x, y, z);
5935
+ },
5936
+ get position() {
5937
+ return lead.position;
5938
+ },
5939
+ get rotation() {
5940
+ return lead.rotation;
5941
+ },
5942
+ get scale() {
5943
+ return lead.scale;
5944
+ },
4843
5945
  play: skinnedHandle?.play ? (name, opts2) => {
4844
5946
  skinnedHandle.play(name, opts2);
4845
5947
  } : void 0,
@@ -4876,6 +5978,17 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4876
5978
  rz: p.offset?.rotation?.[2] ?? 0
4877
5979
  }));
4878
5980
  const childHandles = [];
5981
+ const posOut = [basePos[0], basePos[1], basePos[2]];
5982
+ const rotOut = [baseRot[0], baseRot[1], baseRot[2]];
5983
+ const sclOut = [1, 1, 1];
5984
+ const initialScale = opts.scale;
5985
+ if (typeof initialScale === "number") {
5986
+ sclOut[0] = sclOut[1] = sclOut[2] = initialScale;
5987
+ } else if (initialScale) {
5988
+ sclOut[0] = initialScale[0];
5989
+ sclOut[1] = initialScale[1];
5990
+ sclOut[2] = initialScale[2];
5991
+ }
4879
5992
  for (let i = 0; i < composite.parts.length; i++) {
4880
5993
  const part = composite.parts[i];
4881
5994
  const off = offsets[i];
@@ -4889,30 +6002,59 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4889
6002
  childHandles.push(this.addInstance(partOpts));
4890
6003
  }
4891
6004
  return {
6005
+ id: childHandles[0].id,
4892
6006
  skinned: childHandles.some((h) => h.skinned),
6007
+ prefabId: composite.id,
4893
6008
  setPosition(x, y, z) {
6009
+ posOut[0] = x;
6010
+ posOut[1] = y;
6011
+ posOut[2] = z;
4894
6012
  for (let i = 0; i < childHandles.length; i++) {
4895
6013
  const o = offsets[i];
4896
6014
  childHandles[i].setPosition(x + o.px, y + o.py, z + o.pz);
4897
6015
  }
4898
6016
  },
4899
6017
  setRotation(x, y, z) {
6018
+ rotOut[0] = x;
6019
+ rotOut[1] = y;
6020
+ rotOut[2] = z;
4900
6021
  for (let i = 0; i < childHandles.length; i++) {
4901
6022
  const o = offsets[i];
4902
6023
  childHandles[i].setRotation(x + o.rx, y + o.ry, z + o.rz);
4903
6024
  }
4904
6025
  },
4905
6026
  setScale(x, y, z) {
6027
+ sclOut[0] = x;
6028
+ sclOut[1] = y;
6029
+ sclOut[2] = z;
4906
6030
  for (const h of childHandles)
4907
6031
  h.setScale(x, y, z);
4908
6032
  },
6033
+ teleport(x, y, z) {
6034
+ posOut[0] = x;
6035
+ posOut[1] = y;
6036
+ posOut[2] = z;
6037
+ for (let i = 0; i < childHandles.length; i++) {
6038
+ const o = offsets[i];
6039
+ childHandles[i].teleport(x + o.px, y + o.py, z + o.pz);
6040
+ }
6041
+ },
6042
+ get position() {
6043
+ return posOut;
6044
+ },
6045
+ get rotation() {
6046
+ return rotOut;
6047
+ },
6048
+ get scale() {
6049
+ return sclOut;
6050
+ },
4909
6051
  destroy() {
4910
6052
  for (const h of childHandles)
4911
6053
  h.destroy();
4912
6054
  }
4913
6055
  };
4914
6056
  }
4915
- addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot) {
6057
+ addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot, prefabId = null) {
4916
6058
  const slot = this.skinnedFreeList.allocate();
4917
6059
  if (slot === -1)
4918
6060
  throw new Error(`Max skinned instances (${this.maxSkinnedInstances}) reached`);
@@ -4981,10 +6123,16 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4981
6123
  const capturedBoneOffset = boneOffset;
4982
6124
  const capturedSkinIndex = skinIndex;
4983
6125
  let destroyed = false;
4984
- return {
6126
+ const posOut = [0, 0, 0];
6127
+ const rotOut = [0, 0, 0];
6128
+ const sclOut = [0, 0, 0];
6129
+ const id = ++this.nextInstanceId;
6130
+ const handle = {
6131
+ id,
4985
6132
  slot,
4986
6133
  modelId: modelHandle.id,
4987
6134
  skinned: true,
6135
+ prefabId,
4988
6136
  setPosition(nx, ny, nz) {
4989
6137
  dynamicData[dynBase + DYN_CURR_PX] = nx;
4990
6138
  dynamicData[dynBase + DYN_CURR_PY] = ny;
@@ -5000,6 +6148,32 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5000
6148
  staticData[statBase + SSTAT_SY] = ny;
5001
6149
  staticData[statBase + SSTAT_SZ] = nz;
5002
6150
  },
6151
+ teleport(nx, ny, nz) {
6152
+ dynamicData[dynBase + DYN_PREV_PX] = nx;
6153
+ dynamicData[dynBase + DYN_PREV_PY] = ny;
6154
+ dynamicData[dynBase + DYN_PREV_PZ] = nz;
6155
+ dynamicData[dynBase + DYN_CURR_PX] = nx;
6156
+ dynamicData[dynBase + DYN_CURR_PY] = ny;
6157
+ dynamicData[dynBase + DYN_CURR_PZ] = nz;
6158
+ },
6159
+ get position() {
6160
+ posOut[0] = dynamicData[dynBase + DYN_CURR_PX];
6161
+ posOut[1] = dynamicData[dynBase + DYN_CURR_PY];
6162
+ posOut[2] = dynamicData[dynBase + DYN_CURR_PZ];
6163
+ return posOut;
6164
+ },
6165
+ get rotation() {
6166
+ rotOut[0] = dynamicData[dynBase + DYN_CURR_RX];
6167
+ rotOut[1] = dynamicData[dynBase + DYN_CURR_RY];
6168
+ rotOut[2] = dynamicData[dynBase + DYN_CURR_RZ];
6169
+ return rotOut;
6170
+ },
6171
+ get scale() {
6172
+ sclOut[0] = staticData[statBase + SSTAT_SX];
6173
+ sclOut[1] = staticData[statBase + SSTAT_SY];
6174
+ sclOut[2] = staticData[statBase + SSTAT_SZ];
6175
+ return sclOut;
6176
+ },
5003
6177
  play(name, opts2) {
5004
6178
  const state = animStates[slot];
5005
6179
  if (state)
@@ -5019,6 +6193,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5019
6193
  dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
5020
6194
  staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
5021
6195
  animStates[slot] = null;
6196
+ self.skinnedInstanceHandles[slot] = null;
5022
6197
  self.skinnedStaticDirty = true;
5023
6198
  if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
5024
6199
  let pool = self.freedBoneOffsets.get(capturedSkinIndex);
@@ -5030,6 +6205,8 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5030
6205
  }
5031
6206
  }
5032
6207
  };
6208
+ this.skinnedInstanceHandles[slot] = handle;
6209
+ return handle;
5033
6210
  }
5034
6211
  /**
5035
6212
  * Drain pending resyncs from the coordinator. Per affected skin: rebuild
@@ -5076,9 +6253,9 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5076
6253
  animState.prevClipId = remap3[animState.prevClipId];
5077
6254
  }
5078
6255
  }
5079
- this.packedAnimData = (0, import_renderer5.createPackedAnimationData)();
6256
+ this.packedAnimData = (0, import_renderer7.createPackedAnimationData)();
5080
6257
  for (const sm of this.skinnedModels) {
5081
- (0, import_renderer5.packSkinAndAnimations)(this.packedAnimData, sm.parsedSkin.data, sm.parsedSkin.animClips);
6258
+ (0, import_renderer7.packSkinAndAnimations)(this.packedAnimData, sm.parsedSkin.data, sm.parsedSkin.animClips);
5082
6259
  }
5083
6260
  if (this.tryUploadInPlace())
5084
6261
  return;
@@ -5165,7 +6342,8 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5165
6342
  { binding: 1, resource: { buffer: this.rawSkinnedDynamicBuffer } },
5166
6343
  { binding: 2, resource: { buffer: this.rawSkinnedStaticBuffer } },
5167
6344
  { binding: 3, resource: { buffer: this.rawSkinnedSlotIndexBuffer } },
5168
- { binding: 4, resource: { buffer: rawBoneBuffer } }
6345
+ { binding: 4, resource: { buffer: rawBoneBuffer } },
6346
+ { binding: 5, resource: { buffer: this.rawLightBuffer } }
5169
6347
  ]
5170
6348
  });
5171
6349
  this.device.queue.writeBuffer(
@@ -5301,6 +6479,113 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5301
6479
  sDyn[base + DYN_PREV_RZ] = sDyn[base + DYN_CURR_RZ];
5302
6480
  }
5303
6481
  });
6482
+ this.lights.storePrevious();
6483
+ }
6484
+ /** Resolve an instance's declared hitbox name to its Hitbox via the bucket's library. */
6485
+ resolveHitbox(handle) {
6486
+ if (!this._prefabs || !handle.prefabId)
6487
+ return null;
6488
+ const lib = this._prefabs.hitboxLibrary;
6489
+ if (!lib)
6490
+ return null;
6491
+ const prefab = this._prefabs.get(handle.prefabId);
6492
+ const name = prefab?.hitbox;
6493
+ return name ? lib.get(name) : null;
6494
+ }
6495
+ /**
6496
+ * Pick test for a single instance. Returns the ray-`t` and the struck
6497
+ * part name, or `null`. Uses the prefab's declared hitbox when
6498
+ * available; falls back to the model's axis-aligned bounding box.
6499
+ */
6500
+ testInstanceRay(ray, handle, cx, cy, cz, sx, sy, sz, halfX, halfY, halfZ) {
6501
+ const hitbox = this.resolveHitbox(handle);
6502
+ if (hitbox) {
6503
+ const hit = (0, import_hitbox3.testHitbox3D)(ray, hitbox, cx, cy, cz, sx, sy, sz);
6504
+ return hit ? { distance: hit.distance, part: hit.part } : null;
6505
+ }
6506
+ const t = ray.entryBox(cx, cy, cz, halfX * sx, halfY * sy, halfZ * sz);
6507
+ return t === null ? null : { distance: t, part: null };
6508
+ }
6509
+ /** Push every instance the screen ray hits within [near, far] into the hit buffer. Unsorted. */
6510
+ _collectRaycastHitsInto(screenX, screenY, rc) {
6511
+ const ray = this.camera.screenToRay(screenX, screenY);
6512
+ const ox = ray.origin[0], oy = ray.origin[1], oz = ray.origin[2];
6513
+ const dx = ray.direction[0], dy = ray.direction[1], dz = ray.direction[2];
6514
+ const minDistance = this.camera.near;
6515
+ const maxDistance = this.camera.far;
6516
+ const dyn = this.dynamicData;
6517
+ const stat = this.staticData;
6518
+ const models = this.models;
6519
+ this.batcher.each((_, instances, count) => {
6520
+ for (let i = 0; i < count; i++) {
6521
+ const slot = instances[i];
6522
+ const handle = this.instanceHandles[slot];
6523
+ if (handle === null)
6524
+ continue;
6525
+ const model = models[handle.modelId];
6526
+ if (!model)
6527
+ continue;
6528
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6529
+ const statBase = slot * STATIC_MESH_FLOATS;
6530
+ const hit = this.testInstanceRay(
6531
+ ray,
6532
+ handle,
6533
+ dyn[dynBase + DYN_CURR_PX],
6534
+ dyn[dynBase + DYN_CURR_PY],
6535
+ dyn[dynBase + DYN_CURR_PZ],
6536
+ stat[statBase + STAT_SX],
6537
+ stat[statBase + STAT_SY],
6538
+ stat[statBase + STAT_SZ],
6539
+ model.halfX,
6540
+ model.halfY,
6541
+ model.halfZ
6542
+ );
6543
+ if (hit === null)
6544
+ continue;
6545
+ const t = hit.distance;
6546
+ if (t < minDistance || t > maxDistance)
6547
+ continue;
6548
+ rc.push(handle, t, ox + dx * t, oy + dy * t, oz + dz * t, t, hit.part);
6549
+ }
6550
+ });
6551
+ const sDyn = this.skinnedDynamicData;
6552
+ const sStat = this.skinnedStaticData;
6553
+ const skinned = this.skinnedModels;
6554
+ this.skinnedBatcher.each((_, instances, count) => {
6555
+ for (let i = 0; i < count; i++) {
6556
+ const slot = instances[i];
6557
+ const handle = this.skinnedInstanceHandles[slot];
6558
+ if (handle === null)
6559
+ continue;
6560
+ const model = models[handle.modelId];
6561
+ if (!model)
6562
+ continue;
6563
+ const skin = skinned[model.skinIndex];
6564
+ if (!skin)
6565
+ continue;
6566
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6567
+ const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
6568
+ const hit = this.testInstanceRay(
6569
+ ray,
6570
+ handle,
6571
+ sDyn[dynBase + DYN_CURR_PX],
6572
+ sDyn[dynBase + DYN_CURR_PY],
6573
+ sDyn[dynBase + DYN_CURR_PZ],
6574
+ sStat[statBase + SSTAT_SX],
6575
+ sStat[statBase + SSTAT_SY],
6576
+ sStat[statBase + SSTAT_SZ],
6577
+ model.halfX,
6578
+ model.halfY,
6579
+ model.halfZ
6580
+ );
6581
+ if (hit === null)
6582
+ continue;
6583
+ const t = hit.distance;
6584
+ if (t < minDistance || t > maxDistance)
6585
+ continue;
6586
+ rc.push(handle, t, ox + dx * t, oy + dy * t, oz + dz * t, t, hit.part);
6587
+ }
6588
+ });
5304
6589
  }
5305
6590
  render(alpha) {
5306
6591
  if (!this._initialized)
@@ -5329,18 +6614,26 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5329
6614
  );
5330
6615
  this.staticDirty = false;
5331
6616
  }
6617
+ const packed = this.lights.pack();
6618
+ if (packed.count > 0) {
6619
+ this.device.queue.writeBuffer(
6620
+ this.rawLightBuffer,
6621
+ 0,
6622
+ packed.data.buffer,
6623
+ packed.data.byteOffset,
6624
+ packed.byteLength
6625
+ );
6626
+ }
5332
6627
  const vpMatrix = this.camera.getViewProjectionMatrix();
5333
6628
  this.uniformData.set(vpMatrix, 0);
5334
- this.uniformData[16] = alpha;
5335
- this.uniformData[17] = 0.3;
5336
- this.uniformData[18] = 0.8;
5337
- this.uniformData[19] = 0.5;
6629
+ this.uniformData[MESH_UNIFORM_ALPHA_OFFSET] = alpha;
6630
+ this.lights.writeUniforms(this.uniformData, MESH_UNIFORM_LIGHT_OFFSET, packed.count);
5338
6631
  this.device.queue.writeBuffer(
5339
6632
  this.rawUniformBuffer,
5340
6633
  0,
5341
6634
  this.uniformData.buffer,
5342
6635
  this.uniformData.byteOffset,
5343
- 80
6636
+ this.uniformData.byteLength
5344
6637
  );
5345
6638
  this.extractFrustumPlanes(vpMatrix);
5346
6639
  let indexOffset = 0;
@@ -5516,9 +6809,76 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
5516
6809
  pass.draw(model.vertexCount, batch.count, 0, batch.offset);
5517
6810
  }
5518
6811
  }
6812
+ if (this.debug.hitboxes) {
6813
+ this.drawDebugHitboxes(pass, vpMatrix);
6814
+ }
5519
6815
  pass.end();
5520
6816
  this.device.queue.submit([encoder.finish()]);
5521
6817
  }
6818
+ drawDebugHitboxes(pass, vp) {
6819
+ if (!this._prefabs)
6820
+ return;
6821
+ const debug = this.hitboxDebug;
6822
+ const state = this.raycast.state;
6823
+ debug.begin(vp);
6824
+ const dyn = this.dynamicData;
6825
+ const stat = this.staticData;
6826
+ this.batcher.each((_, instances, count) => {
6827
+ for (let i = 0; i < count; i++) {
6828
+ const slot = instances[i];
6829
+ const handle = this.instanceHandles[slot];
6830
+ if (handle === null)
6831
+ continue;
6832
+ const hb = this.resolveHitbox(handle);
6833
+ if (!hb)
6834
+ continue;
6835
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6836
+ const statBase = slot * STATIC_MESH_FLOATS;
6837
+ const hovered = state.containsId(handle.id);
6838
+ for (const part of hb.parts) {
6839
+ debug.emit(
6840
+ part,
6841
+ hovered,
6842
+ dyn[dynBase + DYN_CURR_PX],
6843
+ dyn[dynBase + DYN_CURR_PY],
6844
+ dyn[dynBase + DYN_CURR_PZ],
6845
+ stat[statBase + STAT_SX],
6846
+ stat[statBase + STAT_SY],
6847
+ stat[statBase + STAT_SZ]
6848
+ );
6849
+ }
6850
+ }
6851
+ });
6852
+ const sDyn = this.skinnedDynamicData;
6853
+ const sStat = this.skinnedStaticData;
6854
+ this.skinnedBatcher.each((_, instances, count) => {
6855
+ for (let i = 0; i < count; i++) {
6856
+ const slot = instances[i];
6857
+ const handle = this.skinnedInstanceHandles[slot];
6858
+ if (handle === null)
6859
+ continue;
6860
+ const hb = this.resolveHitbox(handle);
6861
+ if (!hb)
6862
+ continue;
6863
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6864
+ const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
6865
+ const hovered = state.containsId(handle.id);
6866
+ for (const part of hb.parts) {
6867
+ debug.emit(
6868
+ part,
6869
+ hovered,
6870
+ sDyn[dynBase + DYN_CURR_PX],
6871
+ sDyn[dynBase + DYN_CURR_PY],
6872
+ sDyn[dynBase + DYN_CURR_PZ],
6873
+ sStat[statBase + SSTAT_SX],
6874
+ sStat[statBase + SSTAT_SY],
6875
+ sStat[statBase + SSTAT_SZ]
6876
+ );
6877
+ }
6878
+ }
6879
+ });
6880
+ debug.flush(pass);
6881
+ }
5522
6882
  /**
5523
6883
  * Extract 6 frustum planes from a column-major VP matrix.
5524
6884
  * Each plane is [a, b, c, d] where ax + by + cz + d >= 0 means inside.
@@ -5699,12 +7059,12 @@ var ParticleEmitter = class {
5699
7059
  this.renderer = renderer;
5700
7060
  this.config = config;
5701
7061
  this.rng = new import_simple_rng.SimpleRNG(config.seed);
5702
- const max2 = config.max;
5703
- this.sprites = new Array(max2).fill(null);
5704
- this.lifetimes = new Float32Array(max2);
5705
- this.maxLifetimes = new Float32Array(max2);
5706
- this.velocitiesX = new Float32Array(max2);
5707
- this.velocitiesY = new Float32Array(max2);
7062
+ const max3 = config.max;
7063
+ this.sprites = new Array(max3).fill(null);
7064
+ this.lifetimes = new Float32Array(max3);
7065
+ this.maxLifetimes = new Float32Array(max3);
7066
+ this.velocitiesX = new Float32Array(max3);
7067
+ this.velocitiesY = new Float32Array(max3);
5708
7068
  }
5709
7069
  emit(x, y, count = 1) {
5710
7070
  for (let i = 0; i < count; i++) {
@@ -5780,58 +7140,3 @@ var ParticleEmitter = class {
5780
7140
  this.head = 0;
5781
7141
  }
5782
7142
  };
5783
-
5784
- // src/shaders/utils.ts
5785
- var import_typegpu11 = __toESM(require("typegpu"), 1);
5786
- var d6 = __toESM(require("typegpu/data"), 1);
5787
- var std4 = __toESM(require("typegpu/std"), 1);
5788
- var rotate2d = import_typegpu11.default.fn([d6.vec2f, d6.f32], d6.vec2f)(
5789
- function rotate2d2(point, angle) {
5790
- "use gpu";
5791
- const c = std4.cos(angle);
5792
- const s = std4.sin(angle);
5793
- return d6.vec2f(
5794
- point.x * c - point.y * s,
5795
- point.x * s + point.y * c
5796
- );
5797
- }
5798
- );
5799
- var worldToClip2d = import_typegpu11.default.fn([d6.vec2f, d6.mat3x3f], d6.vec4f)(
5800
- function worldToClip2d2(worldPos, cameraMatrix) {
5801
- "use gpu";
5802
- const clip = cameraMatrix * d6.vec3f(worldPos.x, worldPos.y, 1);
5803
- return d6.vec4f(clip.x, clip.y, 0, 1);
5804
- }
5805
- );
5806
- var worldToClip3d = import_typegpu11.default.fn([d6.vec3f, d6.mat4x4f], d6.vec4f)(
5807
- function worldToClip3d2(worldPos, vpMatrix) {
5808
- "use gpu";
5809
- return vpMatrix * d6.vec4f(worldPos.x, worldPos.y, worldPos.z, 1);
5810
- }
5811
- );
5812
- var remap = import_typegpu11.default.fn([d6.f32, d6.f32, d6.f32, d6.f32, d6.f32], d6.f32)(
5813
- function remap2(value, inMin, inMax, outMin, outMax) {
5814
- "use gpu";
5815
- const t = (value - inMin) / (inMax - inMin);
5816
- return outMin + t * (outMax - outMin);
5817
- }
5818
- );
5819
- var scaleRotate2d = import_typegpu11.default.fn([d6.vec2f, d6.f32], d6.mat2x2f)(
5820
- function scaleRotate2d2(scale, angle) {
5821
- "use gpu";
5822
- const c = std4.cos(angle);
5823
- const s = std4.sin(angle);
5824
- return d6.mat2x2f(
5825
- scale.x * c,
5826
- scale.x * s,
5827
- -(scale.y * s),
5828
- scale.y * c
5829
- );
5830
- }
5831
- );
5832
- var inverseLerp = import_typegpu11.default.fn([d6.f32, d6.f32, d6.f32], d6.f32)(
5833
- function inverseLerp2(min, max2, value) {
5834
- "use gpu";
5835
- return std4.saturate((value - min) / (max2 - min));
5836
- }
5837
- );