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
@@ -7,7 +7,7 @@ var std = { ..._std };
7
7
  // src/2d/renderer.ts
8
8
  import tgpu4 from "typegpu";
9
9
  import * as d4 from "typegpu/data";
10
- import { FreeList as FreeList2 } from "murow/core/free-list";
10
+ import { FreeList } from "murow/core/free-list";
11
11
  import { Base2DRenderer } from "murow/renderer";
12
12
 
13
13
  // src/core/constants.ts
@@ -162,8 +162,46 @@ var MeshUniforms = d2.struct({
162
162
  alpha: d2.f32,
163
163
  lightDirX: d2.f32,
164
164
  lightDirY: d2.f32,
165
- lightDirZ: d2.f32
165
+ lightDirZ: d2.f32,
166
+ lightDirR: d2.f32,
167
+ lightDirG: d2.f32,
168
+ lightDirB: d2.f32,
169
+ lightDirIntensity: d2.f32,
170
+ ambientR: d2.f32,
171
+ ambientG: d2.f32,
172
+ ambientB: d2.f32,
173
+ lightCount: d2.u32
174
+ });
175
+ var MESH_UNIFORM_ALPHA_OFFSET = 16;
176
+ var MESH_UNIFORM_LIGHT_OFFSET = 17;
177
+ var MESH_UNIFORM_FLOATS = 28;
178
+ var Light = d2.struct({
179
+ kind: d2.f32,
180
+ currPosX: d2.f32,
181
+ currPosY: d2.f32,
182
+ currPosZ: d2.f32,
183
+ prevPosX: d2.f32,
184
+ prevPosY: d2.f32,
185
+ prevPosZ: d2.f32,
186
+ currDirX: d2.f32,
187
+ currDirY: d2.f32,
188
+ currDirZ: d2.f32,
189
+ prevDirX: d2.f32,
190
+ prevDirY: d2.f32,
191
+ prevDirZ: d2.f32,
192
+ colorR: d2.f32,
193
+ colorG: d2.f32,
194
+ colorB: d2.f32,
195
+ intensity: d2.f32,
196
+ range: d2.f32,
197
+ innerCos: d2.f32,
198
+ outerCos: d2.f32,
199
+ castsShadow: d2.f32,
200
+ shadowMapIndex: d2.f32
166
201
  });
