murow 0.1.1 → 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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/core/clock/clock.js +1 -0
  3. package/dist/cjs/core/clock/index.js +1 -0
  4. package/dist/cjs/core/hitbox/hitbox-library.js +1 -0
  5. package/dist/cjs/core/hitbox/hitbox.js +1 -0
  6. package/dist/cjs/core/hitbox/index.js +1 -0
  7. package/dist/cjs/core/hitbox/test.js +1 -0
  8. package/dist/cjs/core/index.js +1 -1
  9. package/dist/cjs/core/prediction/prediction.js +1 -1
  10. package/dist/cjs/core/ray/ray-3d.js +1 -1
  11. package/dist/cjs/core/raycast/hit-buffer.js +1 -0
  12. package/dist/cjs/core/raycast/index.js +1 -0
  13. package/dist/cjs/core/raycast/raycaster.js +1 -0
  14. package/dist/cjs/core/slot-map/index.js +1 -0
  15. package/dist/cjs/core/slot-map/slot-map.js +1 -0
  16. package/dist/cjs/core/state-machine/index.js +1 -0
  17. package/dist/cjs/core/state-machine/state-machine.js +1 -0
  18. package/dist/cjs/core/timeline/index.js +1 -0
  19. package/dist/cjs/core/timeline/timeline.js +1 -0
  20. package/dist/cjs/game/loop/loop.js +1 -1
  21. package/dist/cjs/game/loop/ticker-schedule.js +1 -0
  22. package/dist/cjs/renderer/index.js +1 -1
  23. package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -1
  24. package/dist/cjs/renderer/prefab-bucket/index.js +1 -1
  25. package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -1
  26. package/dist/cjs/renderer/prefab-bucket/specs.js +1 -1
  27. package/dist/cjs/renderer/raycast/index.js +1 -0
  28. package/dist/cjs/renderer/raycast/raycast.js +1 -0
  29. package/dist/esm/core/clock/clock.js +1 -0
  30. package/dist/esm/core/clock/index.js +1 -0
  31. package/dist/esm/core/hitbox/hitbox-library.js +1 -0
  32. package/dist/esm/core/hitbox/hitbox.js +1 -0
  33. package/dist/esm/core/hitbox/index.js +1 -0
  34. package/dist/esm/core/hitbox/test.js +1 -0
  35. package/dist/esm/core/index.js +1 -1
  36. package/dist/esm/core/prediction/prediction.js +1 -1
  37. package/dist/esm/core/ray/ray-3d.js +1 -1
  38. package/dist/esm/core/raycast/hit-buffer.js +1 -0
  39. package/dist/esm/core/raycast/index.js +1 -0
  40. package/dist/esm/core/raycast/raycaster.js +1 -0
  41. package/dist/esm/core/slot-map/index.js +1 -0
  42. package/dist/esm/core/slot-map/slot-map.js +1 -0
  43. package/dist/esm/core/state-machine/index.js +1 -0
  44. package/dist/esm/core/state-machine/state-machine.js +1 -0
  45. package/dist/esm/core/timeline/index.js +1 -0
  46. package/dist/esm/core/timeline/timeline.js +1 -0
  47. package/dist/esm/game/loop/loop.js +1 -1
  48. package/dist/esm/game/loop/ticker-schedule.js +1 -0
  49. package/dist/esm/renderer/index.js +1 -1
  50. package/dist/esm/renderer/prefab-bucket/concrete.js +1 -1
  51. package/dist/esm/renderer/prefab-bucket/index.js +1 -1
  52. package/dist/esm/renderer/prefab-bucket/parsers.js +1 -1
  53. package/dist/esm/renderer/raycast/index.js +1 -0
  54. package/dist/esm/renderer/raycast/raycast.js +1 -0
  55. package/dist/netcode/cjs/index.js +144 -140
  56. package/dist/netcode/esm/index.js +144 -140
  57. package/dist/netcode/types/client/game-client.d.ts +17 -3
  58. package/dist/netcode/types/client/strategies/snapshot-interpolation.d.ts +33 -0
  59. package/dist/netcode/types/codec/delta-codec.d.ts +1 -1
  60. package/dist/netcode/types/components/sync-spec.d.ts +6 -0
  61. package/dist/types/core/clock/clock.d.ts +37 -0
  62. package/dist/types/core/clock/index.d.ts +1 -0
  63. package/dist/types/core/hitbox/hitbox-library.d.ts +29 -0
  64. package/dist/types/core/hitbox/hitbox.d.ts +50 -0
  65. package/dist/types/core/hitbox/index.d.ts +3 -0
  66. package/dist/types/core/hitbox/test.d.ts +44 -0
  67. package/dist/types/core/index.d.ts +6 -0
  68. package/dist/types/core/prediction/prediction.d.ts +35 -58
  69. package/dist/types/core/ray/ray-3d.d.ts +21 -1
  70. package/dist/types/core/raycast/hit-buffer.d.ts +43 -0
  71. package/dist/types/core/raycast/index.d.ts +2 -0
  72. package/dist/types/core/raycast/raycaster.d.ts +54 -0
  73. package/dist/types/core/slot-map/index.d.ts +1 -0
  74. package/dist/types/core/slot-map/slot-map.d.ts +109 -0
  75. package/dist/types/core/state-machine/index.d.ts +1 -0
  76. package/dist/types/core/state-machine/state-machine.d.ts +114 -0
  77. package/dist/types/core/timeline/index.d.ts +1 -0
  78. package/dist/types/core/timeline/timeline.d.ts +34 -0
  79. package/dist/types/game/loop/loop.d.ts +30 -0
  80. package/dist/types/game/loop/ticker-schedule.d.ts +52 -0
  81. package/dist/types/renderer/index.d.ts +1 -0
  82. package/dist/types/renderer/prefab-bucket/concrete.d.ts +16 -6
  83. package/dist/types/renderer/prefab-bucket/index.d.ts +11 -7
  84. package/dist/types/renderer/prefab-bucket/specs.d.ts +10 -0
  85. package/dist/types/renderer/raycast/index.d.ts +1 -0
  86. package/dist/types/renderer/raycast/raycast.d.ts +24 -0
  87. package/dist/types/renderer/types.d.ts +1 -0
  88. package/dist/webgpu/cjs/index.js +1777 -587
  89. package/dist/webgpu/esm/index.js +1769 -573
  90. package/dist/webgpu/types/2d/raycast.d.ts +45 -0
  91. package/dist/webgpu/types/2d/renderer.d.ts +11 -0
  92. package/dist/webgpu/types/2d/sprite-accessor.d.ts +3 -1
  93. package/dist/webgpu/types/3d/hitbox.d.ts +32 -0
  94. package/dist/webgpu/types/3d/lights.d.ts +113 -0
  95. package/dist/webgpu/types/3d/lights.test.d.ts +1 -0
  96. package/dist/webgpu/types/3d/raycast.d.ts +44 -0
  97. package/dist/webgpu/types/3d/renderer.d.ts +50 -1
  98. package/dist/webgpu/types/3d/shader.d.ts +88 -5
  99. package/dist/webgpu/types/core/types.d.ts +55 -0
  100. package/dist/webgpu/types/geometry/geometry-builder.d.ts +1 -4
  101. package/dist/webgpu/types/index.d.ts +1 -0
  102. package/dist/webgpu/types/shaders/utils.d.ts +24 -0
  103. package/package.json +1 -1
  104. package/dist/netcode/types/client/interpolation-buffer.d.ts +0 -37
  105. /package/dist/netcode/types/client/{interpolation-buffer.test.d.ts → strategies/snapshot-interpolation.test.d.ts} +0 -0
@@ -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,
@@ -4669,7 +5726,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4669
5726
  const posOut = [0, 0, 0];
4670
5727
  const rotOut = [0, 0, 0];
4671
5728
  const sclOut = [0, 0, 0];
5729
+ const id = ++this.nextInstanceId;
4672
5730
  const handle = {
5731
+ id,
4673
5732
  slot,
4674
5733
  modelId: modelHandle.id,
4675
5734
  skinned: false,
@@ -4723,9 +5782,11 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4723
5782
  self.freeList.free(slot);
4724
5783
  dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4725
5784
  staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
5785
+ self.instanceHandles[slot] = null;
4726
5786
  self.staticDirty = true;
4727
5787
  }
4728
5788
  };
5789
+ this.instanceHandles[slot] = handle;
4729
5790
  return handle;
4730
5791
  }
4731
5792
  addGltfInstance(opts, gltf, prefabId) {
@@ -4747,6 +5808,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4747
5808
  const skinnedHandle = childHandles.find((h) => h.skinned);
4748
5809
  const lead = childHandles[0];
4749
5810
  return {
5811
+ id: lead.id,
4750
5812
  skinned: gltf.skinned,
4751
5813
  prefabId,
4752
5814
  setPosition(x, y, z) {
@@ -4834,6 +5896,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4834
5896
  childHandles.push(this.addInstance(partOpts));
4835
5897
  }
4836
5898
  return {
5899
+ id: childHandles[0].id,
4837
5900
  skinned: childHandles.some((h) => h.skinned),
4838
5901
  prefabId: composite.id,
4839
5902
  setPosition(x, y, z) {
@@ -4957,7 +6020,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4957
6020
  const posOut = [0, 0, 0];
4958
6021
  const rotOut = [0, 0, 0];
4959
6022
  const sclOut = [0, 0, 0];
4960
- return {
6023
+ const id = ++this.nextInstanceId;
6024
+ const handle = {
6025
+ id,
4961
6026
  slot,
4962
6027
  modelId: modelHandle.id,
4963
6028
  skinned: true,
@@ -5022,6 +6087,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5022
6087
  dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
5023
6088
  staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
5024
6089
  animStates[slot] = null;
6090
+ self.skinnedInstanceHandles[slot] = null;
5025
6091
  self.skinnedStaticDirty = true;
5026
6092
  if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
5027
6093
  let pool = self.freedBoneOffsets.get(capturedSkinIndex);
@@ -5033,6 +6099,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5033
6099
  }
5034
6100
  }
5035
6101
  };
6102
+ this.skinnedInstanceHandles[slot] = handle;
6103
+ return handle;
5036
6104
  }
5037
6105
  /**
5038
6106
  * Drain pending resyncs from the coordinator. Per affected skin: rebuild
@@ -5168,7 +6236,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5168
6236
  { binding: 1, resource: { buffer: this.rawSkinnedDynamicBuffer } },
5169
6237
  { binding: 2, resource: { buffer: this.rawSkinnedStaticBuffer } },
5170
6238
  { binding: 3, resource: { buffer: this.rawSkinnedSlotIndexBuffer } },
5171
- { binding: 4, resource: { buffer: rawBoneBuffer } }
6239
+ { binding: 4, resource: { buffer: rawBoneBuffer } },
6240
+ { binding: 5, resource: { buffer: this.rawLightBuffer } }
5172
6241
  ]
5173
6242
  });
5174
6243
  this.device.queue.writeBuffer(
@@ -5304,6 +6373,113 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5304
6373
  sDyn[base + DYN_PREV_RZ] = sDyn[base + DYN_CURR_RZ];
5305
6374
  }
5306
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
+ });
5307
6483
  }
5308
6484
  render(alpha) {
5309
6485
  if (!this._initialized)
@@ -5332,18 +6508,26 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5332
6508
  );
5333
6509
  this.staticDirty = false;
5334
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
+ }
5335
6521
  const vpMatrix = this.camera.getViewProjectionMatrix();
5336
6522
  this.uniformData.set(vpMatrix, 0);
5337
- this.uniformData[16] = alpha;
5338
- this.uniformData[17] = 0.3;
5339
- this.uniformData[18] = 0.8;
5340
- 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);
5341
6525
  this.device.queue.writeBuffer(
5342
6526
  this.rawUniformBuffer,
5343
6527
  0,
5344
6528
  this.uniformData.buffer,
5345
6529
  this.uniformData.byteOffset,
5346
- 80
6530
+ this.uniformData.byteLength
5347
6531
  );
5348
6532
  this.extractFrustumPlanes(vpMatrix);
5349
6533
  let indexOffset = 0;
@@ -5519,9 +6703,76 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5519
6703
  pass.draw(model.vertexCount, batch.count, 0, batch.offset);
5520
6704
  }
5521
6705
  }
6706
+ if (this.debug.hitboxes) {
6707
+ this.drawDebugHitboxes(pass, vpMatrix);
6708
+ }
5522
6709
  pass.end();
5523
6710
  this.device.queue.submit([encoder.finish()]);
5524
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
+ }
5525
6776
  /**
5526
6777
  * Extract 6 frustum planes from a column-major VP matrix.
5527
6778
  * Each plane is [a, b, c, d] where ax + by + cz + d >= 0 means inside.
@@ -5702,12 +6953,12 @@ var ParticleEmitter = class {
5702
6953
  this.renderer = renderer;
5703
6954
  this.config = config;
5704
6955
  this.rng = new SimpleRNG(config.seed);
5705
- const max2 = config.max;
5706
- this.sprites = new Array(max2).fill(null);
5707
- this.lifetimes = new Float32Array(max2);
5708
- this.maxLifetimes = new Float32Array(max2);
5709
- this.velocitiesX = new Float32Array(max2);
5710
- 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);
5711
6962
  }
5712
6963
  emit(x, y, count = 1) {
5713
6964
  for (let i = 0; i < count; i++) {
@@ -5783,61 +7034,6 @@ var ParticleEmitter = class {
5783
7034
  this.head = 0;
5784
7035
  }
5785
7036
  };
5786
-
5787
- // src/shaders/utils.ts
5788
- import tgpu7 from "typegpu";
5789
- import * as d6 from "typegpu/data";
5790
- import * as std4 from "typegpu/std";
5791
- var rotate2d = tgpu7.fn([d6.vec2f, d6.f32], d6.vec2f)(
5792
- function rotate2d2(point, angle) {
5793
- "use gpu";
5794
- const c = std4.cos(angle);
5795
- const s = std4.sin(angle);
5796
- return d6.vec2f(
5797
- point.x * c - point.y * s,
5798
- point.x * s + point.y * c
5799
- );
5800
- }
5801
- );
5802
- var worldToClip2d = tgpu7.fn([d6.vec2f, d6.mat3x3f], d6.vec4f)(
5803
- function worldToClip2d2(worldPos, cameraMatrix) {
5804
- "use gpu";
5805
- const clip = cameraMatrix * d6.vec3f(worldPos.x, worldPos.y, 1);
5806
- return d6.vec4f(clip.x, clip.y, 0, 1);
5807
- }
5808
- );
5809
- var worldToClip3d = tgpu7.fn([d6.vec3f, d6.mat4x4f], d6.vec4f)(
5810
- function worldToClip3d2(worldPos, vpMatrix) {
5811
- "use gpu";
5812
- return vpMatrix * d6.vec4f(worldPos.x, worldPos.y, worldPos.z, 1);
5813
- }
5814
- );
5815
- var remap = tgpu7.fn([d6.f32, d6.f32, d6.f32, d6.f32, d6.f32], d6.f32)(
5816
- function remap2(value, inMin, inMax, outMin, outMax) {
5817
- "use gpu";
5818
- const t = (value - inMin) / (inMax - inMin);
5819
- return outMin + t * (outMax - outMin);
5820
- }
5821
- );
5822
- var scaleRotate2d = tgpu7.fn([d6.vec2f, d6.f32], d6.mat2x2f)(
5823
- function scaleRotate2d2(scale, angle) {
5824
- "use gpu";
5825
- const c = std4.cos(angle);
5826
- const s = std4.sin(angle);
5827
- return d6.mat2x2f(
5828
- scale.x * c,
5829
- scale.x * s,
5830
- -(scale.y * s),
5831
- scale.y * c
5832
- );
5833
- }
5834
- );
5835
- var inverseLerp = tgpu7.fn([d6.f32, d6.f32, d6.f32], d6.f32)(
5836
- function inverseLerp2(min, max2, value) {
5837
- "use gpu";
5838
- return std4.saturate((value - min) / (max2 - min));
5839
- }
5840
- );
5841
7037
  export {
5842
7038
  AnimationController,
5843
7039
  Camera2D,