202
+ var LIGHT_FLOATS = 22;
203
+ var LIGHT_KIND_POINT = 1;
204
+ var LIGHT_KIND_SPOT = 2;
167
205
  var InstanceAnimStateGPU = d2.struct({
168
206
  clipId: d2.i32,
169
207
  time: d2.f32,
@@ -186,15 +224,19 @@ import { SparseBatcher } from "murow/core/sparse-batcher";
186
224
 
187
225
  // src/2d/sprite-accessor.ts
188
226
  var SpriteAccessor = class {
189
- constructor(dynamicData, staticData, slot, sheetId, onStaticDirty) {
227
+ constructor(dynamicData, staticData, id, slot, sheetId, onStaticDirty) {
190
228
  this.dynamicData = dynamicData;
191
229
  this.staticData = staticData;
230
+ this._id = id;
192
231
  this._slot = slot;
193
232
  this._sheetId = sheetId;
194
233
  this.dynamicBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
195
234
  this.staticBase = slot * STATIC_FLOATS_PER_SPRITE;
196
235
  this._onStaticDirty = onStaticDirty;
197
236
  }
237
+ get id() {
238
+ return this._id;
239
+ }
198
240
  get slot() {
199
241
  return this._slot;
200
242
  }
@@ -318,6 +360,91 @@ var SpriteAccessor = class {
318
360
  }
319
361
  };
320
362
 
363
+ // src/2d/raycast.ts
364
+ import { HitBuffer } from "murow/core/raycast";
365
+ import {
366
+ Raycast,
367
+ RaycastMemo
368
+ } from "murow/renderer";
369
+ var WebGPURaycast2D = class extends Raycast {
370
+ constructor(renderer) {
371
+ super();
372
+ this.renderer = renderer;
373
+ this.state = new HitBuffer(2);
374
+ this.resultBuffer = [];
375
+ this.memos = /* @__PURE__ */ new Set();
376
+ }
377
+ update(input) {
378
+ this.state.reset();
379
+ this.renderer._collectRaycastHitsInto(input.mouse.position.x, input.mouse.position.y, this.state);
380
+ for (const m of this.memos)
381
+ m._invalidate();
382
+ }
383
+ /**
384
+ * Topmost sprite under the cursor, or null. The returned object is
385
+ * pool-backed and valid only until the next `update()` -- copy what
386
+ * you need, or use `memo` for results that persist across frames.
387
+ */
388
+ hit(opts) {
389
+ return this.state.nearest(opts?.filter, opts?.maxDistance ?? Infinity);
390
+ }
391
+ /**
392
+ * Every sprite under the cursor, topmost first. The array and its
393
+ * entries are reused and overwritten by the next `update()`.
394
+ */
395
+ hitAll(opts) {
396
+ this.state.collectInto(this.resultBuffer, opts?.filter, opts?.maxDistance ?? Infinity);
397
+ return this.resultBuffer;
398
+ }
399
+ memo(opts) {
400
+ const m = new WebGPURaycastMemo2D(this.state, opts, () => this.memos.delete(m));
401
+ this.memos.add(m);
402
+ return m;
403
+ }
404
+ clearMemos() {
405
+ for (const m of this.memos)
406
+ m._detach();
407
+ this.memos.clear();
408
+ }
409
+ };
410
+ var WebGPURaycastMemo2D = class extends RaycastMemo {
411
+ constructor(state, opts, onDispose) {
412
+ super();
413
+ this.state = state;
414
+ this.opts = opts;
415
+ this.onDispose = onDispose;
416
+ this.dirty = true;
417
+ this.detached = false;
418
+ this.cached = [];
419
+ }
420
+ get hits() {
421
+ if (this.detached)
422
+ return this.cached;
423
+ if (this.dirty) {
424
+ this.state.collectInto(this.cached, this.opts.filter, this.opts.maxDistance ?? Infinity);
425
+ this.dirty = false;
426
+ }
427
+ return this.cached;
428
+ }
429
+ get first() {
430
+ const arr = this.hits;
431
+ return arr.length > 0 ? arr[0] : null;
432
+ }
433
+ dispose() {
434
+ if (this.detached)
435
+ return;
436
+ this.onDispose();
437
+ this._detach();
438
+ }
439
+ _invalidate() {
440
+ this.dirty = true;
441
+ }
442
+ _detach() {
443
+ this.detached = true;
444
+ this.cached.length = 0;
445
+ }
446
+ };
447
+
321
448
  // src/camera/camera-2d.ts
322
449
  import { lerp } from "murow/core/lerp";
323
450
  var Camera2D = class {
@@ -1084,7 +1211,7 @@ function resolveBuiltInGeometry(name) {
1084
1211
  }
1085
1212
 
1086
1213
  // src/geometry/geometry-builder.ts
1087
- import { FreeList } from "murow/core/free-list";
1214
+ import { SlotMap } from "murow/core/slot-map";
1088
1215
 
1089
1216
  // src/shaders/runtime-transpile.ts
1090
1217
  import * as acorn from "acorn";
@@ -1262,7 +1389,6 @@ var CustomGeometry = class {
1262
1389
  this._uniformDirty = true;
1263
1390
  this._isComputeSourced = false;
1264
1391
  this._drawCount = 0;
1265
- this.activeCount = 0;
1266
1392
  this.name = name;
1267
1393
  this.root = root;
1268
1394
  this.maxInstances = maxInstances;
@@ -1276,7 +1402,7 @@ var CustomGeometry = class {
1276
1402
  this.staticFloatsPerInstance = 0;
1277
1403
  for (const n of this.staticFieldNames)
1278
1404
  this.staticFloatsPerInstance += getFieldFloats(layoutConfig.static[n]);
1279
- this.freeList = new FreeList(maxInstances);
1405
+ this.slots = new SlotMap(maxInstances);
1280
1406
  this.dynamicData = new Float32Array(maxInstances * this.dynamicFloatsPerInstance);
1281
1407
  this.staticData = new Float32Array(maxInstances * this.staticFloatsPerInstance);
1282
1408
  this.uniformValues = { ...uniformValues };
@@ -1287,8 +1413,6 @@ var CustomGeometry = class {
1287
1413
  this.dataBindGroup = dataBindGroup;
1288
1414
  this.canvas = canvas;
1289
1415
  this.clearColor = clearColor;
1290
- this.activeSlots = new Uint32Array(maxInstances);
1291
- this.slotToActive = new Int32Array(maxInstances).fill(-1);
1292
1416
  this._ctx = new InstanceContext();
1293
1417
  this._isComputeSourced = isComputeSourced;
1294
1418
  if (isComputeSourced)
@@ -1302,34 +1426,20 @@ var CustomGeometry = class {
1302
1426
  this._drawCount = count;
1303
1427
  }
1304
1428
  addInstance(data) {
1305
- const slot = this.freeList.allocate();
1429
+ const slot = this.slots.add();
1306
1430
  if (slot === -1)
1307
1431
  throw new Error(`Max instances (${this.maxInstances}) reached for "${this.name}"`);
1308
1432
  this.setInstanceData(slot, data);
1309
- this.activeSlots[this.activeCount] = slot;
1310
- this.slotToActive[slot] = this.activeCount;
1311
- this.activeCount++;
1312
1433
  return slot;
1313
1434
  }
1314
1435
  removeInstance(slot) {
1315
- this.freeList.free(slot);
1316
1436
  const dynBase = slot * this.dynamicFloatsPerInstance;
1317
1437
  const statBase = slot * this.staticFloatsPerInstance;
1318
1438
  this.dynamicData.fill(0, dynBase, dynBase + this.dynamicFloatsPerInstance);
1319
1439
  this.staticData.fill(0, statBase, statBase + this.staticFloatsPerInstance);
1320
1440
  this._dynamicDirty = true;
1321
1441
  this._staticDirty = true;
1322
- const activeIdx = this.slotToActive[slot];
1323
- if (activeIdx !== -1) {
1324
- const lastIdx = this.activeCount - 1;
1325
- if (activeIdx !== lastIdx) {
1326
- const lastSlot = this.activeSlots[lastIdx];
1327
- this.activeSlots[activeIdx] = lastSlot;
1328
- this.slotToActive[lastSlot] = activeIdx;
1329
- }
1330
- this.slotToActive[slot] = -1;
1331
- this.activeCount--;
1332
- }
1442
+ this.slots.remove(slot);
1333
1443
  }
1334
1444
  setInstanceData(slot, data) {
1335
1445
  const rawData = data;
@@ -1402,7 +1512,7 @@ var CustomGeometry = class {
1402
1512
  this._uniformDirty = true;
1403
1513
  }
1404
1514
  getActiveCount() {
1405
- return this.freeList.getAllocatedCount();
1515
+ return this.slots.size;
1406
1516
  }
1407
1517
  updateAll(callback) {
1408
1518
  this._ctx._bind(
@@ -1414,8 +1524,8 @@ var CustomGeometry = class {
1414
1524
  this.staticFieldNames,
1415
1525
  this.layoutConfig
1416
1526
  );
1417
- const count = this.activeCount;
1418
- const slots = this.activeSlots;
1527
+ const count = this.slots.size;
1528
+ const slots = this.slots.activeSlots;
1419
1529
  for (let i = 0; i < count; i++) {
1420
1530
  const slot = slots[i];
1421
1531
  this._ctx._setSlot(slot);
@@ -1426,7 +1536,7 @@ var CustomGeometry = class {
1426
1536
  }
1427
1537
  render() {
1428
1538
  const device = this.root.device;
1429
- const count = this._isComputeSourced ? this._drawCount : this.freeList.getAllocatedCount();
1539
+ const count = this._isComputeSourced ? this._drawCount : this.slots.size;
1430
1540
  if (count === 0)
1431
1541
  return;
1432
1542
  const context = this.canvas.getContext("webgpu");
@@ -2051,6 +2161,7 @@ var ComputeBuilder = class {
2051
2161
  };
2052
2162
 
2053
2163
  // src/2d/renderer.ts
2164
+ import { testHitbox2D, pointInQuad2D } from "murow/core/hitbox";
2054
2165
  var prefab2DHandles = /* @__PURE__ */ new WeakMap();
2055
2166
  function isPrefab2D(value) {
2056
2167
  return value.type === "spritesheet";
@@ -2074,15 +2185,19 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2074
2185
  this.sheets = /* @__PURE__ */ new Map();
2075
2186
  this.nextSheetId = 0;
2076
2187
  this.uniformData = new Float32Array(20);
2188
+ this.nextSpriteId = 0;
2077
2189
  this.resizeObserver = null;
2078
2190
  this.resizeCallbacks = [];
2079
2191
  this._prefabs = options.prefabs ?? null;
2080
2192
  this.camera = new Camera2D(canvas.width || 800, canvas.height || 600);
2081
- this.freeList = new FreeList2(resolvedMaxSprites);
2193
+ this.raycast = new WebGPURaycast2D(this);
2194
+ this.freeList = new FreeList(resolvedMaxSprites);
2082
2195
  this.batcher = new SparseBatcher(resolvedMaxSprites);
2083
2196
  this.dynamicData = new Float32Array(resolvedMaxSprites * DYNAMIC_FLOATS_PER_SPRITE);
2084
2197
  this.staticData = new Float32Array(resolvedMaxSprites * STATIC_FLOATS_PER_SPRITE);
2085
2198
  this.slotIndexData = new Uint32Array(resolvedMaxSprites);
2199
+ this.spriteHandles = new Array(resolvedMaxSprites).fill(null);
2200
+ this.spriteHitboxes = new Array(resolvedMaxSprites).fill(null);
2086
2201
  }
2087
2202
  get device() {
2088
2203
  return this._device;
@@ -2242,6 +2357,9 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2242
2357
  const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
2243
2358
  const statBase = slot * STATIC_FLOATS_PER_SPRITE;
2244
2359
  const sheet = isPrefab2D(opts.sheet) ? resolveSpritePrefabHandle(opts.sheet) : opts.sheet;
2360
+ const hitboxName = isPrefab2D(opts.sheet) ? opts.sheet.hitbox : void 0;
2361
+ const lib = this._prefabs?.hitboxLibrary ?? null;
2362
+ const hitbox = hitboxName && lib ? lib.get(hitboxName) : null;
2245
2363
  const [px, py] = opts.position ?? [0, 0];
2246
2364
  this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] = px;
2247
2365
  this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] = py;
@@ -2270,15 +2388,19 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2270
2388
  this.staticData[statBase + STATIC_OFFSET_TINT_A] = tint[3];
2271
2389
  this.staticDirty = true;
2272
2390
  this.batcher.add(opts.layer ?? 0, sheet.id, slot);
2273
- return new SpriteAccessor(
2391
+ const accessor = new SpriteAccessor(
2274
2392
  this.dynamicData,
2275
2393
  this.staticData,
2394
+ this.nextSpriteId++,
2276
2395
  slot,
2277
2396
  sheet.id,
2278
2397
  () => {
2279
2398
  this.staticDirty = true;
2280
2399
  }
2281
2400
  );
2401
+ this.spriteHandles[slot] = accessor;
2402
+ this.spriteHitboxes[slot] = hitbox;
2403
+ return accessor;
2282
2404
  }
2283
2405
  removeSprite(sprite) {
2284
2406
  const accessor = sprite;
@@ -2288,8 +2410,47 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2288
2410
  const statBase = accessor.slot * STATIC_FLOATS_PER_SPRITE;
2289
2411
  this.dynamicData.fill(0, dynBase, dynBase + DYNAMIC_FLOATS_PER_SPRITE);
2290
2412
  this.staticData.fill(0, statBase, statBase + STATIC_FLOATS_PER_SPRITE);
2413
+ this.spriteHandles[accessor.slot] = null;
2414
+ this.spriteHitboxes[accessor.slot] = void 0;
2291
2415
  this.staticDirty = true;
2292
2416
  }
2417
+ /**
2418
+ * Point-test every sprite against the unprojected cursor and push the
2419
+ * hits into the buffer. Sort key is `-layer` so the topmost sprite is
2420
+ * "nearest". A declared hitbox overrides the default rendered quad.
2421
+ */
2422
+ _collectRaycastHitsInto(screenX, screenY, rc) {
2423
+ const [wx, wy] = this.camera.screenToWorld(screenX, screenY);
2424
+ const dyn = this.dynamicData;
2425
+ const stat = this.staticData;
2426
+ this.batcher.each((_sheetId, instances, count) => {
2427
+ for (let i = 0; i < count; i++) {
2428
+ const slot = instances[i];
2429
+ const handle = this.spriteHandles[slot];
2430
+ if (handle === null)
2431
+ continue;
2432
+ const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
2433
+ const statBase = slot * STATIC_FLOATS_PER_SPRITE;
2434
+ const cx = dyn[dynBase + DYNAMIC_OFFSET_CURR_X];
2435
+ const cy = dyn[dynBase + DYNAMIC_OFFSET_CURR_Y];
2436
+ const rot = dyn[dynBase + DYNAMIC_OFFSET_CURR_ROTATION];
2437
+ const sx = stat[statBase + STATIC_OFFSET_SCALE_X];
2438
+ const sy = stat[statBase + STATIC_OFFSET_SCALE_Y];
2439
+ const layer = stat[statBase + STATIC_OFFSET_LAYER];
2440
+ const hb = this.spriteHitboxes[slot];
2441
+ let part = null;
2442
+ if (hb) {
2443
+ const hit = testHitbox2D(hb, cx, cy, sx, sy, rot, wx, wy);
2444
+ if (!hit)
2445
+ continue;
2446
+ part = hit.part;
2447
+ } else if (!pointInQuad2D(cx, cy, sx, sy, rot, wx, wy)) {
2448
+ continue;
2449
+ }
2450
+ rc.push(handle, -layer, wx, wy, 0, layer, part);
2451
+ }
2452
+ });
2453
+ }
2293
2454
  storePreviousState() {
2294
2455
  this.camera.storePrevious();
2295
2456
  const dyn = this.dynamicData;
@@ -2500,311 +2661,127 @@ var AnimationController = class {
2500
2661
  };
2501
2662
 
2502
2663
  // src/3d/renderer.ts
2503
- import tgpu6 from "typegpu";
2664
+ import tgpu7 from "typegpu";
2504
2665
  import { Base3DRenderer } from "murow/renderer";
2505
- import { FreeList as FreeList3 } from "murow/core/free-list";
2666
+ import { FreeList as FreeList2 } from "murow/core/free-list";
2506
2667
  import { SparseBatcher as SparseBatcher2 } from "murow/core/sparse-batcher";
2507
2668
 
2508
- // src/camera/camera-3d.ts
2509
- import { lerp as lerp2 } from "murow/core/lerp";
2510
- import { Ray3D } from "murow/core/ray";
2511
- var Camera3D = class {
2512
- constructor() {
2513
- this.position = [0, 5, -10];
2514
- this.target = [0, 0, 0];
2515
- this.up = [0, 1, 0];
2516
- this.fov = 60;
2517
- this.near = 0.1;
2518
- this.far = 1e3;
2519
- this.aspect = 1;
2520
- this.movement = "local";
2521
- // Previous state for interpolation (stored before each tick)
2522
- this._prevPosition = [0, 5, -10];
2523
- this._prevTarget = [0, 0, 0];
2524
- // Interpolated state used for rendering
2525
- this._renderPosition = [0, 5, -10];
2526
- this._renderTarget = [0, 0, 0];
2527
- this._viewMatrix = new Float32Array(16);
2528
- this._projMatrix = new Float32Array(16);
2529
- this._vpMatrix = new Float32Array(16);
2530
- this._ray = new Ray3D();
2531
- this._width = 1;
2532
- this._height = 1;
2533
- }
2534
- /**
2535
- * Store current position/target as previous. Call before each tick.
2536
- */
2537
- storePrevious() {
2538
- this._prevPosition[0] = this.position[0];
2539
- this._prevPosition[1] = this.position[1];
2540
- this._prevPosition[2] = this.position[2];
2541
- this._prevTarget[0] = this.target[0];
2542
- this._prevTarget[1] = this.target[1];
2543
- this._prevTarget[2] = this.target[2];
2544
- }
2545
- /**
2546
- * Smoothly move the camera toward a target point.
2547
- * Call each tick. The camera and its look-at target lerp toward the given position.
2548
- * @param targetX World X to follow
2549
- * @param targetY World Y to follow
2550
- * @param targetZ World Z to follow
2551
- * @param smoothing 0-1. 1 = snap instantly, 0.1 = lazy follow. Default 1.
2552
- */
2553
- follow(targetX, targetY, targetZ, smoothing = 1) {
2554
- const dx = targetX - this.target[0];
2555
- const dy = targetY - this.target[1];
2556
- const dz = targetZ - this.target[2];
2557
- const mx = dx * smoothing;
2558
- const my = dy * smoothing;
2559
- const mz = dz * smoothing;
2560
- this.target[0] += mx;
2561
- this.target[1] += my;
2562
- this.target[2] += mz;
2563
- this.position[0] += mx;
2564
- this.position[1] += my;
2565
- this.position[2] += mz;
2566
- }
2567
- /**
2568
- * Interpolate between previous and current state. Call before rendering.
2569
- */
2570
- interpolate(alpha) {
2571
- this._renderPosition[0] = lerp2(this._prevPosition[0], this.position[0], alpha);
2572
- this._renderPosition[1] = lerp2(this._prevPosition[1], this.position[1], alpha);
2573
- this._renderPosition[2] = lerp2(this._prevPosition[2], this.position[2], alpha);
2574
- this._renderTarget[0] = lerp2(this._prevTarget[0], this.target[0], alpha);
2575
- this._renderTarget[1] = lerp2(this._prevTarget[1], this.target[1], alpha);
2576
- this._renderTarget[2] = lerp2(this._prevTarget[2], this.target[2], alpha);
2577
- }
2578
- /**
2579
- * Build the view matrix (lookAt) using interpolated state.
2580
- */
2581
- getViewMatrix() {
2582
- lookAt(this._viewMatrix, this._renderPosition, this._renderTarget, this.up);
2583
- return this._viewMatrix;
2584
- }
2585
- /**
2586
- * Build the perspective projection matrix.
2587
- */
2588
- getProjectionMatrix() {
2589
- perspective(this._projMatrix, this.fov * (Math.PI / 180), this.aspect, this.near, this.far);
2590
- return this._projMatrix;
2669
+ // src/3d/shader.ts
2670
+ import tgpu6 from "typegpu";
2671
+ import * as d6 from "typegpu/data";
2672
+ import * as std4 from "typegpu/std";
2673
+
2674
+ // src/shaders/utils.ts
2675
+ import tgpu5 from "typegpu";
2676
+ import * as d5 from "typegpu/data";
2677
+ import * as std3 from "typegpu/std";
2678
+ var rotate2d = tgpu5.fn([d5.vec2f, d5.f32], d5.vec2f)(
2679
+ function rotate2d2(point, angle) {
2680
+ "use gpu";
2681
+ const c = std3.cos(angle);
2682
+ const s = std3.sin(angle);
2683
+ return d5.vec2f(
2684
+ point.x * c - point.y * s,
2685
+ point.x * s + point.y * c
2686
+ );
2591
2687
  }
2592
- /**
2593
- * Build the combined view-projection matrix.
2594
- */
2595
- getViewProjectionMatrix() {
2596
- this.getViewMatrix();
2597
- this.getProjectionMatrix();
2598
- mat4Multiply(this._vpMatrix, this._projMatrix, this._viewMatrix);
2599
- return this._vpMatrix;
2688
+ );
2689
+ var worldToClip2d = tgpu5.fn([d5.vec2f, d5.mat3x3f], d5.vec4f)(
2690
+ function worldToClip2d2(worldPos, cameraMatrix) {
2691
+ "use gpu";
2692
+ const clip = cameraMatrix * d5.vec3f(worldPos.x, worldPos.y, 1);
2693
+ return d5.vec4f(clip.x, clip.y, 0, 1);
2600
2694
  }
2601
- setAspect(width, height) {
2602
- this.aspect = width / height;
2603
- this._width = width;
2604
- this._height = height;
2695
+ );
2696
+ var worldToClip3d = tgpu5.fn([d5.vec3f, d5.mat4x4f], d5.vec4f)(
2697
+ function worldToClip3d2(worldPos, vpMatrix) {
2698
+ "use gpu";
2699
+ return vpMatrix * d5.vec4f(worldPos.x, worldPos.y, worldPos.z, 1);
2605
2700
  }
2606
- setPosition(x, y, z) {
2607
- this.position[0] = x;
2608
- this.position[1] = y;
2609
- this.position[2] = z;
2701
+ );
2702
+ var remap = tgpu5.fn([d5.f32, d5.f32, d5.f32, d5.f32, d5.f32], d5.f32)(
2703
+ function remap2(value, inMin, inMax, outMin, outMax) {
2704
+ "use gpu";
2705
+ const t = (value - inMin) / (inMax - inMin);
2706
+ return outMin + t * (outMax - outMin);
2610
2707
  }
2611
- /**
2612
- * Unproject a screen coordinate into a world-space ray.
2613
- * Requires `setAspect(width, height)` to have been called first.
2614
- * Returns a pre-allocated Ray3D — copy origin/direction if you need to store it.
2615
- */
2616
- screenToRay(screenX, screenY) {
2617
- this.getViewMatrix();
2618
- const m = this._viewMatrix;
2619
- const ndcX = 2 * screenX / this._width - 1;
2620
- const ndcY = 1 - 2 * screenY / this._height;
2621
- const t = Math.tan(this.fov * Math.PI / 180 * 0.5);
2622
- const rightX = m[0], rightY = m[4], rightZ = m[8];
2623
- const upX = m[1], upY = m[5], upZ = m[9];
2624
- const fwdX = -m[2], fwdY = -m[6], fwdZ = -m[10];
2625
- const dx = fwdX + rightX * ndcX * t * this.aspect + upX * ndcY * t;
2626
- const dy = fwdY + rightY * ndcX * t * this.aspect + upY * ndcY * t;
2627
- const dz = fwdZ + rightZ * ndcX * t * this.aspect + upZ * ndcY * t;
2628
- this._ray.set(this.position[0], this.position[1], this.position[2], dx, dy, dz);
2629
- return this._ray;
2708
+ );
2709
+ var scaleRotate2d = tgpu5.fn([d5.vec2f, d5.f32], d5.mat2x2f)(
2710
+ function scaleRotate2d2(scale, angle) {
2711
+ "use gpu";
2712
+ const c = std3.cos(angle);
2713
+ const s = std3.sin(angle);
2714
+ return d5.mat2x2f(
2715
+ scale.x * c,
2716
+ scale.x * s,
2717
+ -(scale.y * s),
2718
+ scale.y * c
2719
+ );
2630
2720
  }
2631
- setTarget(x, y, z) {
2632
- this.target[0] = x;
2633
- this.target[1] = y;
2634
- this.target[2] = z;
2721
+ );
2722
+ var inverseLerp = tgpu5.fn([d5.f32, d5.f32, d5.f32], d5.f32)(
2723
+ function inverseLerp2(min, max3, value) {
2724
+ "use gpu";
2725
+ return std3.saturate((value - min) / (max3 - min));
2635
2726
  }
2636
- /**
2637
- * Move the camera. Behaviour is determined by the `movement` property:
2638
- * - `'local'` — along camera axes, pitch included (free-fly / spectator)
2639
- * - `'grounded'` — yaw-projected XZ + world Y (FPS)
2640
- * - `'global'` — world axes directly (isometric / platformer)
2641
- */
2642
- move(right, up, forward) {
2643
- if (this.movement === "grounded")
2644
- this._moveGrounded(right, up, forward);
2645
- else if (this.movement === "global")
2646
- this._moveGlobal(right, up, forward);
2647
- else
2648
- this._moveLocal(right, up, forward);
2727
+ );
2728
+ var lightContribution = tgpu5.fn(
2729
+ [d5.vec3f, d5.vec3f, d5.vec3f, d5.vec4f, d5.vec3f, d5.vec3f],
2730
+ d5.vec3f
2731
+ )(
2732
+ function lightContribution2(pos, axis, color, params, normal, worldPos) {
2733
+ "use gpu";
2734
+ const intensity = params.x;
2735
+ const range = params.y;
2736
+ const innerCos = params.z;
2737
+ const outerCos = params.w;
2738
+ const toLight = d5.vec3f(pos.x - worldPos.x, pos.y - worldPos.y, pos.z - worldPos.z);
2739
+ const dist = std3.length(toLight);
2740
+ const dir = std3.normalize(toLight);
2741
+ const lambert = std3.max(std3.dot(normal, dir), 0);
2742
+ const atten = std3.saturate(1 - dist / std3.max(range, 1e-4));
2743
+ const falloff = atten * atten;
2744
+ const axisLen = std3.length(axis);
2745
+ const isSpot = axisLen > 1e-4;
2746
+ const safeAxis = std3.select(d5.vec3f(0, 0, 1), axis, isSpot);
2747
+ const cosAngle = std3.dot(std3.normalize(safeAxis), d5.vec3f(-dir.x, -dir.y, -dir.z));
2748
+ const spotCone = std3.smoothstep(outerCos, innerCos, cosAngle);
2749
+ const cone = std3.select(1, spotCone, isSpot);
2750
+ const shadowFactor = 1;
2751
+ const scale = lambert * falloff * cone * intensity * shadowFactor;
2752
+ return d5.vec3f(color.x * scale, color.y * scale, color.z * scale);
2649
2753
  }
2650
- _moveLocal(right, up, forward) {
2651
- this.getViewMatrix();
2652
- const m = this._viewMatrix;
2653
- const dx = m[0] * right + m[1] * up - m[2] * forward;
2654
- const dy = m[4] * right + m[5] * up - m[6] * forward;
2655
- const dz = m[8] * right + m[9] * up - m[10] * forward;
2656
- this.position[0] += dx;
2657
- this.position[1] += dy;
2658
- this.position[2] += dz;
2659
- this.target[0] += dx;
2660
- this.target[1] += dy;
2661
- this.target[2] += dz;
2754
+ );
2755
+ var tonemap = tgpu5.fn([d5.vec3f], d5.vec3f)(
2756
+ function tonemap2(c) {
2757
+ "use gpu";
2758
+ return d5.vec3f(c.x / (1 + c.x), c.y / (1 + c.y), c.z / (1 + c.z));
2662
2759
  }
2663
- _moveGrounded(right, up, forward) {
2664
- const yaw = Math.atan2(this.target[0] - this.position[0], this.target[2] - this.position[2]);
2665
- const dx = Math.sin(yaw) * forward + Math.cos(yaw) * right;
2666
- const dz = Math.cos(yaw) * forward - Math.sin(yaw) * right;
2667
- this.position[0] += dx;
2668
- this.position[1] += up;
2669
- this.position[2] += dz;
2670
- this.target[0] += dx;
2671
- this.target[1] += up;
2672
- this.target[2] += dz;
2673
- }
2674
- _moveGlobal(right, up, forward) {
2675
- this.position[0] += right;
2676
- this.position[1] += up;
2677
- this.position[2] += forward;
2678
- this.target[0] += right;
2679
- this.target[1] += up;
2680
- this.target[2] += forward;
2681
- }
2682
- /**
2683
- * Orbit around the target point. Zero allocations.
2684
- * @param yawDelta Horizontal rotation in radians (positive = rotate right)
2685
- * @param pitchDelta Vertical rotation in radians (positive = rotate up)
2686
- */
2687
- orbit(yawDelta, pitchDelta) {
2688
- let ox = this.position[0] - this.target[0];
2689
- let oy = this.position[1] - this.target[1];
2690
- let oz = this.position[2] - this.target[2];
2691
- const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
2692
- let yaw = Math.atan2(ox, oz);
2693
- let pitch = Math.asin(oy / dist);
2694
- yaw += yawDelta;
2695
- pitch += pitchDelta;
2696
- pitch = Math.max(-Math.PI * 0.49, Math.min(Math.PI * 0.49, pitch));
2697
- this.position[0] = this.target[0] + Math.sin(yaw) * Math.cos(pitch) * dist;
2698
- this.position[1] = this.target[1] + Math.sin(pitch) * dist;
2699
- this.position[2] = this.target[2] + Math.cos(yaw) * Math.cos(pitch) * dist;
2700
- }
2701
- /**
2702
- * Zoom by adjusting distance to target. Zero allocations.
2703
- * @param delta Positive = zoom in, negative = zoom out
2704
- */
2705
- zoom(delta) {
2706
- let ox = this.position[0] - this.target[0];
2707
- let oy = this.position[1] - this.target[1];
2708
- let oz = this.position[2] - this.target[2];
2709
- const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
2710
- const newDist = Math.max(0.1, dist - delta);
2711
- const scale = newDist / dist;
2712
- this.position[0] = this.target[0] + ox * scale;
2713
- this.position[1] = this.target[1] + oy * scale;
2714
- this.position[2] = this.target[2] + oz * scale;
2715
- }
2716
- };
2717
- function lookAt(out, eye, center, up) {
2718
- let fx = center[0] - eye[0];
2719
- let fy = center[1] - eye[1];
2720
- let fz = center[2] - eye[2];
2721
- let len = 1 / Math.sqrt(fx * fx + fy * fy + fz * fz);
2722
- fx *= len;
2723
- fy *= len;
2724
- fz *= len;
2725
- let sx = fy * up[2] - fz * up[1];
2726
- let sy = fz * up[0] - fx * up[2];
2727
- let sz = fx * up[1] - fy * up[0];
2728
- len = Math.sqrt(sx * sx + sy * sy + sz * sz);
2729
- if (len > 0) {
2730
- len = 1 / len;
2731
- sx *= len;
2732
- sy *= len;
2733
- sz *= len;
2734
- }
2735
- const ux = sy * fz - sz * fy;
2736
- const uy = sz * fx - sx * fz;
2737
- const uz = sx * fy - sy * fx;
2738
- out[0] = sx;
2739
- out[1] = ux;
2740
- out[2] = -fx;
2741
- out[3] = 0;
2742
- out[4] = sy;
2743
- out[5] = uy;
2744
- out[6] = -fy;
2745
- out[7] = 0;
2746
- out[8] = sz;
2747
- out[9] = uz;
2748
- out[10] = -fz;
2749
- out[11] = 0;
2750
- out[12] = -(sx * eye[0] + sy * eye[1] + sz * eye[2]);
2751
- out[13] = -(ux * eye[0] + uy * eye[1] + uz * eye[2]);
2752
- out[14] = fx * eye[0] + fy * eye[1] + fz * eye[2];
2753
- out[15] = 1;
2754
- }
2755
- function perspective(out, fovRad, aspect, near, far) {
2756
- const f = 1 / Math.tan(fovRad * 0.5);
2757
- const rangeInv = 1 / (near - far);
2758
- out[0] = f / aspect;
2759
- out[1] = 0;
2760
- out[2] = 0;
2761
- out[3] = 0;
2762
- out[4] = 0;
2763
- out[5] = f;
2764
- out[6] = 0;
2765
- out[7] = 0;
2766
- out[8] = 0;
2767
- out[9] = 0;
2768
- out[10] = (near + far) * rangeInv;
2769
- out[11] = -1;
2770
- out[12] = 0;
2771
- out[13] = 0;
2772
- out[14] = 2 * near * far * rangeInv;
2773
- out[15] = 0;
2774
- }
2775
- function mat4Multiply(out, a, b) {
2776
- for (let i = 0; i < 4; i++) {
2777
- const ai0 = a[i], ai1 = a[i + 4], ai2 = a[i + 8], ai3 = a[i + 12];
2778
- out[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3];
2779
- out[i + 4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7];
2780
- out[i + 8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11];
2781
- out[i + 12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15];
2782
- }
2783
- }
2760
+ );
2784
2761
 
2785
2762
  // src/3d/shader.ts
2786
- import tgpu5 from "typegpu";
2787
- import * as d5 from "typegpu/data";
2788
- import * as std3 from "typegpu/std";
2763
+ var MAX_LIGHTS = 64;
2789
2764
  function createMeshLayout(maxInstances) {
2790
- return tgpu5.bindGroupLayout({
2765
+ return tgpu6.bindGroupLayout({
2791
2766
  uniforms: { uniform: MeshUniforms },
2792
- dynamicInstances: { storage: d5.arrayOf(DynamicMesh, maxInstances) },
2793
- staticInstances: { storage: d5.arrayOf(StaticMesh, maxInstances) },
2794
- slotIndices: { storage: d5.arrayOf(d5.u32, maxInstances) }
2767
+ dynamicInstances: { storage: d6.arrayOf(DynamicMesh, maxInstances) },
2768
+ staticInstances: { storage: d6.arrayOf(StaticMesh, maxInstances) },
2769
+ slotIndices: { storage: d6.arrayOf(d6.u32, maxInstances) },
2770
+ lights: { storage: d6.arrayOf(Light, MAX_LIGHTS) }
2795
2771
  });
2796
2772
  }
2797
2773
  function createMeshVertex(meshLayout) {
2798
- return tgpu5.vertexFn({
2774
+ return tgpu6.vertexFn({
2799
2775
  in: {
2800
- position: d5.location(0, d5.vec3f),
2801
- normal: d5.location(1, d5.vec3f),
2802
- instanceIndex: d5.builtin.instanceIndex
2776
+ position: d6.location(0, d6.vec3f),
2777
+ normal: d6.location(1, d6.vec3f),
2778
+ instanceIndex: d6.builtin.instanceIndex
2803
2779
  },
2804
2780
  out: {
2805
- pos: d5.builtin.position,
2806
- vNormal: d5.vec3f,
2807
- vColor: d5.vec3f
2781
+ pos: d6.builtin.position,
2782
+ vNormal: d6.vec3f,
2783
+ vColor: d6.vec3f,
2784
+ vWorldPos: d6.vec3f
2808
2785
  }
2809
2786
  })(function(input) {
2810
2787
  const instanceIndex = input.instanceIndex;
@@ -2812,115 +2789,144 @@ function createMeshVertex(meshLayout) {
2812
2789
  const dyn = meshLayout.$.dynamicInstances[slot];
2813
2790
  const stat = meshLayout.$.staticInstances[slot];
2814
2791
  const alpha = meshLayout.$.uniforms.alpha;
2815
- const px = std3.mix(dyn.prevPosX, dyn.currPosX, alpha);
2816
- const py = std3.mix(dyn.prevPosY, dyn.currPosY, alpha);
2817
- const pz = std3.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
2818
- const rx = std3.mix(dyn.prevRotX, dyn.currRotX, alpha);
2819
- const ry = std3.mix(dyn.prevRotY, dyn.currRotY, alpha);
2820
- const rz = std3.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
2792
+ const px = std4.mix(dyn.prevPosX, dyn.currPosX, alpha);
2793
+ const py = std4.mix(dyn.prevPosY, dyn.currPosY, alpha);
2794
+ const pz = std4.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
2795
+ const rx = std4.mix(dyn.prevRotX, dyn.currRotX, alpha);
2796
+ const ry = std4.mix(dyn.prevRotY, dyn.currRotY, alpha);
2797
+ const rz = std4.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
2821
2798
  const sx = stat.scaleX;
2822
2799
  const sy = stat.scaleY;
2823
2800
  const sz = stat.scaleZ;
2824
- const scaled = d5.vec3f(
2825
- std3.mul(input.position.x, sx),
2826
- std3.mul(input.position.y, sy),
2827
- std3.mul(input.position.z, sz)
2801
+ const scaled = d6.vec3f(
2802
+ std4.mul(input.position.x, sx),
2803
+ std4.mul(input.position.y, sy),
2804
+ std4.mul(input.position.z, sz)
2828
2805
  );
2829
- const czr = std3.cos(rz);
2830
- const szr = std3.sin(rz);
2831
- const rz1 = d5.vec3f(
2832
- std3.sub(std3.mul(scaled.x, czr), std3.mul(scaled.y, szr)),
2833
- std3.add(std3.mul(scaled.x, szr), std3.mul(scaled.y, czr)),
2806
+ const czr = std4.cos(rz);
2807
+ const szr = std4.sin(rz);
2808
+ const rz1 = d6.vec3f(
2809
+ std4.sub(std4.mul(scaled.x, czr), std4.mul(scaled.y, szr)),
2810
+ std4.add(std4.mul(scaled.x, szr), std4.mul(scaled.y, czr)),
2834
2811
  scaled.z
2835
2812
  );
2836
- const cyr = std3.cos(ry);
2837
- const syr = std3.sin(ry);
2838
- const ry1 = d5.vec3f(
2839
- std3.add(std3.mul(rz1.x, cyr), std3.mul(rz1.z, syr)),
2813
+ const cyr = std4.cos(ry);
2814
+ const syr = std4.sin(ry);
2815
+ const ry1 = d6.vec3f(
2816
+ std4.add(std4.mul(rz1.x, cyr), std4.mul(rz1.z, syr)),
2840
2817
  rz1.y,
2841
- std3.sub(std3.mul(rz1.z, cyr), std3.mul(rz1.x, syr))
2818
+ std4.sub(std4.mul(rz1.z, cyr), std4.mul(rz1.x, syr))
2842
2819
  );
2843
- const cxr = std3.cos(rx);
2844
- const sxr = std3.sin(rx);
2845
- const rx1 = d5.vec3f(
2820
+ const cxr = std4.cos(rx);
2821
+ const sxr = std4.sin(rx);
2822
+ const rx1 = d6.vec3f(
2846
2823
  ry1.x,
2847
- std3.sub(std3.mul(ry1.y, cxr), std3.mul(ry1.z, sxr)),
2848
- std3.add(std3.mul(ry1.y, sxr), std3.mul(ry1.z, cxr))
2824
+ std4.sub(std4.mul(ry1.y, cxr), std4.mul(ry1.z, sxr)),
2825
+ std4.add(std4.mul(ry1.y, sxr), std4.mul(ry1.z, cxr))
2849
2826
  );
2850
- const worldPos = d5.vec4f(
2851
- std3.add(rx1.x, px),
2852
- std3.add(rx1.y, py),
2853
- std3.add(rx1.z, pz),
2827
+ const worldPos = d6.vec4f(
2828
+ std4.add(rx1.x, px),
2829
+ std4.add(rx1.y, py),
2830
+ std4.add(rx1.z, pz),
2854
2831
  1
2855
2832
  );
2856
2833
  const nScaled = input.normal;
2857
- const nRz = d5.vec3f(
2858
- std3.sub(std3.mul(nScaled.x, czr), std3.mul(nScaled.y, szr)),
2859
- std3.add(std3.mul(nScaled.x, szr), std3.mul(nScaled.y, czr)),
2834
+ const nRz = d6.vec3f(
2835
+ std4.sub(std4.mul(nScaled.x, czr), std4.mul(nScaled.y, szr)),
2836
+ std4.add(std4.mul(nScaled.x, szr), std4.mul(nScaled.y, czr)),
2860
2837
  nScaled.z
2861
2838
  );
2862
- const nRy = d5.vec3f(
2863
- std3.add(std3.mul(nRz.x, cyr), std3.mul(nRz.z, syr)),
2839
+ const nRy = d6.vec3f(
2840
+ std4.add(std4.mul(nRz.x, cyr), std4.mul(nRz.z, syr)),
2864
2841
  nRz.y,
2865
- std3.sub(std3.mul(nRz.z, cyr), std3.mul(nRz.x, syr))
2842
+ std4.sub(std4.mul(nRz.z, cyr), std4.mul(nRz.x, syr))
2866
2843
  );
2867
- const nRx = d5.vec3f(
2844
+ const nRx = d6.vec3f(
2868
2845
  nRy.x,
2869
- std3.sub(std3.mul(nRy.y, cxr), std3.mul(nRy.z, sxr)),
2870
- std3.add(std3.mul(nRy.y, sxr), std3.mul(nRy.z, cxr))
2846
+ std4.sub(std4.mul(nRy.y, cxr), std4.mul(nRy.z, sxr)),
2847
+ std4.add(std4.mul(nRy.y, sxr), std4.mul(nRy.z, cxr))
2871
2848
  );
2872
- const clipPos = std3.mul(meshLayout.$.uniforms.viewProjection, worldPos);
2849
+ const clipPos = std4.mul(meshLayout.$.uniforms.viewProjection, worldPos);
2873
2850
  return {
2874
2851
  pos: clipPos,
2875
2852
  vNormal: nRx,
2876
- vColor: d5.vec3f(stat.colorR, stat.colorG, stat.colorB)
2853
+ vColor: d6.vec3f(stat.colorR, stat.colorG, stat.colorB),
2854
+ vWorldPos: d6.vec3f(worldPos.x, worldPos.y, worldPos.z)
2877
2855
  };
2878
2856
  });
2879
2857
  }
2880
2858
  function createMeshFragment(meshLayout) {
2881
- return tgpu5.fragmentFn({
2859
+ return tgpu6.fragmentFn({
2882
2860
  in: {
2883
- vNormal: d5.vec3f,
2884
- vColor: d5.vec3f
2861
+ vNormal: d6.vec3f,
2862
+ vColor: d6.vec3f,
2863
+ vWorldPos: d6.vec3f
2885
2864
  },
2886
- out: d5.vec4f
2865
+ out: d6.vec4f
2887
2866
  })(function(input) {
2888
- const normal = std3.normalize(input.vNormal);
2889
- const lightDir = std3.normalize(d5.vec3f(
2890
- meshLayout.$.uniforms.lightDirX,
2891
- meshLayout.$.uniforms.lightDirY,
2892
- meshLayout.$.uniforms.lightDirZ
2893
- ));
2894
- const diff = std3.max(std3.dot(normal, lightDir), 0);
2895
- const ambient = std3.mul(input.vColor, 0.3);
2896
- const diffuse = std3.mul(input.vColor, diff);
2897
- return d5.vec4f(
2898
- std3.add(ambient.x, diffuse.x),
2899
- std3.add(ambient.y, diffuse.y),
2900
- std3.add(ambient.z, diffuse.z),
2901
- 1
2867
+ const u = meshLayout.$.uniforms;
2868
+ const baseColor = input.vColor;
2869
+ const worldPos = input.vWorldPos;
2870
+ const normal = std4.normalize(input.vNormal);
2871
+ const lightDir = std4.normalize(d6.vec3f(u.lightDirX, u.lightDirY, u.lightDirZ));
2872
+ const diff = std4.max(std4.dot(normal, lightDir), 0) * u.lightDirIntensity;
2873
+ let acc = d6.vec3f(
2874
+ baseColor.x * (u.ambientR + u.lightDirR * diff),
2875
+ baseColor.y * (u.ambientG + u.lightDirG * diff),
2876
+ baseColor.z * (u.ambientB + u.lightDirB * diff)
2902
2877
  );
2878
+ const count = u.lightCount;
2879
+ const a = u.alpha;
2880
+ for (let i = d6.u32(0); i < count; i++) {
2881
+ const L = meshLayout.$.lights[i];
2882
+ const pos = d6.vec3f(
2883
+ std4.mix(L.prevPosX, L.currPosX, a),
2884
+ std4.mix(L.prevPosY, L.currPosY, a),
2885
+ std4.mix(L.prevPosZ, L.currPosZ, a)
2886
+ );
2887
+ const axis = d6.vec3f(
2888
+ std4.mix(L.prevDirX, L.currDirX, a),
2889
+ std4.mix(L.prevDirY, L.currDirY, a),
2890
+ std4.mix(L.prevDirZ, L.currDirZ, a)
2891
+ );
2892
+ const c = lightContribution(
2893
+ pos,
2894
+ axis,
2895
+ d6.vec3f(L.colorR, L.colorG, L.colorB),
2896
+ d6.vec4f(L.intensity, L.range, L.innerCos, L.outerCos),
2897
+ normal,
2898
+ worldPos
2899
+ );
2900
+ acc = d6.vec3f(
2901
+ acc.x + baseColor.x * c.x,
2902
+ acc.y + baseColor.y * c.y,
2903
+ acc.z + baseColor.z * c.z
2904
+ );
2905
+ }
2906
+ const mapped = tonemap(acc);
2907
+ return d6.vec4f(mapped.x, mapped.y, mapped.z, 1);
2903
2908
  });
2904
2909
  }
2905
2910
  function createTextureBindGroupLayout() {
2906
- return tgpu5.bindGroupLayout({
2911
+ return tgpu6.bindGroupLayout({
2907
2912
  modelTexture: { texture: "float" },
2908
2913
  modelSampler: { sampler: "filtering" }
2909
2914
  });
2910
2915
  }
2911
2916
  function createTexturedMeshVertex(meshLayout) {
2912
- return tgpu5.vertexFn({
2917
+ return tgpu6.vertexFn({
2913
2918
  in: {
2914
- position: d5.location(0, d5.vec3f),
2915
- normal: d5.location(1, d5.vec3f),
2916
- uv: d5.location(2, d5.vec2f),
2917
- instanceIndex: d5.builtin.instanceIndex
2919
+ position: d6.location(0, d6.vec3f),
2920
+ normal: d6.location(1, d6.vec3f),
2921
+ uv: d6.location(2, d6.vec2f),
2922
+ instanceIndex: d6.builtin.instanceIndex
2918
2923
  },
2919
2924
  out: {
2920
- pos: d5.builtin.position,
2921
- vNormal: d5.vec3f,
2922
- vColor: d5.vec3f,
2923
- vUV: d5.vec2f
2925
+ pos: d6.builtin.position,
2926
+ vNormal: d6.vec3f,
2927
+ vColor: d6.vec3f,
2928
+ vUV: d6.vec2f,
2929
+ vWorldPos: d6.vec3f
2924
2930
  }
2925
2931
  })(function(input) {
2926
2932
  const instanceIndex = input.instanceIndex;
@@ -2928,128 +2934,157 @@ function createTexturedMeshVertex(meshLayout) {
2928
2934
  const dyn = meshLayout.$.dynamicInstances[slot];
2929
2935
  const stat = meshLayout.$.staticInstances[slot];
2930
2936
  const alpha = meshLayout.$.uniforms.alpha;
2931
- const px = std3.mix(dyn.prevPosX, dyn.currPosX, alpha);
2932
- const py = std3.mix(dyn.prevPosY, dyn.currPosY, alpha);
2933
- const pz = std3.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
2934
- const rx = std3.mix(dyn.prevRotX, dyn.currRotX, alpha);
2935
- const ry = std3.mix(dyn.prevRotY, dyn.currRotY, alpha);
2936
- const rz = std3.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
2937
+ const px = std4.mix(dyn.prevPosX, dyn.currPosX, alpha);
2938
+ const py = std4.mix(dyn.prevPosY, dyn.currPosY, alpha);
2939
+ const pz = std4.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
2940
+ const rx = std4.mix(dyn.prevRotX, dyn.currRotX, alpha);
2941
+ const ry = std4.mix(dyn.prevRotY, dyn.currRotY, alpha);
2942
+ const rz = std4.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
2937
2943
  const sx = stat.scaleX;
2938
2944
  const sy = stat.scaleY;
2939
2945
  const sz = stat.scaleZ;
2940
- const scaled = d5.vec3f(
2941
- std3.mul(input.position.x, sx),
2942
- std3.mul(input.position.y, sy),
2943
- std3.mul(input.position.z, sz)
2946
+ const scaled = d6.vec3f(
2947
+ std4.mul(input.position.x, sx),
2948
+ std4.mul(input.position.y, sy),
2949
+ std4.mul(input.position.z, sz)
2944
2950
  );
2945
- const czr = std3.cos(rz);
2946
- const szr = std3.sin(rz);
2947
- const rz1 = d5.vec3f(
2948
- std3.sub(std3.mul(scaled.x, czr), std3.mul(scaled.y, szr)),
2949
- std3.add(std3.mul(scaled.x, szr), std3.mul(scaled.y, czr)),
2951
+ const czr = std4.cos(rz);
2952
+ const szr = std4.sin(rz);
2953
+ const rz1 = d6.vec3f(
2954
+ std4.sub(std4.mul(scaled.x, czr), std4.mul(scaled.y, szr)),
2955
+ std4.add(std4.mul(scaled.x, szr), std4.mul(scaled.y, czr)),
2950
2956
  scaled.z
2951
2957
  );
2952
- const cyr = std3.cos(ry);
2953
- const syr = std3.sin(ry);
2954
- const ry1 = d5.vec3f(
2955
- std3.add(std3.mul(rz1.x, cyr), std3.mul(rz1.z, syr)),
2958
+ const cyr = std4.cos(ry);
2959
+ const syr = std4.sin(ry);
2960
+ const ry1 = d6.vec3f(
2961
+ std4.add(std4.mul(rz1.x, cyr), std4.mul(rz1.z, syr)),
2956
2962
  rz1.y,
2957
- std3.sub(std3.mul(rz1.z, cyr), std3.mul(rz1.x, syr))
2963
+ std4.sub(std4.mul(rz1.z, cyr), std4.mul(rz1.x, syr))
2958
2964
  );
2959
- const cxr = std3.cos(rx);
2960
- const sxr = std3.sin(rx);
2961
- const rx1 = d5.vec3f(
2965
+ const cxr = std4.cos(rx);
2966
+ const sxr = std4.sin(rx);
2967
+ const rx1 = d6.vec3f(
2962
2968
  ry1.x,
2963
- std3.sub(std3.mul(ry1.y, cxr), std3.mul(ry1.z, sxr)),
2964
- std3.add(std3.mul(ry1.y, sxr), std3.mul(ry1.z, cxr))
2969
+ std4.sub(std4.mul(ry1.y, cxr), std4.mul(ry1.z, sxr)),
2970
+ std4.add(std4.mul(ry1.y, sxr), std4.mul(ry1.z, cxr))
2965
2971
  );
2966
- const worldPos = d5.vec4f(
2967
- std3.add(rx1.x, px),
2968
- std3.add(rx1.y, py),
2969
- std3.add(rx1.z, pz),
2972
+ const worldPos = d6.vec4f(
2973
+ std4.add(rx1.x, px),
2974
+ std4.add(rx1.y, py),
2975
+ std4.add(rx1.z, pz),
2970
2976
  1
2971
2977
  );
2972
2978
  const nScaled = input.normal;
2973
- const nRz = d5.vec3f(
2974
- std3.sub(std3.mul(nScaled.x, czr), std3.mul(nScaled.y, szr)),
2975
- std3.add(std3.mul(nScaled.x, szr), std3.mul(nScaled.y, czr)),
2979
+ const nRz = d6.vec3f(
2980
+ std4.sub(std4.mul(nScaled.x, czr), std4.mul(nScaled.y, szr)),
2981
+ std4.add(std4.mul(nScaled.x, szr), std4.mul(nScaled.y, czr)),
2976
2982
  nScaled.z
2977
2983
  );
2978
- const nRy = d5.vec3f(
2979
- std3.add(std3.mul(nRz.x, cyr), std3.mul(nRz.z, syr)),
2984
+ const nRy = d6.vec3f(
2985
+ std4.add(std4.mul(nRz.x, cyr), std4.mul(nRz.z, syr)),
2980
2986
  nRz.y,
2981
- std3.sub(std3.mul(nRz.z, cyr), std3.mul(nRz.x, syr))
2987
+ std4.sub(std4.mul(nRz.z, cyr), std4.mul(nRz.x, syr))
2982
2988
  );
2983
- const nRx = d5.vec3f(
2989
+ const nRx = d6.vec3f(
2984
2990
  nRy.x,
2985
- std3.sub(std3.mul(nRy.y, cxr), std3.mul(nRy.z, sxr)),
2986
- std3.add(std3.mul(nRy.y, sxr), std3.mul(nRy.z, cxr))
2991
+ std4.sub(std4.mul(nRy.y, cxr), std4.mul(nRy.z, sxr)),
2992
+ std4.add(std4.mul(nRy.y, sxr), std4.mul(nRy.z, cxr))
2987
2993
  );
2988
- const clipPos = std3.mul(meshLayout.$.uniforms.viewProjection, worldPos);
2994
+ const clipPos = std4.mul(meshLayout.$.uniforms.viewProjection, worldPos);
2989
2995
  return {
2990
2996
  pos: clipPos,
2991
2997
  vNormal: nRx,
2992
- vColor: d5.vec3f(stat.colorR, stat.colorG, stat.colorB),
2993
- vUV: input.uv
2998
+ vColor: d6.vec3f(stat.colorR, stat.colorG, stat.colorB),
2999
+ vUV: input.uv,
3000
+ vWorldPos: d6.vec3f(worldPos.x, worldPos.y, worldPos.z)
2994
3001
  };
2995
3002
  });
2996
3003
  }
2997
3004
  function createTexturedMeshFragment(meshLayout, texLayout) {
2998
- return tgpu5.fragmentFn({
3005
+ return tgpu6.fragmentFn({
2999
3006
  in: {
3000
- vNormal: d5.vec3f,
3001
- vColor: d5.vec3f,
3002
- vUV: d5.vec2f
3007
+ vNormal: d6.vec3f,
3008
+ vColor: d6.vec3f,
3009
+ vUV: d6.vec2f,
3010
+ vWorldPos: d6.vec3f
3003
3011
  },
3004
- out: d5.vec4f
3012
+ out: d6.vec4f
3005
3013
  })(function(input) {
3006
- const normal = std3.normalize(input.vNormal);
3007
- const lightDir = std3.normalize(d5.vec3f(
3008
- meshLayout.$.uniforms.lightDirX,
3009
- meshLayout.$.uniforms.lightDirY,
3010
- meshLayout.$.uniforms.lightDirZ
3011
- ));
3012
- const texColor = std3.textureSample(texLayout.$.modelTexture, texLayout.$.modelSampler, input.vUV);
3013
- const baseColor = d5.vec3f(
3014
- std3.mul(texColor.x, input.vColor.x),
3015
- std3.mul(texColor.y, input.vColor.y),
3016
- std3.mul(texColor.z, input.vColor.z)
3014
+ const u = meshLayout.$.uniforms;
3015
+ const worldPos = input.vWorldPos;
3016
+ const normal = std4.normalize(input.vNormal);
3017
+ const texColor = std4.textureSample(texLayout.$.modelTexture, texLayout.$.modelSampler, input.vUV);
3018
+ const baseColor = d6.vec3f(
3019
+ std4.mul(texColor.x, input.vColor.x),
3020
+ std4.mul(texColor.y, input.vColor.y),
3021
+ std4.mul(texColor.z, input.vColor.z)
3017
3022
  );
3018
- const diff = std3.max(std3.dot(normal, lightDir), 0);
3019
- const ambient = std3.mul(baseColor, 0.3);
3020
- const diffuse = std3.mul(baseColor, diff);
3021
- return d5.vec4f(
3022
- std3.add(ambient.x, diffuse.x),
3023
- std3.add(ambient.y, diffuse.y),
3024
- std3.add(ambient.z, diffuse.z),
3025
- std3.mul(texColor.w, 1)
3023
+ const lightDir = std4.normalize(d6.vec3f(u.lightDirX, u.lightDirY, u.lightDirZ));
3024
+ const diff = std4.max(std4.dot(normal, lightDir), 0) * u.lightDirIntensity;
3025
+ let acc = d6.vec3f(
3026
+ baseColor.x * (u.ambientR + u.lightDirR * diff),
3027
+ baseColor.y * (u.ambientG + u.lightDirG * diff),
3028
+ baseColor.z * (u.ambientB + u.lightDirB * diff)
3026
3029
  );
3030
+ const count = u.lightCount;
3031
+ const a = u.alpha;
3032
+ for (let i = d6.u32(0); i < count; i++) {
3033
+ const L = meshLayout.$.lights[i];
3034
+ const pos = d6.vec3f(
3035
+ std4.mix(L.prevPosX, L.currPosX, a),
3036
+ std4.mix(L.prevPosY, L.currPosY, a),
3037
+ std4.mix(L.prevPosZ, L.currPosZ, a)
3038
+ );
3039
+ const axis = d6.vec3f(
3040
+ std4.mix(L.prevDirX, L.currDirX, a),
3041
+ std4.mix(L.prevDirY, L.currDirY, a),
3042
+ std4.mix(L.prevDirZ, L.currDirZ, a)
3043
+ );
3044
+ const c = lightContribution(
3045
+ pos,
3046
+ axis,
3047
+ d6.vec3f(L.colorR, L.colorG, L.colorB),
3048
+ d6.vec4f(L.intensity, L.range, L.innerCos, L.outerCos),
3049
+ normal,
3050
+ worldPos
3051
+ );
3052
+ acc = d6.vec3f(
3053
+ acc.x + baseColor.x * c.x,
3054
+ acc.y + baseColor.y * c.y,
3055
+ acc.z + baseColor.z * c.z
3056
+ );
3057
+ }
3058
+ const mapped = tonemap(acc);
3059
+ return d6.vec4f(mapped.x, mapped.y, mapped.z, texColor.w);
3027
3060
  });
3028
3061
  }
3029
3062
  function createSkinnedMeshLayout(maxInstances, maxBones) {
3030
- return tgpu5.bindGroupLayout({
3063
+ return tgpu6.bindGroupLayout({
3031
3064
  uniforms: { uniform: MeshUniforms },
3032
- dynamicInstances: { storage: d5.arrayOf(DynamicMesh, maxInstances) },
3033
- staticInstances: { storage: d5.arrayOf(SkinnedStaticMesh, maxInstances) },
3034
- slotIndices: { storage: d5.arrayOf(d5.u32, maxInstances) },
3035
- boneMatrices: { storage: d5.arrayOf(d5.mat4x4f, maxBones) }
3065
+ dynamicInstances: { storage: d6.arrayOf(DynamicMesh, maxInstances) },
3066
+ staticInstances: { storage: d6.arrayOf(SkinnedStaticMesh, maxInstances) },
3067
+ slotIndices: { storage: d6.arrayOf(d6.u32, maxInstances) },
3068
+ boneMatrices: { storage: d6.arrayOf(d6.mat4x4f, maxBones) },
3069
+ lights: { storage: d6.arrayOf(Light, MAX_LIGHTS) }
3036
3070
  });
3037
3071
  }
3038
3072
  function createSkinnedMeshVertex(layout) {
3039
- return tgpu5.vertexFn({
3073
+ return tgpu6.vertexFn({
3040
3074
  in: {
3041
- position: d5.location(0, d5.vec3f),
3042
- normal: d5.location(1, d5.vec3f),
3043
- uv: d5.location(2, d5.vec2f),
3044
- joints: d5.location(3, d5.vec4u),
3045
- weights: d5.location(4, d5.vec4f),
3046
- instanceIndex: d5.builtin.instanceIndex
3075
+ position: d6.location(0, d6.vec3f),
3076
+ normal: d6.location(1, d6.vec3f),
3077
+ uv: d6.location(2, d6.vec2f),
3078
+ joints: d6.location(3, d6.vec4u),
3079
+ weights: d6.location(4, d6.vec4f),
3080
+ instanceIndex: d6.builtin.instanceIndex
3047
3081
  },
3048
3082
  out: {
3049
- pos: d5.builtin.position,
3050
- vNormal: d5.vec3f,
3051
- vColor: d5.vec3f,
3052
- vUV: d5.vec2f
3083
+ pos: d6.builtin.position,
3084
+ vNormal: d6.vec3f,
3085
+ vColor: d6.vec3f,
3086
+ vUV: d6.vec2f,
3087
+ vWorldPos: d6.vec3f
3053
3088
  }
3054
3089
  })(function(input) {
3055
3090
  const slot = layout.$.slotIndices[input.instanceIndex];
@@ -3069,69 +3104,69 @@ function createSkinnedMeshVertex(layout) {
3069
3104
  const m1 = layout.$.boneMatrices[boneOffset + j1];
3070
3105
  const m2 = layout.$.boneMatrices[boneOffset + j2];
3071
3106
  const m3 = layout.$.boneMatrices[boneOffset + j3];
3072
- const p = d5.vec4f(input.position.x, input.position.y, input.position.z, 1);
3107
+ const p = d6.vec4f(input.position.x, input.position.y, input.position.z, 1);
3073
3108
  const sp0 = m0 * p;
3074
3109
  const sp1 = m1 * p;
3075
3110
  const sp2 = m2 * p;
3076
3111
  const sp3 = m3 * p;
3077
- const skinnedPos = d5.vec3f(
3112
+ const skinnedPos = d6.vec3f(
3078
3113
  sp0.x * w0 + sp1.x * w1 + sp2.x * w2 + sp3.x * w3,
3079
3114
  sp0.y * w0 + sp1.y * w1 + sp2.y * w2 + sp3.y * w3,
3080
3115
  sp0.z * w0 + sp1.z * w1 + sp2.z * w2 + sp3.z * w3
3081
3116
  );
3082
- const n = d5.vec4f(input.normal.x, input.normal.y, input.normal.z, 0);
3117
+ const n = d6.vec4f(input.normal.x, input.normal.y, input.normal.z, 0);
3083
3118
  const sn0 = m0 * n;
3084
3119
  const sn1 = m1 * n;
3085
3120
  const sn2 = m2 * n;
3086
3121
  const sn3 = m3 * n;
3087
- const skinnedNormal = d5.vec3f(
3122
+ const skinnedNormal = d6.vec3f(
3088
3123
  sn0.x * w0 + sn1.x * w1 + sn2.x * w2 + sn3.x * w3,
3089
3124
  sn0.y * w0 + sn1.y * w1 + sn2.y * w2 + sn3.y * w3,
3090
3125
  sn0.z * w0 + sn1.z * w1 + sn2.z * w2 + sn3.z * w3
3091
3126
  );
3092
- const px = std3.mix(dyn.prevPosX, dyn.currPosX, alpha);
3093
- const py = std3.mix(dyn.prevPosY, dyn.currPosY, alpha);
3094
- const pz = std3.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
3095
- const rx = std3.mix(dyn.prevRotX, dyn.currRotX, alpha);
3096
- const ry = std3.mix(dyn.prevRotY, dyn.currRotY, alpha);
3097
- const rz = std3.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
3127
+ const px = std4.mix(dyn.prevPosX, dyn.currPosX, alpha);
3128
+ const py = std4.mix(dyn.prevPosY, dyn.currPosY, alpha);
3129
+ const pz = std4.mix(dyn.prevPosZ, dyn.currPosZ, alpha);
3130
+ const rx = std4.mix(dyn.prevRotX, dyn.currRotX, alpha);
3131
+ const ry = std4.mix(dyn.prevRotY, dyn.currRotY, alpha);
3132
+ const rz = std4.mix(dyn.prevRotZ, dyn.currRotZ, alpha);
3098
3133
  const sx = stat.scaleX;
3099
3134
  const sy = stat.scaleY;
3100
3135
  const sz = stat.scaleZ;
3101
- const scaled = d5.vec3f(skinnedPos.x * sx, skinnedPos.y * sy, skinnedPos.z * sz);
3102
- const czr = std3.cos(rz);
3103
- const szr = std3.sin(rz);
3104
- const rz1 = d5.vec3f(
3136
+ const scaled = d6.vec3f(skinnedPos.x * sx, skinnedPos.y * sy, skinnedPos.z * sz);
3137
+ const czr = std4.cos(rz);
3138
+ const szr = std4.sin(rz);
3139
+ const rz1 = d6.vec3f(
3105
3140
  scaled.x * czr - scaled.y * szr,
3106
3141
  scaled.x * szr + scaled.y * czr,
3107
3142
  scaled.z
3108
3143
  );
3109
- const cyr = std3.cos(ry);
3110
- const syr = std3.sin(ry);
3111
- const ry1 = d5.vec3f(
3144
+ const cyr = std4.cos(ry);
3145
+ const syr = std4.sin(ry);
3146
+ const ry1 = d6.vec3f(
3112
3147
  rz1.x * cyr + rz1.z * syr,
3113
3148
  rz1.y,
3114
3149
  rz1.z * cyr - rz1.x * syr
3115
3150
  );
3116
- const cxr = std3.cos(rx);
3117
- const sxr = std3.sin(rx);
3118
- const rx1 = d5.vec3f(
3151
+ const cxr = std4.cos(rx);
3152
+ const sxr = std4.sin(rx);
3153
+ const rx1 = d6.vec3f(
3119
3154
  ry1.x,
3120
3155
  ry1.y * cxr - ry1.z * sxr,
3121
3156
  ry1.y * sxr + ry1.z * cxr
3122
3157
  );
3123
- const worldPos = d5.vec4f(rx1.x + px, rx1.y + py, rx1.z + pz, 1);
3124
- const nRz = d5.vec3f(
3158
+ const worldPos = d6.vec4f(rx1.x + px, rx1.y + py, rx1.z + pz, 1);
3159
+ const nRz = d6.vec3f(
3125
3160
  skinnedNormal.x * czr - skinnedNormal.y * szr,
3126
3161
  skinnedNormal.x * szr + skinnedNormal.y * czr,
3127
3162
  skinnedNormal.z
3128
3163
  );
3129
- const nRy = d5.vec3f(
3164
+ const nRy = d6.vec3f(
3130
3165
  nRz.x * cyr + nRz.z * syr,
3131
3166
  nRz.y,
3132
3167
  nRz.z * cyr - nRz.x * syr
3133
3168
  );
3134
- const nRx = d5.vec3f(
3169
+ const nRx = d6.vec3f(
3135
3170
  nRy.x,
3136
3171
  nRy.y * cxr - nRy.z * sxr,
3137
3172
  nRy.y * sxr + nRy.z * cxr
@@ -3140,11 +3175,625 @@ function createSkinnedMeshVertex(layout) {
3140
3175
  return {
3141
3176
  pos: clipPos,
3142
3177
  vNormal: nRx,
3143
- vColor: d5.vec3f(stat.colorR, stat.colorG, stat.colorB),
3144
- vUV: input.uv
3178
+ vColor: d6.vec3f(stat.colorR, stat.colorG, stat.colorB),
3179
+ vUV: input.uv,
3180
+ vWorldPos: d6.vec3f(worldPos.x, worldPos.y, worldPos.z)
3145
3181
  };
3146
3182
  });
3147
3183
  }
3184
+ function createSkinnedMeshFragment(meshLayout) {
3185
+ return tgpu6.fragmentFn({
3186
+ in: {
3187
+ vNormal: d6.vec3f,
3188
+ vColor: d6.vec3f,
3189
+ vUV: d6.vec2f,
3190
+ vWorldPos: d6.vec3f
3191
+ },
3192
+ out: d6.vec4f
3193
+ })(function(input) {
3194
+ const u = meshLayout.$.uniforms;
3195
+ const baseColor = input.vColor;
3196
+ const worldPos = input.vWorldPos;
3197
+ const normal = std4.normalize(input.vNormal);
3198
+ const lightDir = std4.normalize(d6.vec3f(u.lightDirX, u.lightDirY, u.lightDirZ));
3199
+ const diff = std4.max(std4.dot(normal, lightDir), 0) * u.lightDirIntensity;
3200
+ let acc = d6.vec3f(
3201
+ baseColor.x * (u.ambientR + u.lightDirR * diff),
3202
+ baseColor.y * (u.ambientG + u.lightDirG * diff),
3203
+ baseColor.z * (u.ambientB + u.lightDirB * diff)
3204
+ );
3205
+ const count = u.lightCount;
3206
+ const a = u.alpha;
3207
+ for (let i = d6.u32(0); i < count; i++) {
3208
+ const L = meshLayout.$.lights[i];
3209
+ const pos = d6.vec3f(
3210
+ std4.mix(L.prevPosX, L.currPosX, a),
3211
+ std4.mix(L.prevPosY, L.currPosY, a),
3212
+ std4.mix(L.prevPosZ, L.currPosZ, a)
3213
+ );
3214
+ const axis = d6.vec3f(
3215
+ std4.mix(L.prevDirX, L.currDirX, a),
3216
+ std4.mix(L.prevDirY, L.currDirY, a),
3217
+ std4.mix(L.prevDirZ, L.currDirZ, a)
3218
+ );
3219
+ const c = lightContribution(
3220
+ pos,
3221
+ axis,
3222
+ d6.vec3f(L.colorR, L.colorG, L.colorB),
3223
+ d6.vec4f(L.intensity, L.range, L.innerCos, L.outerCos),
3224
+ normal,
3225
+ worldPos
3226
+ );
3227
+ acc = d6.vec3f(
3228
+ acc.x + baseColor.x * c.x,
3229
+ acc.y + baseColor.y * c.y,
3230
+ acc.z + baseColor.z * c.z
3231
+ );
3232
+ }
3233
+ const mapped = tonemap(acc);
3234
+ return d6.vec4f(mapped.x, mapped.y, mapped.z, 1);
3235
+ });
3236
+ }
3237
+
3238
+ // src/3d/lights.ts
3239
+ import { SlotMap as SlotMap2 } from "murow/core/slot-map";
3240
+ var KIND = 0;
3241
+ var CURR_POS_X = 1;
3242
+ var CURR_POS_Y = 2;
3243
+ var CURR_POS_Z = 3;
3244
+ var PREV_POS_X = 4;
3245
+ var PREV_POS_Y = 5;
3246
+ var PREV_POS_Z = 6;
3247
+ var CURR_DIR_X = 7;
3248
+ var CURR_DIR_Y = 8;
3249
+ var CURR_DIR_Z = 9;
3250
+ var PREV_DIR_X = 10;
3251
+ var PREV_DIR_Y = 11;
3252
+ var PREV_DIR_Z = 12;
3253
+ var COL_R = 13;
3254
+ var COL_G = 14;
3255
+ var COL_B = 15;
3256
+ var INTENSITY = 16;
3257
+ var RANGE = 17;
3258
+ var INNER_COS = 18;
3259
+ var OUTER_COS = 19;
3260
+ var CASTS_SHADOW = 20;
3261
+ var SHADOW_INDEX = 21;
3262
+ var LightSystem = class {
3263
+ constructor(maxLights) {
3264
+ this.maxLights = maxLights;
3265
+ // Global directional + ambient terms (the classic fixed look; now configurable).
3266
+ this.dirDir = [0.3, 0.8, 0.5];
3267
+ this.dirColor = [1, 1, 1];
3268
+ this.dirIntensity = 1;
3269
+ this.ambient = [0.3, 0.3, 0.3];
3270
+ this.data = new Float32Array(maxLights * LIGHT_FLOATS);
3271
+ this.slots = new SlotMap2(maxLights);
3272
+ this.enabled = new Uint8Array(maxLights).fill(1);
3273
+ this.handles = new Array(maxLights).fill(null);
3274
+ this.uploadData = new Float32Array(maxLights * LIGHT_FLOATS);
3275
+ this.angle = new Float32Array(maxLights);
3276
+ this.smoothness = new Float32Array(maxLights);
3277
+ }
3278
+ /** Add a dynamic point or spot light. Throws past `maxLights`. */
3279
+ add(spec) {
3280
+ const slot = this.slots.add();
3281
+ if (slot === -1)
3282
+ throw new Error(`Max lights (${this.maxLights}) reached`);
3283
+ this.enabled[slot] = 1;
3284
+ this.writeSlot(slot, spec);
3285
+ const data = this.data;
3286
+ const enabledArr = this.enabled;
3287
+ const slots = this.slots;
3288
+ const handles = this.handles;
3289
+ const angleArr = this.angle;
3290
+ const smoothArr = this.smoothness;
3291
+ const base = slot * LIGHT_FLOATS;
3292
+ let destroyed = false;
3293
+ const posOut = [0, 0, 0];
3294
+ const dirOut = [0, 0, 0];
3295
+ const colOut = [0, 0, 0];
3296
+ const handle = {
3297
+ slot,
3298
+ setPosition(x, y, z) {
3299
+ data[base + CURR_POS_X] = x;
3300
+ data[base + CURR_POS_Y] = y;
3301
+ data[base + CURR_POS_Z] = z;
3302
+ },
3303
+ setDirection(x, y, z) {
3304
+ data[base + CURR_DIR_X] = x;
3305
+ data[base + CURR_DIR_Y] = y;
3306
+ data[base + CURR_DIR_Z] = z;
3307
+ },
3308
+ teleport(x, y, z) {
3309
+ data[base + CURR_POS_X] = x;
3310
+ data[base + CURR_POS_Y] = y;
3311
+ data[base + CURR_POS_Z] = z;
3312
+ data[base + PREV_POS_X] = x;
3313
+ data[base + PREV_POS_Y] = y;
3314
+ data[base + PREV_POS_Z] = z;
3315
+ },
3316
+ setColor(r, g, b) {
3317
+ data[base + COL_R] = r;
3318
+ data[base + COL_G] = g;
3319
+ data[base + COL_B] = b;
3320
+ },
3321
+ get position() {
3322
+ posOut[0] = data[base + CURR_POS_X];
3323
+ posOut[1] = data[base + CURR_POS_Y];
3324
+ posOut[2] = data[base + CURR_POS_Z];
3325
+ return posOut;
3326
+ },
3327
+ get direction() {
3328
+ dirOut[0] = data[base + CURR_DIR_X];
3329
+ dirOut[1] = data[base + CURR_DIR_Y];
3330
+ dirOut[2] = data[base + CURR_DIR_Z];
3331
+ return dirOut;
3332
+ },
3333
+ get color() {
3334
+ colOut[0] = data[base + COL_R];
3335
+ colOut[1] = data[base + COL_G];
3336
+ colOut[2] = data[base + COL_B];
3337
+ return colOut;
3338
+ },
3339
+ get intensity() {
3340
+ return data[base + INTENSITY];
3341
+ },
3342
+ set intensity(v) {
3343
+ data[base + INTENSITY] = v;
3344
+ },
3345
+ get range() {
3346
+ return data[base + RANGE];
3347
+ },
3348
+ set range(v) {
3349
+ data[base + RANGE] = v;
3350
+ },
3351
+ get angle() {
3352
+ return angleArr[slot];
3353
+ },
3354
+ set angle(v) {
3355
+ angleArr[slot] = v;
3356
+ const { innerCos, outerCos } = coneCosines(v, smoothArr[slot]);
3357
+ data[base + INNER_COS] = innerCos;
3358
+ data[base + OUTER_COS] = outerCos;
3359
+ },
3360
+ get smoothness() {
3361
+ return smoothArr[slot];
3362
+ },
3363
+ set smoothness(v) {
3364
+ const s = v < 0 ? 0 : v > 1 ? 1 : v;
3365
+ smoothArr[slot] = s;
3366
+ const { innerCos, outerCos } = coneCosines(angleArr[slot], s);
3367
+ data[base + INNER_COS] = innerCos;
3368
+ data[base + OUTER_COS] = outerCos;
3369
+ },
3370
+ get enabled() {
3371
+ return enabledArr[slot] === 1;
3372
+ },
3373
+ set enabled(v) {
3374
+ enabledArr[slot] = v ? 1 : 0;
3375
+ },
3376
+ destroy() {
3377
+ if (destroyed)
3378
+ return;
3379
+ destroyed = true;
3380
+ data.fill(0, base, base + LIGHT_FLOATS);
3381
+ handles[slot] = null;
3382
+ slots.remove(slot);
3383
+ }
3384
+ };
3385
+ this.handles[slot] = handle;
3386
+ return handle;
3387
+ }
3388
+ /**
3389
+ * Set the global directional light (the "sun"). `direction` points from the
3390
+ * surface toward the light. Defaults to `(0.3, 0.8, 0.5)`, white, intensity 1.
3391
+ */
3392
+ setDirectional(direction, color = [1, 1, 1], intensity = 1) {
3393
+ this.dirDir = [direction[0], direction[1], direction[2]];
3394
+ this.dirColor = [color[0], color[1], color[2]];
3395
+ this.dirIntensity = intensity;
3396
+ }
3397
+ /** Set the global ambient term. Defaults to `(0.3, 0.3, 0.3)`. */
3398
+ setAmbient(color) {
3399
+ this.ambient = [color[0], color[1], color[2]];
3400
+ }
3401
+ /** Number of live dynamic lights. */
3402
+ get count() {
3403
+ return this.slots.size;
3404
+ }
3405
+ /**
3406
+ * Pack enabled lights into a dense run for upload. Disabled lights are
3407
+ * skipped so the shader loop only walks contributing lights. Returns the
3408
+ * shared scratch buffer, the light count, and the byte length to upload
3409
+ * (so the caller never needs the record layout).
3410
+ */
3411
+ pack() {
3412
+ const active = this.slots.activeSlots;
3413
+ const size = this.slots.size;
3414
+ const src = this.data;
3415
+ const dst = this.uploadData;
3416
+ let count = 0;
3417
+ for (let i = 0; i < size; i++) {
3418
+ const slot = active[i];
3419
+ if (this.enabled[slot] === 0)
3420
+ continue;
3421
+ const sBase = slot * LIGHT_FLOATS;
3422
+ dst.set(src.subarray(sBase, sBase + LIGHT_FLOATS), count * LIGHT_FLOATS);
3423
+ count++;
3424
+ }
3425
+ return { data: dst, count, byteLength: count * LIGHT_FLOATS * 4 };
3426
+ }
3427
+ /**
3428
+ * Stamp the directional + ambient terms and the light count into the
3429
+ * renderer's uniform array, starting at `offset` (the float index after the
3430
+ * VP matrix + alpha). Layout: lightDir(3), dirColor(3), dirIntensity(1),
3431
+ * ambient(3), then lightCount as a u32 reinterpret at `offset + 10`.
3432
+ */
3433
+ writeUniforms(uniformData, offset, count) {
3434
+ uniformData[offset + 0] = this.dirDir[0];
3435
+ uniformData[offset + 1] = this.dirDir[1];
3436
+ uniformData[offset + 2] = this.dirDir[2];
3437
+ uniformData[offset + 3] = this.dirColor[0];
3438
+ uniformData[offset + 4] = this.dirColor[1];
3439
+ uniformData[offset + 5] = this.dirColor[2];
3440
+ uniformData[offset + 6] = this.dirIntensity;
3441
+ uniformData[offset + 7] = this.ambient[0];
3442
+ uniformData[offset + 8] = this.ambient[1];
3443
+ uniformData[offset + 9] = this.ambient[2];
3444
+ new Uint32Array(uniformData.buffer)[offset + 10] = count;
3445
+ }
3446
+ /**
3447
+ * Snapshot every live light's current position/direction into its prev
3448
+ * slot. Called from the renderer's `storePreviousState()` in `pre-tick`, so
3449
+ * the shader can `mix(prev, curr, alpha)` and moving lights interpolate at
3450
+ * render rate instead of snapping at the tick boundary.
3451
+ */
3452
+ storePrevious() {
3453
+ const active = this.slots.activeSlots;
3454
+ const size = this.slots.size;
3455
+ const data = this.data;
3456
+ for (let i = 0; i < size; i++) {
3457
+ const base = active[i] * LIGHT_FLOATS;
3458
+ data[base + PREV_POS_X] = data[base + CURR_POS_X];
3459
+ data[base + PREV_POS_Y] = data[base + CURR_POS_Y];
3460
+ data[base + PREV_POS_Z] = data[base + CURR_POS_Z];
3461
+ data[base + PREV_DIR_X] = data[base + CURR_DIR_X];
3462
+ data[base + PREV_DIR_Y] = data[base + CURR_DIR_Y];
3463
+ data[base + PREV_DIR_Z] = data[base + CURR_DIR_Z];
3464
+ }
3465
+ }
3466
+ /** Write a light spec into its CPU slot. Prev is seeded to curr (no spawn lerp). */
3467
+ writeSlot(slot, spec) {
3468
+ const base = slot * LIGHT_FLOATS;
3469
+ const data = this.data;
3470
+ const color = spec.color ?? [1, 1, 1];
3471
+ const [px, py, pz] = spec.position;
3472
+ data[base + KIND] = spec.type === "spot" ? LIGHT_KIND_SPOT : LIGHT_KIND_POINT;
3473
+ data[base + CURR_POS_X] = px;
3474
+ data[base + PREV_POS_X] = px;
3475
+ data[base + CURR_POS_Y] = py;
3476
+ data[base + PREV_POS_Y] = py;
3477
+ data[base + CURR_POS_Z] = pz;
3478
+ data[base + PREV_POS_Z] = pz;
3479
+ data[base + COL_R] = color[0];
3480
+ data[base + COL_G] = color[1];
3481
+ data[base + COL_B] = color[2];
3482
+ data[base + INTENSITY] = spec.intensity ?? 1;
3483
+ data[base + RANGE] = spec.range ?? 10;
3484
+ data[base + CASTS_SHADOW] = 0;
3485
+ data[base + SHADOW_INDEX] = -1;
3486
+ if (spec.type === "spot") {
3487
+ const [dx, dy, dz] = spec.direction;
3488
+ data[base + CURR_DIR_X] = dx;
3489
+ data[base + PREV_DIR_X] = dx;
3490
+ data[base + CURR_DIR_Y] = dy;
3491
+ data[base + PREV_DIR_Y] = dy;
3492
+ data[base + CURR_DIR_Z] = dz;
3493
+ data[base + PREV_DIR_Z] = dz;
3494
+ const angle = spec.angle ?? 0.5;
3495
+ const smoothness = Math.min(1, Math.max(0, spec.smoothness ?? 0.5));
3496
+ this.angle[slot] = angle;
3497
+ this.smoothness[slot] = smoothness;
3498
+ const { innerCos, outerCos } = coneCosines(angle, smoothness);
3499
+ data[base + INNER_COS] = innerCos;
3500
+ data[base + OUTER_COS] = outerCos;
3501
+ } else {
3502
+ data[base + CURR_DIR_X] = 0;
3503
+ data[base + PREV_DIR_X] = 0;
3504
+ data[base + CURR_DIR_Y] = 0;
3505
+ data[base + PREV_DIR_Y] = 0;
3506
+ data[base + CURR_DIR_Z] = 0;
3507
+ data[base + PREV_DIR_Z] = 0;
3508
+ this.angle[slot] = 0;
3509
+ this.smoothness[slot] = 0;
3510
+ data[base + INNER_COS] = 1;
3511
+ data[base + OUTER_COS] = -1;
3512
+ }
3513
+ }
3514
+ };
3515
+ function coneCosines(angle, smoothness) {
3516
+ const outerCos = Math.cos(angle);
3517
+ const innerCos = Math.cos(angle * (1 - smoothness));
3518
+ return { innerCos, outerCos };
3519
+ }
3520
+
3521
+ // src/camera/camera-3d.ts
3522
+ import { lerp as lerp2 } from "murow/core/lerp";
3523
+ import { Ray3D } from "murow/core/ray";
3524
+ var Camera3D = class {
3525
+ constructor() {
3526
+ this.position = [0, 5, -10];
3527
+ this.target = [0, 0, 0];
3528
+ this.up = [0, 1, 0];
3529
+ this.fov = 60;
3530
+ this.near = 0.1;
3531
+ this.far = 1e3;
3532
+ this.aspect = 1;
3533
+ this.movement = "local";
3534
+ // Previous state for interpolation (stored before each tick)
3535
+ this._prevPosition = [0, 5, -10];
3536
+ this._prevTarget = [0, 0, 0];
3537
+ // Interpolated state used for rendering
3538
+ this._renderPosition = [0, 5, -10];
3539
+ this._renderTarget = [0, 0, 0];
3540
+ this._viewMatrix = new Float32Array(16);
3541
+ this._projMatrix = new Float32Array(16);
3542
+ this._vpMatrix = new Float32Array(16);
3543
+ this._ray = new Ray3D();
3544
+ this._width = 1;
3545
+ this._height = 1;
3546
+ }
3547
+ /**
3548
+ * Store current position/target as previous. Call before each tick.
3549
+ */
3550
+ storePrevious() {
3551
+ this._prevPosition[0] = this.position[0];
3552
+ this._prevPosition[1] = this.position[1];
3553
+ this._prevPosition[2] = this.position[2];
3554
+ this._prevTarget[0] = this.target[0];
3555
+ this._prevTarget[1] = this.target[1];
3556
+ this._prevTarget[2] = this.target[2];
3557
+ }
3558
+ /**
3559
+ * Smoothly move the camera toward a target point.
3560
+ * Call each tick. The camera and its look-at target lerp toward the given position.
3561
+ * @param targetX World X to follow
3562
+ * @param targetY World Y to follow
3563
+ * @param targetZ World Z to follow
3564
+ * @param smoothing 0-1. 1 = snap instantly, 0.1 = lazy follow. Default 1.
3565
+ */
3566
+ follow(targetX, targetY, targetZ, smoothing = 1) {
3567
+ const dx = targetX - this.target[0];
3568
+ const dy = targetY - this.target[1];
3569
+ const dz = targetZ - this.target[2];
3570
+ const mx = dx * smoothing;
3571
+ const my = dy * smoothing;
3572
+ const mz = dz * smoothing;
3573
+ this.target[0] += mx;
3574
+ this.target[1] += my;
3575
+ this.target[2] += mz;
3576
+ this.position[0] += mx;
3577
+ this.position[1] += my;
3578
+ this.position[2] += mz;
3579
+ }
3580
+ /**
3581
+ * Interpolate between previous and current state. Call before rendering.
3582
+ */
3583
+ interpolate(alpha) {
3584
+ this._renderPosition[0] = lerp2(this._prevPosition[0], this.position[0], alpha);
3585
+ this._renderPosition[1] = lerp2(this._prevPosition[1], this.position[1], alpha);
3586
+ this._renderPosition[2] = lerp2(this._prevPosition[2], this.position[2], alpha);
3587
+ this._renderTarget[0] = lerp2(this._prevTarget[0], this.target[0], alpha);
3588
+ this._renderTarget[1] = lerp2(this._prevTarget[1], this.target[1], alpha);
3589
+ this._renderTarget[2] = lerp2(this._prevTarget[2], this.target[2], alpha);
3590
+ }
3591
+ /**
3592
+ * Build the view matrix (lookAt) using interpolated state.
3593
+ */
3594
+ getViewMatrix() {
3595
+ lookAt(this._viewMatrix, this._renderPosition, this._renderTarget, this.up);
3596
+ return this._viewMatrix;
3597
+ }
3598
+ /**
3599
+ * Build the perspective projection matrix.
3600
+ */
3601
+ getProjectionMatrix() {
3602
+ perspective(this._projMatrix, this.fov * (Math.PI / 180), this.aspect, this.near, this.far);
3603
+ return this._projMatrix;
3604
+ }
3605
+ /**
3606
+ * Build the combined view-projection matrix.
3607
+ */
3608
+ getViewProjectionMatrix() {
3609
+ this.getViewMatrix();
3610
+ this.getProjectionMatrix();
3611
+ mat4Multiply(this._vpMatrix, this._projMatrix, this._viewMatrix);
3612
+ return this._vpMatrix;
3613
+ }
3614
+ setAspect(width, height) {
3615
+ this.aspect = width / height;
3616
+ this._width = width;
3617
+ this._height = height;
3618
+ }
3619
+ setPosition(x, y, z) {
3620
+ this.position[0] = x;
3621
+ this.position[1] = y;
3622
+ this.position[2] = z;
3623
+ }
3624
+ /**
3625
+ * Unproject a screen coordinate into a world-space ray.
3626
+ * Requires `setAspect(width, height)` to have been called first.
3627
+ * Returns a pre-allocated Ray3D — copy origin/direction if you need to store it.
3628
+ */
3629
+ screenToRay(screenX, screenY) {
3630
+ this.getViewMatrix();
3631
+ const m = this._viewMatrix;
3632
+ const ndcX = 2 * screenX / this._width - 1;
3633
+ const ndcY = 1 - 2 * screenY / this._height;
3634
+ const t = Math.tan(this.fov * Math.PI / 180 * 0.5);
3635
+ const rightX = m[0], rightY = m[4], rightZ = m[8];
3636
+ const upX = m[1], upY = m[5], upZ = m[9];
3637
+ const fwdX = -m[2], fwdY = -m[6], fwdZ = -m[10];
3638
+ const dx = fwdX + rightX * ndcX * t * this.aspect + upX * ndcY * t;
3639
+ const dy = fwdY + rightY * ndcX * t * this.aspect + upY * ndcY * t;
3640
+ const dz = fwdZ + rightZ * ndcX * t * this.aspect + upZ * ndcY * t;
3641
+ this._ray.set(this.position[0], this.position[1], this.position[2], dx, dy, dz);
3642
+ return this._ray;
3643
+ }
3644
+ setTarget(x, y, z) {
3645
+ this.target[0] = x;
3646
+ this.target[1] = y;
3647
+ this.target[2] = z;
3648
+ }
3649
+ /**
3650
+ * Move the camera. Behaviour is determined by the `movement` property:
3651
+ * - `'local'` — along camera axes, pitch included (free-fly / spectator)
3652
+ * - `'grounded'` — yaw-projected XZ + world Y (FPS)
3653
+ * - `'global'` — world axes directly (isometric / platformer)
3654
+ */
3655
+ move(right, up, forward) {
3656
+ if (this.movement === "grounded")
3657
+ this._moveGrounded(right, up, forward);
3658
+ else if (this.movement === "global")
3659
+ this._moveGlobal(right, up, forward);
3660
+ else
3661
+ this._moveLocal(right, up, forward);
3662
+ }
3663
+ _moveLocal(right, up, forward) {
3664
+ this.getViewMatrix();
3665
+ const m = this._viewMatrix;
3666
+ const dx = m[0] * right + m[1] * up - m[2] * forward;
3667
+ const dy = m[4] * right + m[5] * up - m[6] * forward;
3668
+ const dz = m[8] * right + m[9] * up - m[10] * forward;
3669
+ this.position[0] += dx;
3670
+ this.position[1] += dy;
3671
+ this.position[2] += dz;
3672
+ this.target[0] += dx;
3673
+ this.target[1] += dy;
3674
+ this.target[2] += dz;
3675
+ }
3676
+ _moveGrounded(right, up, forward) {
3677
+ const yaw = Math.atan2(this.target[0] - this.position[0], this.target[2] - this.position[2]);
3678
+ const dx = Math.sin(yaw) * forward + Math.cos(yaw) * right;
3679
+ const dz = Math.cos(yaw) * forward - Math.sin(yaw) * right;
3680
+ this.position[0] += dx;
3681
+ this.position[1] += up;
3682
+ this.position[2] += dz;
3683
+ this.target[0] += dx;
3684
+ this.target[1] += up;
3685
+ this.target[2] += dz;
3686
+ }
3687
+ _moveGlobal(right, up, forward) {
3688
+ this.position[0] += right;
3689
+ this.position[1] += up;
3690
+ this.position[2] += forward;
3691
+ this.target[0] += right;
3692
+ this.target[1] += up;
3693
+ this.target[2] += forward;
3694
+ }
3695
+ /**
3696
+ * Orbit around the target point. Zero allocations.
3697
+ * @param yawDelta Horizontal rotation in radians (positive = rotate right)
3698
+ * @param pitchDelta Vertical rotation in radians (positive = rotate up)
3699
+ */
3700
+ orbit(yawDelta, pitchDelta) {
3701
+ let ox = this.position[0] - this.target[0];
3702
+ let oy = this.position[1] - this.target[1];
3703
+ let oz = this.position[2] - this.target[2];
3704
+ const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
3705
+ let yaw = Math.atan2(ox, oz);
3706
+ let pitch = Math.asin(oy / dist);
3707
+ yaw += yawDelta;
3708
+ pitch += pitchDelta;
3709
+ pitch = Math.max(-Math.PI * 0.49, Math.min(Math.PI * 0.49, pitch));
3710
+ this.position[0] = this.target[0] + Math.sin(yaw) * Math.cos(pitch) * dist;
3711
+ this.position[1] = this.target[1] + Math.sin(pitch) * dist;
3712
+ this.position[2] = this.target[2] + Math.cos(yaw) * Math.cos(pitch) * dist;
3713
+ }
3714
+ /**
3715
+ * Zoom by adjusting distance to target. Zero allocations.
3716
+ * @param delta Positive = zoom in, negative = zoom out
3717
+ */
3718
+ zoom(delta) {
3719
+ let ox = this.position[0] - this.target[0];
3720
+ let oy = this.position[1] - this.target[1];
3721
+ let oz = this.position[2] - this.target[2];
3722
+ const dist = Math.sqrt(ox * ox + oy * oy + oz * oz);
3723
+ const newDist = Math.max(0.1, dist - delta);
3724
+ const scale = newDist / dist;
3725
+ this.position[0] = this.target[0] + ox * scale;
3726
+ this.position[1] = this.target[1] + oy * scale;
3727
+ this.position[2] = this.target[2] + oz * scale;
3728
+ }
3729
+ };
3730
+ function lookAt(out, eye, center, up) {
3731
+ let fx = center[0] - eye[0];
3732
+ let fy = center[1] - eye[1];
3733
+ let fz = center[2] - eye[2];
3734
+ let len = 1 / Math.sqrt(fx * fx + fy * fy + fz * fz);
3735
+ fx *= len;
3736
+ fy *= len;
3737
+ fz *= len;
3738
+ let sx = fy * up[2] - fz * up[1];
3739
+ let sy = fz * up[0] - fx * up[2];
3740
+ let sz = fx * up[1] - fy * up[0];
3741
+ len = Math.sqrt(sx * sx + sy * sy + sz * sz);
3742
+ if (len > 0) {
3743
+ len = 1 / len;
3744
+ sx *= len;
3745
+ sy *= len;
3746
+ sz *= len;
3747
+ }
3748
+ const ux = sy * fz - sz * fy;
3749
+ const uy = sz * fx - sx * fz;
3750
+ const uz = sx * fy - sy * fx;
3751
+ out[0] = sx;
3752
+ out[1] = ux;
3753
+ out[2] = -fx;
3754
+ out[3] = 0;
3755
+ out[4] = sy;
3756
+ out[5] = uy;
3757
+ out[6] = -fy;
3758
+ out[7] = 0;
3759
+ out[8] = sz;
3760
+ out[9] = uz;
3761
+ out[10] = -fz;
3762
+ out[11] = 0;
3763
+ out[12] = -(sx * eye[0] + sy * eye[1] + sz * eye[2]);
3764
+ out[13] = -(ux * eye[0] + uy * eye[1] + uz * eye[2]);
3765
+ out[14] = fx * eye[0] + fy * eye[1] + fz * eye[2];
3766
+ out[15] = 1;
3767
+ }
3768
+ function perspective(out, fovRad, aspect, near, far) {
3769
+ const f = 1 / Math.tan(fovRad * 0.5);
3770
+ const rangeInv = 1 / (near - far);
3771
+ out[0] = f / aspect;
3772
+ out[1] = 0;
3773
+ out[2] = 0;
3774
+ out[3] = 0;
3775
+ out[4] = 0;
3776
+ out[5] = f;
3777
+ out[6] = 0;
3778
+ out[7] = 0;
3779
+ out[8] = 0;
3780
+ out[9] = 0;
3781
+ out[10] = (near + far) * rangeInv;
3782
+ out[11] = -1;
3783
+ out[12] = 0;
3784
+ out[13] = 0;
3785
+ out[14] = 2 * near * far * rangeInv;
3786
+ out[15] = 0;
3787
+ }
3788
+ function mat4Multiply(out, a, b) {
3789
+ for (let i = 0; i < 4; i++) {
3790
+ const ai0 = a[i], ai1 = a[i + 4], ai2 = a[i + 8], ai3 = a[i + 12];
3791
+ out[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3];
3792
+ out[i + 4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7];
3793
+ out[i + 8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11];
3794
+ out[i + 12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15];
3795
+ }
3796
+ }
3148
3797
 
3149
3798
  // src/3d/renderer.ts
3150
3799
  import {
@@ -3153,6 +3802,7 @@ import {
3153
3802
  parseGltf,
3154
3803
  SkeletalAnimation
3155
3804
  } from "murow/renderer";
3805
+ import { testHitbox3D } from "murow/core/hitbox";
3156
3806
 
3157
3807
  // src/3d/skeletal-animation-compute/packer.ts
3158
3808
  function packAnimationData(packed) {
@@ -3352,8 +4002,8 @@ function buildAnimationKernel(root, packed, maxInstances, maxTotalBones, budgets
3352
4002
  let by = animF32[offB + 1];
3353
4003
  let bz = animF32[offB + 2];
3354
4004
  let bw = animF32[offB + 3];
3355
- const dot2 = ax * bx + ay * by + az * bz + aw * bw;
3356
- if (dot2 < 0) {
4005
+ const dot3 = ax * bx + ay * by + az * bz + aw * bw;
4006
+ if (dot3 < 0) {
3357
4007
  bx = -bx;
3358
4008
  by = -by;
3359
4009
  bz = -bz;
@@ -3628,6 +4278,330 @@ var GltfClipResyncCoordinator = class {
3628
4278
  }
3629
4279
  };
3630
4280
 
4281
+ // src/3d/raycast.ts
4282
+ import { HitBuffer as HitBuffer2 } from "murow/core/raycast";
4283
+ import {
4284
+ Raycast as Raycast2,
4285
+ RaycastMemo as RaycastMemo2
4286
+ } from "murow/renderer";
4287
+ var WebGPURaycast3D = class extends Raycast2 {
4288
+ constructor(renderer) {
4289
+ super();
4290
+ this.renderer = renderer;
4291
+ this.state = new HitBuffer2(3);
4292
+ this.resultBuffer = [];
4293
+ this.memos = /* @__PURE__ */ new Set();
4294
+ }
4295
+ update(input) {
4296
+ this.state.reset();
4297
+ this.renderer._collectRaycastHitsInto(input.mouse.position.x, input.mouse.position.y, this.state);
4298
+ for (const m of this.memos)
4299
+ m._invalidate();
4300
+ }
4301
+ /**
4302
+ * Nearest hit, or null. The returned object is pool-backed and valid
4303
+ * only until the next `update()` -- copy what you need, or use `memo`
4304
+ * for results that persist across frames.
4305
+ */
4306
+ hit(opts) {
4307
+ return this.state.nearest(opts?.filter, opts?.maxDistance ?? Infinity);
4308
+ }
4309
+ /**
4310
+ * All hits, nearest first. The array and its entries are reused across
4311
+ * calls and overwritten by the next `update()`; do not retain them.
4312
+ */
4313
+ hitAll(opts) {
4314
+ this.state.collectInto(this.resultBuffer, opts?.filter, opts?.maxDistance ?? Infinity);
4315
+ return this.resultBuffer;
4316
+ }
4317
+ memo(opts) {
4318
+ const m = new WebGPURaycastMemo3D(this.state, opts, () => this.memos.delete(m));
4319
+ this.memos.add(m);
4320
+ return m;
4321
+ }
4322
+ clearMemos() {
4323
+ for (const m of this.memos)
4324
+ m._detach();
4325
+ this.memos.clear();
4326
+ }
4327
+ };
4328
+ var WebGPURaycastMemo3D = class extends RaycastMemo2 {
4329
+ constructor(state, opts, onDispose) {
4330
+ super();
4331
+ this.state = state;
4332
+ this.opts = opts;
4333
+ this.onDispose = onDispose;
4334
+ this.dirty = true;
4335
+ this.detached = false;
4336
+ this.cached = [];
4337
+ }
4338
+ get hits() {
4339
+ if (this.detached)
4340
+ return this.cached;
4341
+ if (this.dirty) {
4342
+ this.state.collectInto(this.cached, this.opts.filter, this.opts.maxDistance ?? Infinity);
4343
+ this.dirty = false;
4344
+ }
4345
+ return this.cached;
4346
+ }
4347
+ get first() {
4348
+ const arr = this.hits;
4349
+ return arr.length > 0 ? arr[0] : null;
4350
+ }
4351
+ dispose() {
4352
+ if (this.detached)
4353
+ return;
4354
+ this.onDispose();
4355
+ this._detach();
4356
+ }
4357
+ _invalidate() {
4358
+ this.dirty = true;
4359
+ }
4360
+ _detach() {
4361
+ this.detached = true;
4362
+ this.cached.length = 0;
4363
+ }
4364
+ };
4365
+
4366
+ // src/3d/hitbox.ts
4367
+ import { placePart3D } from "murow/core/hitbox";
4368
+ function buildUnitSphereWireframe(segments = 16) {
4369
+ const out = [];
4370
+ const step2 = Math.PI * 2 / segments;
4371
+ for (let axis = 0; axis < 3; axis++) {
4372
+ for (let i = 0; i < segments; i++) {
4373
+ const a = i * step2;
4374
+ const b = (i + 1) * step2;
4375
+ const ca = Math.cos(a), sa = Math.sin(a);
4376
+ const cb = Math.cos(b), sb = Math.sin(b);
4377
+ if (axis === 0) {
4378
+ out.push(0, ca, sa, 0, cb, sb);
4379
+ } else if (axis === 1) {
4380
+ out.push(ca, 0, sa, cb, 0, sb);
4381
+ } else {
4382
+ out.push(ca, sa, 0, cb, sb, 0);
4383
+ }
4384
+ }
4385
+ }
4386
+ return new Float32Array(out);
4387
+ }
4388
+ function buildUnitBoxWireframe() {
4389
+ const h = 0.5;
4390
+ const corners = [
4391
+ [-h, -h, -h],
4392
+ [h, -h, -h],
4393
+ [h, h, -h],
4394
+ [-h, h, -h],
4395
+ [-h, -h, h],
4396
+ [h, -h, h],
4397
+ [h, h, h],
4398
+ [-h, h, h]
4399
+ ];
4400
+ const edges = [
4401
+ [0, 1],
4402
+ [1, 2],
4403
+ [2, 3],
4404
+ [3, 0],
4405
+ [4, 5],
4406
+ [5, 6],
4407
+ [6, 7],
4408
+ [7, 4],
4409
+ [0, 4],
4410
+ [1, 5],
4411
+ [2, 6],
4412
+ [3, 7]
4413
+ ];
4414
+ const out = [];
4415
+ for (const [a, b] of edges) {
4416
+ out.push(...corners[a], ...corners[b]);
4417
+ }
4418
+ return new Float32Array(out);
4419
+ }
4420
+ function buildUnitCylinderWireframe(segments = 24) {
4421
+ const h = 0.5;
4422
+ const step2 = Math.PI * 2 / segments;
4423
+ const out = [];
4424
+ for (let i = 0; i < segments; i++) {
4425
+ const a = i * step2, b = (i + 1) * step2;
4426
+ const ca = Math.cos(a), sa = Math.sin(a);
4427
+ const cb = Math.cos(b), sb = Math.sin(b);
4428
+ out.push(ca, -h, sa, cb, -h, sb);
4429
+ out.push(ca, h, sa, cb, h, sb);
4430
+ }
4431
+ for (let i = 0; i < 4; i++) {
4432
+ const a = i * (Math.PI * 0.5);
4433
+ const ca = Math.cos(a), sa = Math.sin(a);
4434
+ out.push(ca, -h, sa, ca, h, sa);
4435
+ }
4436
+ return new Float32Array(out);
4437
+ }
4438
+ var IDLE = [1, 0, 1, 1];
4439
+ var HOVERED = [0.2, 1, 0.4, 1];
4440
+ var UNIFORM_STRIDE = 256;
4441
+ var MIN_BINDING_SIZE = 80;
4442
+ var CAPACITY = 4096;
4443
+ var HitboxDebugRenderer = class {
4444
+ constructor() {
4445
+ this.device = null;
4446
+ this.pipeline = null;
4447
+ this.bindGroup = null;
4448
+ this.uniformBuffer = null;
4449
+ this.sphereBuffer = null;
4450
+ this.sphereVertexCount = 0;
4451
+ this.boxBuffer = null;
4452
+ this.boxVertexCount = 0;
4453
+ this.cylinderBuffer = null;
4454
+ this.cylinderVertexCount = 0;
4455
+ this.stage = new Float32Array(0);
4456
+ this.entries = [];
4457
+ this.vp = new Float32Array(16);
4458
+ }
4459
+ init(device, format) {
4460
+ this.device = device;
4461
+ const upload = (data) => {
4462
+ const buf = device.createBuffer({
4463
+ size: data.byteLength,
4464
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4465
+ });
4466
+ device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
4467
+ return buf;
4468
+ };
4469
+ const sphereData = buildUnitSphereWireframe();
4470
+ const boxData = buildUnitBoxWireframe();
4471
+ const cylinderData = buildUnitCylinderWireframe();
4472
+ this.sphereBuffer = upload(sphereData);
4473
+ this.sphereVertexCount = sphereData.length / 3;
4474
+ this.boxBuffer = upload(boxData);
4475
+ this.boxVertexCount = boxData.length / 3;
4476
+ this.cylinderBuffer = upload(cylinderData);
4477
+ this.cylinderVertexCount = cylinderData.length / 3;
4478
+ this.uniformBuffer = device.createBuffer({
4479
+ size: UNIFORM_STRIDE * CAPACITY,
4480
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
4481
+ });
4482
+ this.stage = new Float32Array(UNIFORM_STRIDE * CAPACITY / 4);
4483
+ const bindGroupLayout = device.createBindGroupLayout({
4484
+ entries: [{
4485
+ binding: 0,
4486
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4487
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: MIN_BINDING_SIZE }
4488
+ }]
4489
+ });
4490
+ this.bindGroup = device.createBindGroup({
4491
+ layout: bindGroupLayout,
4492
+ entries: [{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: MIN_BINDING_SIZE } }]
4493
+ });
4494
+ const shaderModule = device.createShaderModule({
4495
+ code: `
4496
+ struct Uniforms {
4497
+ mvp: mat4x4<f32>,
4498
+ color: vec4<f32>,
4499
+ };
4500
+ @group(0) @binding(0) var<uniform> u: Uniforms;
4501
+ @vertex
4502
+ fn vs(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
4503
+ return u.mvp * vec4<f32>(p, 1.0);
4504
+ }
4505
+ @fragment
4506
+ fn fs() -> @location(0) vec4<f32> {
4507
+ return u.color;
4508
+ }
4509
+ `
4510
+ });
4511
+ this.pipeline = device.createRenderPipeline({
4512
+ layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
4513
+ vertex: {
4514
+ module: shaderModule,
4515
+ entryPoint: "vs",
4516
+ buffers: [{
4517
+ arrayStride: 12,
4518
+ attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }]
4519
+ }]
4520
+ },
4521
+ fragment: { module: shaderModule, entryPoint: "fs", targets: [{ format }] },
4522
+ primitive: { topology: "line-list" },
4523
+ depthStencil: {
4524
+ format: "depth24plus",
4525
+ depthWriteEnabled: false,
4526
+ depthCompare: "less-equal"
4527
+ }
4528
+ });
4529
+ }
4530
+ begin(vp) {
4531
+ this.entries.length = 0;
4532
+ this.vp = vp;
4533
+ }
4534
+ emit(hb, hovered, px, py, pz, sx, sy, sz) {
4535
+ const p = placePart3D(hb, px, py, pz, sx, sy, sz);
4536
+ const color = hovered ? HOVERED : IDLE;
4537
+ if (hb.shape === "sphere") {
4538
+ this.collect(this.sphereBuffer, this.sphereVertexCount, p.cx, p.cy, p.cz, p.hx, p.hx, p.hx, color);
4539
+ } else if (hb.shape === "box") {
4540
+ this.collect(this.boxBuffer, this.boxVertexCount, p.cx, p.cy, p.cz, p.hx * 2, p.hy * 2, p.hz * 2, color);
4541
+ } else {
4542
+ this.collect(this.cylinderBuffer, this.cylinderVertexCount, p.cx, p.cy, p.cz, p.hx, p.hy * 2, p.hz, color);
4543
+ }
4544
+ }
4545
+ flush(pass) {
4546
+ const { pipeline, bindGroup, uniformBuffer, entries } = this;
4547
+ if (!pipeline || !bindGroup || !uniformBuffer || entries.length === 0)
4548
+ return;
4549
+ this.device.queue.writeBuffer(
4550
+ uniformBuffer,
4551
+ 0,
4552
+ this.stage.buffer,
4553
+ 0,
4554
+ entries.length * UNIFORM_STRIDE
4555
+ );
4556
+ pass.setPipeline(pipeline);
4557
+ let currentVbo = null;
4558
+ for (const e of entries) {
4559
+ if (e.vbo !== currentVbo) {
4560
+ pass.setVertexBuffer(0, e.vbo);
4561
+ currentVbo = e.vbo;
4562
+ }
4563
+ pass.setBindGroup(0, bindGroup, [e.offset]);
4564
+ pass.draw(e.vertexCount, 1, 0, 0);
4565
+ }
4566
+ }
4567
+ /**
4568
+ * The model matrix is pure scale-then-translate, so `MVP = VP * M`
4569
+ * collapses to scaling VP's first three columns by the extents and
4570
+ * replacing the fourth with `VP * (center, 1)` -- no matrix multiply.
4571
+ */
4572
+ collect(vbo, vertexCount, cx, cy, cz, ex, ey, ez, color) {
4573
+ if (!vbo || vertexCount === 0)
4574
+ return;
4575
+ if (this.entries.length >= CAPACITY)
4576
+ return;
4577
+ const idx = this.entries.length;
4578
+ const base = idx * UNIFORM_STRIDE >>> 2;
4579
+ const f324 = this.stage;
4580
+ const vp = this.vp;
4581
+ f324[base + 0] = vp[0] * ex;
4582
+ f324[base + 1] = vp[1] * ex;
4583
+ f324[base + 2] = vp[2] * ex;
4584
+ f324[base + 3] = vp[3] * ex;
4585
+ f324[base + 4] = vp[4] * ey;
4586
+ f324[base + 5] = vp[5] * ey;
4587
+ f324[base + 6] = vp[6] * ey;
4588
+ f324[base + 7] = vp[7] * ey;
4589
+ f324[base + 8] = vp[8] * ez;
4590
+ f324[base + 9] = vp[9] * ez;
4591
+ f324[base + 10] = vp[10] * ez;
4592
+ f324[base + 11] = vp[11] * ez;
4593
+ f324[base + 12] = vp[0] * cx + vp[4] * cy + vp[8] * cz + vp[12];
4594
+ f324[base + 13] = vp[1] * cx + vp[5] * cy + vp[9] * cz + vp[13];
4595
+ f324[base + 14] = vp[2] * cx + vp[6] * cy + vp[10] * cz + vp[14];
4596
+ f324[base + 15] = vp[3] * cx + vp[7] * cy + vp[11] * cz + vp[15];
4597
+ f324[base + 16] = color[0];
4598
+ f324[base + 17] = color[1];
4599
+ f324[base + 18] = color[2];
4600
+ f324[base + 19] = color[3];
4601
+ this.entries.push({ vbo, vertexCount, offset: idx * UNIFORM_STRIDE });
4602
+ }
4603
+ };
4604
+
3631
4605
  // src/3d/renderer.ts
3632
4606
  var DYN_PREV_PX = 0;
3633
4607
  var DYN_PREV_PY = 1;
@@ -3697,6 +4671,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3697
4671
  this.resizeCallbacks = [];
3698
4672
  // layer=0, sheetId=modelId
3699
4673
  this.staticDirty = false;
4674
+ this.nextInstanceId = 0;
3700
4675
  // Models (vertex + index buffers)
3701
4676
  this.models = [];
3702
4677
  this.nextModelId = 0;
@@ -3722,10 +4697,15 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3722
4697
  this.freedBoneOffsets = /* @__PURE__ */ new Map();
3723
4698
  // Frustum planes (6 planes × 4 floats each), extracted from VP matrix
3724
4699
  this.frustumPlanes = new Float32Array(24);
3725
- this.uniformData = new Float32Array(24);
3726
- // mat4x4 (16) + alpha (1) + lightDir (3) + padding (4)
4700
+ // Dynamic lights — CPU state (SoA, slots, globals) lives in LightSystem;
4701
+ // the renderer owns only the GPU buffer it packs into each frame.
4702
+ this.lights = new LightSystem(MAX_LIGHTS);
4703
+ this.uniformData = new Float32Array(MESH_UNIFORM_FLOATS);
3727
4704
  this.lastRenderTime = 0;
4705
+ this.debug = { hitboxes: false };
4706
+ this.hitboxDebug = new HitboxDebugRenderer();
3728
4707
  this.camera = new Camera3D();
4708
+ this.raycast = new WebGPURaycast3D(this);
3729
4709
  this._prefabs = options.prefabs ?? null;
3730
4710
  const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
3731
4711
  const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
@@ -3737,14 +4717,15 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3737
4717
  this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
3738
4718
  this.boneOffsetRefcount = new Uint32Array(this.maxTotalBones);
3739
4719
  this.boneOffsetSkinIndex = new Uint32Array(this.maxTotalBones);
3740
- this.freeList = new FreeList3(resolvedMaxInstances);
4720
+ this.freeList = new FreeList2(resolvedMaxInstances);
3741
4721
  this.batcher = new SparseBatcher2(resolvedMaxInstances);
3742
4722
  this.dynamicData = new Float32Array(resolvedMaxInstances * DYNAMIC_MESH_FLOATS);
3743
4723
  this.staticData = new Float32Array(resolvedMaxInstances * STATIC_MESH_FLOATS);
3744
4724
  this.slotIndexData = new Uint32Array(resolvedMaxInstances);
3745
4725
  this.instanceModelIds = new Uint8Array(resolvedMaxInstances);
4726
+ this.instanceHandles = new Array(resolvedMaxInstances).fill(null);
3746
4727
  const msi = this.maxSkinnedInstances;
3747
- this.skinnedFreeList = new FreeList3(msi);
4728
+ this.skinnedFreeList = new FreeList2(msi);
3748
4729
  this.skinnedBatcher = new SparseBatcher2(msi);
3749
4730
  this.skinnedDynamicData = new Float32Array(msi * DYNAMIC_MESH_FLOATS);
3750
4731
  this.skinnedStaticData = new Float32Array(msi * SKINNED_STATIC_MESH_FLOATS);
@@ -3753,6 +4734,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3753
4734
  this.skinnedInstanceModelIds = new Uint8Array(msi);
3754
4735
  this.skinnedInstanceBoneOffsets = new Uint32Array(msi);
3755
4736
  this.skinnedAnimStates = new Array(msi).fill(null);
4737
+ this.skinnedInstanceHandles = new Array(msi).fill(null);
3756
4738
  this.boneMatrixData = new Float32Array(this.maxTotalBones * 16);
3757
4739
  const instBufSize = msi * 8;
3758
4740
  this.gpuInstData = new Float32Array(instBufSize);
@@ -3771,7 +4753,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3771
4753
  maxComputeInvocationsPerWorkgroup: a.maxComputeInvocationsPerWorkgroup
3772
4754
  };
3773
4755
  const device = await adapter.requestDevice({ requiredLimits });
3774
- this.root = tgpu6.initFromDevice({ device });
4756
+ this.root = tgpu7.initFromDevice({ device });
3775
4757
  this.device = this.root.device;
3776
4758
  this.context = this.canvas.getContext("webgpu");
3777
4759
  this.format = navigator.gpu.getPreferredCanvasFormat();
@@ -3782,7 +4764,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3782
4764
  });
3783
4765
  this._width = this.canvas.width;
3784
4766
  this._height = this.canvas.height;
3785
- this.camera.aspect = this._width / this._height;
4767
+ this.camera.setAspect(this.canvas.clientWidth || this._width, this.canvas.clientHeight || this._height);
3786
4768
  this.depthTexture = this.device.createTexture({
3787
4769
  size: [this._width, this._height],
3788
4770
  format: "depth24plus",
@@ -3812,7 +4794,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3812
4794
  };
3813
4795
  const vertex = createMeshVertex(this.meshLayout);
3814
4796
  const fragment = createMeshFragment(this.meshLayout);
3815
- const { code: wgslCode } = tgpu6.resolveWithContext([vertex, fragment]);
4797
+ const { code: wgslCode } = tgpu7.resolveWithContext([vertex, fragment]);
3816
4798
  const shaderModule = this.device.createShaderModule({ code: wgslCode });
3817
4799
  const rawBGL = this.root.unwrap(this.meshLayout);
3818
4800
  this.rawPipeline = this.device.createRenderPipeline({
@@ -3825,7 +4807,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3825
4807
  const texLayout = createTextureBindGroupLayout();
3826
4808
  const texVertex = createTexturedMeshVertex(this.meshLayout);
3827
4809
  const texFragment = createTexturedMeshFragment(this.meshLayout, texLayout);
3828
- const { code: texWgslCode } = tgpu6.resolveWithContext([texVertex, texFragment]);
4810
+ const { code: texWgslCode } = tgpu7.resolveWithContext([texVertex, texFragment]);
3829
4811
  const texShaderModule = this.device.createShaderModule({ code: texWgslCode });
3830
4812
  const rawTexBGL = this.root.unwrap(texLayout);
3831
4813
  this.rawTexturedPipeline = this.device.createRenderPipeline({
@@ -3839,17 +4821,20 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3839
4821
  this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxInstances)).$usage("storage");
3840
4822
  this.uniformBuffer = this.root.createBuffer(MeshUniforms).$usage("uniform");
3841
4823
  this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxInstances)).$usage("storage");
4824
+ this.lightBuffer = this.root.createBuffer(d.arrayOf(Light, MAX_LIGHTS)).$usage("storage");
3842
4825
  this.rawDynamicBuffer = this.root.unwrap(this.dynamicBuffer);
3843
4826
  this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
3844
4827
  this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
3845
4828
  this.rawSlotIndexBuffer = this.root.unwrap(this.slotIndexBuffer);
4829
+ this.rawLightBuffer = this.root.unwrap(this.lightBuffer);
3846
4830
  this.rawBindGroup = this.device.createBindGroup({
3847
4831
  layout: rawBGL,
3848
4832
  entries: [
3849
4833
  { binding: 0, resource: { buffer: this.rawUniformBuffer } },
3850
4834
  { binding: 1, resource: { buffer: this.rawDynamicBuffer } },
3851
4835
  { binding: 2, resource: { buffer: this.rawStaticBuffer } },
3852
- { binding: 3, resource: { buffer: this.rawSlotIndexBuffer } }
4836
+ { binding: 3, resource: { buffer: this.rawSlotIndexBuffer } },
4837
+ { binding: 4, resource: { buffer: this.rawLightBuffer } }
3853
4838
  ]
3854
4839
  });
3855
4840
  const msi = this.maxSkinnedInstances;
@@ -3871,8 +4856,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3871
4856
  ]
3872
4857
  };
3873
4858
  const skinnedVertex = createSkinnedMeshVertex(this.skinnedMeshLayout);
3874
- const skinnedFragment = createMeshFragment(this.skinnedMeshLayout);
3875
- const { code: skinnedWgsl } = tgpu6.resolveWithContext([skinnedVertex, skinnedFragment]);
4859
+ const skinnedFragment = createSkinnedMeshFragment(this.skinnedMeshLayout);
4860
+ const { code: skinnedWgsl } = tgpu7.resolveWithContext([skinnedVertex, skinnedFragment]);
3876
4861
  const skinnedShaderModule = this.device.createShaderModule({ code: skinnedWgsl });
3877
4862
  const rawSkinnedBGL = this.root.unwrap(this.skinnedMeshLayout);
3878
4863
  this.rawSkinnedPipeline = this.device.createRenderPipeline({
@@ -3884,7 +4869,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3884
4869
  });
3885
4870
  const skinnedTexVertex = createSkinnedMeshVertex(this.skinnedMeshLayout);
3886
4871
  const skinnedTexFragment = createTexturedMeshFragment(this.skinnedMeshLayout, texLayout);
3887
- const { code: skinnedTexWgsl } = tgpu6.resolveWithContext([skinnedTexVertex, skinnedTexFragment]);
4872
+ const { code: skinnedTexWgsl } = tgpu7.resolveWithContext([skinnedTexVertex, skinnedTexFragment]);
3888
4873
  const skinnedTexShaderModule = this.device.createShaderModule({ code: skinnedTexWgsl });
3889
4874
  this.rawSkinnedTexturedPipeline = this.device.createRenderPipeline({
3890
4875
  layout: this.device.createPipelineLayout({ bindGroupLayouts: [rawSkinnedBGL, rawTexBGL] }),
@@ -3908,12 +4893,14 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3908
4893
  { binding: 1, resource: { buffer: this.rawSkinnedDynamicBuffer } },
3909
4894
  { binding: 2, resource: { buffer: this.rawSkinnedStaticBuffer } },
3910
4895
  { binding: 3, resource: { buffer: this.rawSkinnedSlotIndexBuffer } },
3911
- { binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } }
4896
+ { binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } },
4897
+ { binding: 5, resource: { buffer: this.rawLightBuffer } }
3912
4898
  ]
3913
4899
  });
3914
4900
  if (this._prefabs) {
3915
4901
  this.uploadPrefabBucket(this._prefabs);
3916
4902
  }
4903
+ this.hitboxDebug.init(this.device, this.format);
3917
4904
  this.setupResizeObserver();
3918
4905
  this._initialized = true;
3919
4906
  }
@@ -3983,7 +4970,10 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3983
4970
  alphaMode: "premultiplied"
3984
4971
  });
3985
4972
  }
3986
- this.camera.aspect = w / h;
4973
+ const cssBox = entry.contentBoxSize?.[0];
4974
+ const cssW = cssBox ? cssBox.inlineSize : w;
4975
+ const cssH = cssBox ? cssBox.blockSize : h;
4976
+ this.camera.setAspect(cssW, cssH);
3987
4977
  this.depthTexture.destroy();
3988
4978
  this.depthTexture = this.device.createTexture({
3989
4979
  size: [w, h],
@@ -4037,6 +5027,33 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4037
5027
  get maxSkinned() {
4038
5028
  return this.maxSkinnedInstances;
4039
5029
  }
5030
+ /**
5031
+ * Add a dynamic point or spot light. Returns a live handle whose position,
5032
+ * color, intensity, range, and enabled state can all be changed every frame.
5033
+ * Up to `MAX_LIGHTS` (64) lights may be live at once; throws past that.
5034
+ *
5035
+ * The global directional + ambient terms are separate — see
5036
+ * `setDirectionalLight` / `setAmbient`.
5037
+ */
5038
+ addLight(spec) {
5039
+ return this.lights.add(spec);
5040
+ }
5041
+ /**
5042
+ * Set the global directional light (the "sun"). `direction` points from the
5043
+ * surface toward the light. Defaults to `(0.3, 0.8, 0.5)`, white, intensity 1
5044
+ * — the engine's classic fixed look.
5045
+ */
5046
+ setDirectionalLight(direction, color = [1, 1, 1], intensity = 1) {
5047
+ this.lights.setDirectional(direction, color, intensity);
5048
+ }
5049
+ /** Set the global ambient term. Defaults to `(0.3, 0.3, 0.3)`. */
5050
+ setAmbient(color) {
5051
+ this.lights.setAmbient(color);
5052
+ }
5053
+ /** Number of live dynamic lights. */
5054
+ get lightCount() {
5055
+ return this.lights.count;
5056
+ }
4040
5057
  /**
4041
5058
  * Create a flat grid mesh on the XZ plane at Y=0.
4042
5059
  *
@@ -4327,13 +5344,30 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4327
5344
  const uvs = data.uvs ?? new Float32Array(vertexCount * 2);
4328
5345
  const hasTexture = !!texture;
4329
5346
  let maxRadiusSq = 0;
5347
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
5348
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
4330
5349
  for (let i = 0; i < vertexCount; i++) {
4331
5350
  const px = positions[i * 3], py = positions[i * 3 + 1], pz = positions[i * 3 + 2];
4332
5351
  const rSq = px * px + py * py + pz * pz;
4333
5352
  if (rSq > maxRadiusSq)
4334
5353
  maxRadiusSq = rSq;
5354
+ if (px < minX)
5355
+ minX = px;
5356
+ if (px > maxX)
5357
+ maxX = px;
5358
+ if (py < minY)
5359
+ minY = py;
5360
+ if (py > maxY)
5361
+ maxY = py;
5362
+ if (pz < minZ)
5363
+ minZ = pz;
5364
+ if (pz > maxZ)
5365
+ maxZ = pz;
4335
5366
  }
4336
5367
  const boundingRadius = Math.sqrt(maxRadiusSq);
5368
+ const halfX = vertexCount ? (maxX - minX) * 0.5 : 0;
5369
+ const halfY = vertexCount ? (maxY - minY) * 0.5 : 0;
5370
+ const halfZ = vertexCount ? (maxZ - minZ) * 0.5 : 0;
4337
5371
  const interleaved = new Float32Array(vertexCount * 8);
4338
5372
  for (let i = 0; i < vertexCount; i++) {
4339
5373
  const o = i * 8;
@@ -4394,6 +5428,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4394
5428
  indexCount,
4395
5429
  indexFormat: indices instanceof Uint32Array ? "uint32" : "uint16",
4396
5430
  boundingRadius,
5431
+ halfX,
5432
+ halfY,
5433
+ halfZ,
4397
5434
  hasTexture,
4398
5435
  textureBindGroup,
4399
5436
  skinned: false,
@@ -4412,13 +5449,30 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4412
5449
  const uvs = data.uvs ?? new Float32Array(vertexCount * 2);
4413
5450
  const hasTexture = !!texture;
4414
5451
  let maxRadiusSq = 0;
5452
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
5453
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
4415
5454
  for (let i = 0; i < vertexCount; i++) {
4416
5455
  const px = positions[i * 3], py = positions[i * 3 + 1], pz = positions[i * 3 + 2];
4417
5456
  const rSq = px * px + py * py + pz * pz;
4418
5457
  if (rSq > maxRadiusSq)
4419
5458
  maxRadiusSq = rSq;
5459
+ if (px < minX)
5460
+ minX = px;
5461
+ if (px > maxX)
5462
+ maxX = px;
5463
+ if (py < minY)
5464
+ minY = py;
5465
+ if (py > maxY)
5466
+ maxY = py;
5467
+ if (pz < minZ)
5468
+ minZ = pz;
5469
+ if (pz > maxZ)
5470
+ maxZ = pz;
4420
5471
  }
4421
5472
  const boundingRadius = Math.sqrt(maxRadiusSq);
5473
+ const halfX = vertexCount ? (maxX - minX) * 0.5 : 0;
5474
+ const halfY = vertexCount ? (maxY - minY) * 0.5 : 0;
5475
+ const halfZ = vertexCount ? (maxZ - minZ) * 0.5 : 0;
4422
5476
  const buf = new ArrayBuffer(vertexCount * 56);
4423
5477
  const floatView = new Float32Array(buf);
4424
5478
  const u16View = new Uint16Array(buf);
@@ -4490,6 +5544,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4490
5544
  indexCount,
4491
5545
  indexFormat: indices instanceof Uint32Array ? "uint32" : "uint16",
4492
5546
  boundingRadius,
5547
+ halfX,
5548
+ halfY,
5549
+ halfZ,
4493
5550
  hasTexture,
4494
5551
  textureBindGroup,
4495
5552
  skinned: true,
@@ -4622,17 +5679,18 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4622
5679
  * with another instance (e.g., when spawning all parts of a character).
4623
5680
  */
4624
5681
  addInstance(opts) {
5682
+ const userPrefabId = isPrefab3D(opts.model) ? opts.model.id : null;
4625
5683
  if (isPrefab3D(opts.model) && opts.model.type === "composite") {
4626
5684
  return this.addCompositeInstance(opts, opts.model);
4627
5685
  }
4628
5686
  const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
4629
5687
  if ("parts" in modelOrGltf) {
4630
- return this.addGltfInstance(opts, modelOrGltf);
5688
+ return this.addGltfInstance(opts, modelOrGltf, userPrefabId);
4631
5689
  }
4632
5690
  const modelHandle = modelOrGltf;
4633
5691
  const model = this.models[modelHandle.id];
4634
5692
  if (model?.skinned) {
4635
- return this.addSkinnedInstance(opts, modelHandle, model.skinIndex);
5693
+ return this.addSkinnedInstance(opts, modelHandle, model.skinIndex, void 0, userPrefabId);
4636
5694
  }
4637
5695
  const slot = this.freeList.allocate();
4638
5696
  if (slot === -1)
@@ -4665,10 +5723,16 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4665
5723
  const staticData = this.staticData;
4666
5724
  const self = this;
4667
5725
  let destroyed = false;
5726
+ const posOut = [0, 0, 0];
5727
+ const rotOut = [0, 0, 0];
5728
+ const sclOut = [0, 0, 0];
5729
+ const id = ++this.nextInstanceId;
4668
5730
  const handle = {
5731
+ id,
4669
5732
  slot,
4670
5733
  modelId: modelHandle.id,
4671
5734
  skinned: false,
5735
+ prefabId: userPrefabId,
4672
5736
  setPosition(nx, ny, nz) {
4673
5737
  dynamicData[dynBase + DYN_CURR_PX] = nx;
4674
5738
  dynamicData[dynBase + DYN_CURR_PY] = ny;
@@ -4684,6 +5748,32 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4684
5748
  staticData[statBase + STAT_SY] = ny;
4685
5749
  staticData[statBase + STAT_SZ] = nz;
4686
5750
  },
5751
+ teleport(nx, ny, nz) {
5752
+ dynamicData[dynBase + DYN_PREV_PX] = nx;
5753
+ dynamicData[dynBase + DYN_PREV_PY] = ny;
5754
+ dynamicData[dynBase + DYN_PREV_PZ] = nz;
5755
+ dynamicData[dynBase + DYN_CURR_PX] = nx;
5756
+ dynamicData[dynBase + DYN_CURR_PY] = ny;
5757
+ dynamicData[dynBase + DYN_CURR_PZ] = nz;
5758
+ },
5759
+ get position() {
5760
+ posOut[0] = dynamicData[dynBase + DYN_CURR_PX];
5761
+ posOut[1] = dynamicData[dynBase + DYN_CURR_PY];
5762
+ posOut[2] = dynamicData[dynBase + DYN_CURR_PZ];
5763
+ return posOut;
5764
+ },
5765
+ get rotation() {
5766
+ rotOut[0] = dynamicData[dynBase + DYN_CURR_RX];
5767
+ rotOut[1] = dynamicData[dynBase + DYN_CURR_RY];
5768
+ rotOut[2] = dynamicData[dynBase + DYN_CURR_RZ];
5769
+ return rotOut;
5770
+ },
5771
+ get scale() {
5772
+ sclOut[0] = staticData[statBase + STAT_SX];
5773
+ sclOut[1] = staticData[statBase + STAT_SY];
5774
+ sclOut[2] = staticData[statBase + STAT_SZ];
5775
+ return sclOut;
5776
+ },
4687
5777
  destroy() {
4688
5778
  if (destroyed)
4689
5779
  return;
@@ -4692,12 +5782,14 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4692
5782
  self.freeList.free(slot);
4693
5783
  dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4694
5784
  staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
5785
+ self.instanceHandles[slot] = null;
4695
5786
  self.staticDirty = true;
4696
5787
  }
4697
5788
  };
5789
+ this.instanceHandles[slot] = handle;
4698
5790
  return handle;
4699
5791
  }
4700
- addGltfInstance(opts, gltf) {
5792
+ addGltfInstance(opts, gltf, prefabId) {
4701
5793
  const childHandles = [];
4702
5794
  let firstSkinnedSlot;
4703
5795
  for (const part of gltf.parts) {
@@ -4705,7 +5797,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4705
5797
  const model = this.models[part.id];
4706
5798
  let handle;
4707
5799
  if (model?.skinned) {
4708
- handle = this.addSkinnedInstance(partOpts, part, model.skinIndex, firstSkinnedSlot);
5800
+ handle = this.addSkinnedInstance(partOpts, part, model.skinIndex, firstSkinnedSlot, prefabId);
4709
5801
  if (firstSkinnedSlot === void 0)
4710
5802
  firstSkinnedSlot = handle.slot;
4711
5803
  } else {
@@ -4714,8 +5806,11 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4714
5806
  childHandles.push(handle);
4715
5807
  }
4716
5808
  const skinnedHandle = childHandles.find((h) => h.skinned);
5809
+ const lead = childHandles[0];
4717
5810
  return {
5811
+ id: lead.id,
4718
5812
  skinned: gltf.skinned,
5813
+ prefabId,
4719
5814
  setPosition(x, y, z) {
4720
5815
  for (const h of childHandles)
4721
5816
  h.setPosition(x, y, z);
@@ -4728,6 +5823,19 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4728
5823
  for (const h of childHandles)
4729
5824
  h.setScale(x, y, z);
4730
5825
  },
5826
+ teleport(x, y, z) {
5827
+ for (const h of childHandles)
5828
+ h.teleport(x, y, z);
5829
+ },
5830
+ get position() {
5831
+ return lead.position;
5832
+ },
5833
+ get rotation() {
5834
+ return lead.rotation;
5835
+ },
5836
+ get scale() {
5837
+ return lead.scale;
5838
+ },
4731
5839
  play: skinnedHandle?.play ? (name, opts2) => {
4732
5840
  skinnedHandle.play(name, opts2);
4733
5841
  } : void 0,
@@ -4764,6 +5872,17 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4764
5872
  rz: p.offset?.rotation?.[2] ?? 0
4765
5873
  }));
4766
5874
  const childHandles = [];
5875
+ const posOut = [basePos[0], basePos[1], basePos[2]];
5876
+ const rotOut = [baseRot[0], baseRot[1], baseRot[2]];
5877
+ const sclOut = [1, 1, 1];
5878
+ const initialScale = opts.scale;
5879
+ if (typeof initialScale === "number") {
5880
+ sclOut[0] = sclOut[1] = sclOut[2] = initialScale;
5881
+ } else if (initialScale) {
5882
+ sclOut[0] = initialScale[0];
5883
+ sclOut[1] = initialScale[1];
5884
+ sclOut[2] = initialScale[2];
5885
+ }
4767
5886
  for (let i = 0; i < composite.parts.length; i++) {
4768
5887
  const part = composite.parts[i];
4769
5888
  const off = offsets[i];
@@ -4777,30 +5896,59 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4777
5896
  childHandles.push(this.addInstance(partOpts));
4778
5897
  }
4779
5898
  return {
5899
+ id: childHandles[0].id,
4780
5900
  skinned: childHandles.some((h) => h.skinned),
5901
+ prefabId: composite.id,
4781
5902
  setPosition(x, y, z) {
5903
+ posOut[0] = x;
5904
+ posOut[1] = y;
5905
+ posOut[2] = z;
4782
5906
  for (let i = 0; i < childHandles.length; i++) {
4783
5907
  const o = offsets[i];
4784
5908
  childHandles[i].setPosition(x + o.px, y + o.py, z + o.pz);
4785
5909
  }
4786
5910
  },
4787
5911
  setRotation(x, y, z) {
5912
+ rotOut[0] = x;
5913
+ rotOut[1] = y;
5914
+ rotOut[2] = z;
4788
5915
  for (let i = 0; i < childHandles.length; i++) {
4789
5916
  const o = offsets[i];
4790
5917
  childHandles[i].setRotation(x + o.rx, y + o.ry, z + o.rz);
4791
5918
  }
4792
5919
  },
4793
5920
  setScale(x, y, z) {
5921
+ sclOut[0] = x;
5922
+ sclOut[1] = y;
5923
+ sclOut[2] = z;
4794
5924
  for (const h of childHandles)
4795
5925
  h.setScale(x, y, z);
4796
5926
  },
5927
+ teleport(x, y, z) {
5928
+ posOut[0] = x;
5929
+ posOut[1] = y;
5930
+ posOut[2] = z;
5931
+ for (let i = 0; i < childHandles.length; i++) {
5932
+ const o = offsets[i];
5933
+ childHandles[i].teleport(x + o.px, y + o.py, z + o.pz);
5934
+ }
5935
+ },
5936
+ get position() {
5937
+ return posOut;
5938
+ },
5939
+ get rotation() {
5940
+ return rotOut;
5941
+ },
5942
+ get scale() {
5943
+ return sclOut;
5944
+ },
4797
5945
  destroy() {
4798
5946
  for (const h of childHandles)
4799
5947
  h.destroy();
4800
5948
  }
4801
5949
  };
4802
5950
  }
4803
- addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot) {
5951
+ addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot, prefabId = null) {
4804
5952
  const slot = this.skinnedFreeList.allocate();
4805
5953
  if (slot === -1)
4806
5954
  throw new Error(`Max skinned instances (${this.maxSkinnedInstances}) reached`);
@@ -4869,10 +6017,16 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4869
6017
  const capturedBoneOffset = boneOffset;
4870
6018
  const capturedSkinIndex = skinIndex;
4871
6019
  let destroyed = false;
4872
- return {
6020
+ const posOut = [0, 0, 0];
6021
+ const rotOut = [0, 0, 0];
6022
+ const sclOut = [0, 0, 0];
6023
+ const id = ++this.nextInstanceId;
6024
+ const handle = {
6025
+ id,
4873
6026
  slot,
4874
6027
  modelId: modelHandle.id,
4875
6028
  skinned: true,
6029
+ prefabId,
4876
6030
  setPosition(nx, ny, nz) {
4877
6031
  dynamicData[dynBase + DYN_CURR_PX] = nx;
4878
6032
  dynamicData[dynBase + DYN_CURR_PY] = ny;
@@ -4888,6 +6042,32 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4888
6042
  staticData[statBase + SSTAT_SY] = ny;
4889
6043
  staticData[statBase + SSTAT_SZ] = nz;
4890
6044
  },
6045
+ teleport(nx, ny, nz) {
6046
+ dynamicData[dynBase + DYN_PREV_PX] = nx;
6047
+ dynamicData[dynBase + DYN_PREV_PY] = ny;
6048
+ dynamicData[dynBase + DYN_PREV_PZ] = nz;
6049
+ dynamicData[dynBase + DYN_CURR_PX] = nx;
6050
+ dynamicData[dynBase + DYN_CURR_PY] = ny;
6051
+ dynamicData[dynBase + DYN_CURR_PZ] = nz;
6052
+ },
6053
+ get position() {
6054
+ posOut[0] = dynamicData[dynBase + DYN_CURR_PX];
6055
+ posOut[1] = dynamicData[dynBase + DYN_CURR_PY];
6056
+ posOut[2] = dynamicData[dynBase + DYN_CURR_PZ];
6057
+ return posOut;
6058
+ },
6059
+ get rotation() {
6060
+ rotOut[0] = dynamicData[dynBase + DYN_CURR_RX];
6061
+ rotOut[1] = dynamicData[dynBase + DYN_CURR_RY];
6062
+ rotOut[2] = dynamicData[dynBase + DYN_CURR_RZ];
6063
+ return rotOut;
6064
+ },
6065
+ get scale() {
6066
+ sclOut[0] = staticData[statBase + SSTAT_SX];
6067
+ sclOut[1] = staticData[statBase + SSTAT_SY];
6068
+ sclOut[2] = staticData[statBase + SSTAT_SZ];
6069
+ return sclOut;
6070
+ },
4891
6071
  play(name, opts2) {
4892
6072
  const state = animStates[slot];
4893
6073
  if (state)
@@ -4907,6 +6087,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4907
6087
  dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4908
6088
  staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
4909
6089
  animStates[slot] = null;
6090
+ self.skinnedInstanceHandles[slot] = null;
4910
6091
  self.skinnedStaticDirty = true;
4911
6092
  if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
4912
6093
  let pool = self.freedBoneOffsets.get(capturedSkinIndex);
@@ -4918,6 +6099,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4918
6099
  }
4919
6100
  }
4920
6101
  };
6102
+ this.skinnedInstanceHandles[slot] = handle;
6103
+ return handle;
4921
6104
  }
4922
6105
  /**
4923
6106
  * Drain pending resyncs from the coordinator. Per affected skin: rebuild
@@ -5053,7 +6236,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5053
6236
  { binding: 1, resource: { buffer: this.rawSkinnedDynamicBuffer } },
5054
6237
  { binding: 2, resource: { buffer: this.rawSkinnedStaticBuffer } },
5055
6238
  { binding: 3, resource: { buffer: this.rawSkinnedSlotIndexBuffer } },
5056
- { binding: 4, resource: { buffer: rawBoneBuffer } }
6239
+ { binding: 4, resource: { buffer: rawBoneBuffer } },
6240
+ { binding: 5, resource: { buffer: this.rawLightBuffer } }
5057
6241
  ]
5058
6242
  });
5059
6243
  this.device.queue.writeBuffer(
@@ -5189,6 +6373,113 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5189
6373
  sDyn[base + DYN_PREV_RZ] = sDyn[base + DYN_CURR_RZ];
5190
6374
  }
5191
6375
  });
6376
+ this.lights.storePrevious();
6377
+ }
6378
+ /** Resolve an instance's declared hitbox name to its Hitbox via the bucket's library. */
6379
+ resolveHitbox(handle) {
6380
+ if (!this._prefabs || !handle.prefabId)
6381
+ return null;
6382
+ const lib = this._prefabs.hitboxLibrary;
6383
+ if (!lib)
6384
+ return null;
6385
+ const prefab = this._prefabs.get(handle.prefabId);
6386
+ const name = prefab?.hitbox;
6387
+ return name ? lib.get(name) : null;
6388
+ }
6389
+ /**
6390
+ * Pick test for a single instance. Returns the ray-`t` and the struck
6391
+ * part name, or `null`. Uses the prefab's declared hitbox when
6392
+ * available; falls back to the model's axis-aligned bounding box.
6393
+ */
6394
+ testInstanceRay(ray, handle, cx, cy, cz, sx, sy, sz, halfX, halfY, halfZ) {
6395
+ const hitbox = this.resolveHitbox(handle);
6396
+ if (hitbox) {
6397
+ const hit = testHitbox3D(ray, hitbox, cx, cy, cz, sx, sy, sz);
6398
+ return hit ? { distance: hit.distance, part: hit.part } : null;
6399
+ }
6400
+ const t = ray.entryBox(cx, cy, cz, halfX * sx, halfY * sy, halfZ * sz);
6401
+ return t === null ? null : { distance: t, part: null };
6402
+ }
6403
+ /** Push every instance the screen ray hits within [near, far] into the hit buffer. Unsorted. */
6404
+ _collectRaycastHitsInto(screenX, screenY, rc) {
6405
+ const ray = this.camera.screenToRay(screenX, screenY);
6406
+ const ox = ray.origin[0], oy = ray.origin[1], oz = ray.origin[2];
6407
+ const dx = ray.direction[0], dy = ray.direction[1], dz = ray.direction[2];
6408
+ const minDistance = this.camera.near;
6409
+ const maxDistance = this.camera.far;
6410
+ const dyn = this.dynamicData;
6411
+ const stat = this.staticData;
6412
+ const models = this.models;
6413
+ this.batcher.each((_, instances, count) => {
6414
+ for (let i = 0; i < count; i++) {
6415
+ const slot = instances[i];
6416
+ const handle = this.instanceHandles[slot];
6417
+ if (handle === null)
6418
+ continue;
6419
+ const model = models[handle.modelId];
6420
+ if (!model)
6421
+ continue;
6422
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6423
+ const statBase = slot * STATIC_MESH_FLOATS;
6424
+ const hit = this.testInstanceRay(
6425
+ ray,
6426
+ handle,
6427
+ dyn[dynBase + DYN_CURR_PX],
6428
+ dyn[dynBase + DYN_CURR_PY],
6429
+ dyn[dynBase + DYN_CURR_PZ],
6430
+ stat[statBase + STAT_SX],
6431
+ stat[statBase + STAT_SY],
6432
+ stat[statBase + STAT_SZ],
6433
+ model.halfX,
6434
+ model.halfY,
6435
+ model.halfZ
6436
+ );
6437
+ if (hit === null)
6438
+ continue;
6439
+ const t = hit.distance;
6440
+ if (t < minDistance || t > maxDistance)
6441
+ continue;
6442
+ rc.push(handle, t, ox + dx * t, oy + dy * t, oz + dz * t, t, hit.part);
6443
+ }
6444
+ });
6445
+ const sDyn = this.skinnedDynamicData;
6446
+ const sStat = this.skinnedStaticData;
6447
+ const skinned = this.skinnedModels;
6448
+ this.skinnedBatcher.each((_, instances, count) => {
6449
+ for (let i = 0; i < count; i++) {
6450
+ const slot = instances[i];
6451
+ const handle = this.skinnedInstanceHandles[slot];
6452
+ if (handle === null)
6453
+ continue;
6454
+ const model = models[handle.modelId];
6455
+ if (!model)
6456
+ continue;
6457
+ const skin = skinned[model.skinIndex];
6458
+ if (!skin)
6459
+ continue;
6460
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6461
+ const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
6462
+ const hit = this.testInstanceRay(
6463
+ ray,
6464
+ handle,
6465
+ sDyn[dynBase + DYN_CURR_PX],
6466
+ sDyn[dynBase + DYN_CURR_PY],
6467
+ sDyn[dynBase + DYN_CURR_PZ],
6468
+ sStat[statBase + SSTAT_SX],
6469
+ sStat[statBase + SSTAT_SY],
6470
+ sStat[statBase + SSTAT_SZ],
6471
+ model.halfX,
6472
+ model.halfY,
6473
+ model.halfZ
6474
+ );
6475
+ if (hit === null)
6476
+ continue;
6477
+ const t = hit.distance;
6478
+ if (t < minDistance || t > maxDistance)
6479
+ continue;
6480
+ rc.push(handle, t, ox + dx * t, oy + dy * t, oz + dz * t, t, hit.part);
6481
+ }
6482
+ });
5192
6483
  }
5193
6484
  render(alpha) {
5194
6485
  if (!this._initialized)
@@ -5217,18 +6508,26 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5217
6508
  );
5218
6509
  this.staticDirty = false;
5219
6510
  }
6511
+ const packed = this.lights.pack();
6512
+ if (packed.count > 0) {
6513
+ this.device.queue.writeBuffer(
6514
+ this.rawLightBuffer,
6515
+ 0,
6516
+ packed.data.buffer,
6517
+ packed.data.byteOffset,
6518
+ packed.byteLength
6519
+ );
6520
+ }
5220
6521
  const vpMatrix = this.camera.getViewProjectionMatrix();
5221
6522
  this.uniformData.set(vpMatrix, 0);
5222
- this.uniformData[16] = alpha;
5223
- this.uniformData[17] = 0.3;
5224
- this.uniformData[18] = 0.8;
5225
- this.uniformData[19] = 0.5;
6523
+ this.uniformData[MESH_UNIFORM_ALPHA_OFFSET] = alpha;
6524
+ this.lights.writeUniforms(this.uniformData, MESH_UNIFORM_LIGHT_OFFSET, packed.count);
5226
6525
  this.device.queue.writeBuffer(
5227
6526
  this.rawUniformBuffer,
5228
6527
  0,
5229
6528
  this.uniformData.buffer,
5230
6529
  this.uniformData.byteOffset,
5231
- 80
6530
+ this.uniformData.byteLength
5232
6531
  );
5233
6532
  this.extractFrustumPlanes(vpMatrix);
5234
6533
  let indexOffset = 0;
@@ -5404,9 +6703,76 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5404
6703
  pass.draw(model.vertexCount, batch.count, 0, batch.offset);
5405
6704
  }
5406
6705
  }
6706
+ if (this.debug.hitboxes) {
6707
+ this.drawDebugHitboxes(pass, vpMatrix);
6708
+ }
5407
6709
  pass.end();
5408
6710
  this.device.queue.submit([encoder.finish()]);
5409
6711
  }
6712
+ drawDebugHitboxes(pass, vp) {
6713
+ if (!this._prefabs)
6714
+ return;
6715
+ const debug = this.hitboxDebug;
6716
+ const state = this.raycast.state;
6717
+ debug.begin(vp);
6718
+ const dyn = this.dynamicData;
6719
+ const stat = this.staticData;
6720
+ this.batcher.each((_, instances, count) => {
6721
+ for (let i = 0; i < count; i++) {
6722
+ const slot = instances[i];
6723
+ const handle = this.instanceHandles[slot];
6724
+ if (handle === null)
6725
+ continue;
6726
+ const hb = this.resolveHitbox(handle);
6727
+ if (!hb)
6728
+ continue;
6729
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6730
+ const statBase = slot * STATIC_MESH_FLOATS;
6731
+ const hovered = state.containsId(handle.id);
6732
+ for (const part of hb.parts) {
6733
+ debug.emit(
6734
+ part,
6735
+ hovered,
6736
+ dyn[dynBase + DYN_CURR_PX],
6737
+ dyn[dynBase + DYN_CURR_PY],
6738
+ dyn[dynBase + DYN_CURR_PZ],
6739
+ stat[statBase + STAT_SX],
6740
+ stat[statBase + STAT_SY],
6741
+ stat[statBase + STAT_SZ]
6742
+ );
6743
+ }
6744
+ }
6745
+ });
6746
+ const sDyn = this.skinnedDynamicData;
6747
+ const sStat = this.skinnedStaticData;
6748
+ this.skinnedBatcher.each((_, instances, count) => {
6749
+ for (let i = 0; i < count; i++) {
6750
+ const slot = instances[i];
6751
+ const handle = this.skinnedInstanceHandles[slot];
6752
+ if (handle === null)
6753
+ continue;
6754
+ const hb = this.resolveHitbox(handle);
6755
+ if (!hb)
6756
+ continue;
6757
+ const dynBase = slot * DYNAMIC_MESH_FLOATS;
6758
+ const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
6759
+ const hovered = state.containsId(handle.id);
6760
+ for (const part of hb.parts) {
6761
+ debug.emit(
6762
+ part,
6763
+ hovered,
6764
+ sDyn[dynBase + DYN_CURR_PX],
6765
+ sDyn[dynBase + DYN_CURR_PY],
6766
+ sDyn[dynBase + DYN_CURR_PZ],
6767
+ sStat[statBase + SSTAT_SX],
6768
+ sStat[statBase + SSTAT_SY],
6769
+ sStat[statBase + SSTAT_SZ]
6770
+ );
6771
+ }
6772
+ }
6773
+ });
6774
+ debug.flush(pass);
6775
+ }
5410
6776
  /**
5411
6777
  * Extract 6 frustum planes from a column-major VP matrix.
5412
6778
  * Each plane is [a, b, c, d] where ax + by + cz + d >= 0 means inside.
@@ -5587,12 +6953,12 @@ var ParticleEmitter = class {
5587
6953
  this.renderer = renderer;
5588
6954
  this.config = config;
5589
6955
  this.rng = new SimpleRNG(config.seed);
5590
- const max2 = config.max;
5591
- this.sprites = new Array(max2).fill(null);
5592
- this.lifetimes = new Float32Array(max2);
5593
- this.maxLifetimes = new Float32Array(max2);
5594
- this.velocitiesX = new Float32Array(max2);
5595
- this.velocitiesY = new Float32Array(max2);
6956
+ const max3 = config.max;
6957
+ this.sprites = new Array(max3).fill(null);
6958
+ this.lifetimes = new Float32Array(max3);
6959
+ this.maxLifetimes = new Float32Array(max3);
6960
+ this.velocitiesX = new Float32Array(max3);
6961
+ this.velocitiesY = new Float32Array(max3);
5596
6962
  }
5597
6963
  emit(x, y, count = 1) {
5598
6964
  for (let i = 0; i < count; i++) {
@@ -5668,61 +7034,6 @@ var ParticleEmitter = class {
5668
7034
  this.head = 0;
5669
7035
  }
5670
7036
  };
5671
-
5672
- // src/shaders/utils.ts
5673
- import tgpu7 from "typegpu";
5674
- import * as d6 from "typegpu/data";
5675
- import * as std4 from "typegpu/std";
5676
- var rotate2d = tgpu7.fn([d6.vec2f, d6.f32], d6.vec2f)(
5677
- function rotate2d2(point, angle) {
5678
- "use gpu";
5679
- const c = std4.cos(angle);
5680
- const s = std4.sin(angle);
5681
- return d6.vec2f(
5682
- point.x * c - point.y * s,
5683
- point.x * s + point.y * c
5684
- );
5685
- }
5686
- );
5687
- var worldToClip2d = tgpu7.fn([d6.vec2f, d6.mat3x3f], d6.vec4f)(
5688
- function worldToClip2d2(worldPos, cameraMatrix) {
5689
- "use gpu";
5690
- const clip = cameraMatrix * d6.vec3f(worldPos.x, worldPos.y, 1);
5691
- return d6.vec4f(clip.x, clip.y, 0, 1);
5692
- }
5693
- );
5694
- var worldToClip3d = tgpu7.fn([d6.vec3f, d6.mat4x4f], d6.vec4f)(
5695
- function worldToClip3d2(worldPos, vpMatrix) {
5696
- "use gpu";
5697
- return vpMatrix * d6.vec4f(worldPos.x, worldPos.y, worldPos.z, 1);
5698
- }
5699
- );
5700
- var remap = tgpu7.fn([d6.f32, d6.f32, d6.f32, d6.f32, d6.f32], d6.f32)(
5701
- function remap2(value, inMin, inMax, outMin, outMax) {
5702
- "use gpu";
5703
- const t = (value - inMin) / (inMax - inMin);
5704
- return outMin + t * (outMax - outMin);
5705
- }
5706
- );
5707
- var scaleRotate2d = tgpu7.fn([d6.vec2f, d6.f32], d6.mat2x2f)(
5708
- function scaleRotate2d2(scale, angle) {
5709
- "use gpu";
5710
- const c = std4.cos(angle);
5711
- const s = std4.sin(angle);
5712
- return d6.mat2x2f(
5713
- scale.x * c,
5714
- scale.x * s,
5715
- -(scale.y * s),
5716
- scale.y * c
5717
- );
5718
- }
5719
- );
5720
- var inverseLerp = tgpu7.fn([d6.f32, d6.f32, d6.f32], d6.f32)(
5721
- function inverseLerp2(min, max2, value) {
5722
- "use gpu";
5723
- return std4.saturate((value - min) / (max2 - min));
5724
- }
5725
- );
5726
7037
  export {
5727
7038
  AnimationController,
5728
7039
  Camera2D,