murow 0.0.71 → 0.0.72

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 (65) hide show
  1. package/README.md +7 -5
  2. package/dist/cjs/renderer/{base-2d-renderer.js → base/renderer-2d.js} +1 -1
  3. package/dist/cjs/renderer/{base-3d-renderer.js → base/renderer-3d.js} +1 -1
  4. package/dist/cjs/renderer/gltf/helpers.js +1 -0
  5. package/dist/cjs/renderer/gltf/parser.js +1 -0
  6. package/dist/cjs/renderer/gltf/skeletal-animation.js +1 -0
  7. package/dist/cjs/renderer/gltf/skin-parser.js +1 -0
  8. package/dist/cjs/renderer/index.js +1 -1
  9. package/dist/cjs/renderer/math.js +1 -0
  10. package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -0
  11. package/dist/cjs/renderer/prefab-bucket/index.js +1 -0
  12. package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -0
  13. package/dist/cjs/renderer/prefab-bucket/specs.js +1 -0
  14. package/dist/cjs/renderer/spritesheet/helpers.js +1 -0
  15. package/dist/cjs/renderer/spritesheet/parser.js +1 -0
  16. package/dist/cjs/renderer/types.js +1 -1
  17. package/dist/esm/renderer/base/renderer-2d.js +1 -0
  18. package/dist/esm/renderer/base/renderer-3d.js +1 -0
  19. package/dist/esm/renderer/gltf/helpers.js +1 -0
  20. package/dist/esm/renderer/gltf/parser.js +1 -0
  21. package/dist/esm/renderer/gltf/skeletal-animation.js +1 -0
  22. package/dist/esm/renderer/gltf/skin-parser.js +1 -0
  23. package/dist/esm/renderer/index.js +1 -1
  24. package/dist/esm/renderer/math.js +1 -0
  25. package/dist/esm/renderer/prefab-bucket/concrete.js +1 -0
  26. package/dist/esm/renderer/prefab-bucket/index.js +1 -0
  27. package/dist/esm/renderer/prefab-bucket/parsers.js +1 -0
  28. package/dist/esm/renderer/prefab-bucket/specs.js +0 -0
  29. package/dist/esm/renderer/spritesheet/helpers.js +1 -0
  30. package/dist/esm/renderer/spritesheet/parser.js +1 -0
  31. package/dist/types/renderer/{base-2d-renderer.d.ts → base/renderer-2d.d.ts} +2 -2
  32. package/dist/types/renderer/{base-3d-renderer.d.ts → base/renderer-3d.d.ts} +2 -2
  33. package/dist/types/renderer/{base-renderer.d.ts → base/renderer.d.ts} +1 -1
  34. package/dist/types/renderer/gltf/helpers.d.ts +43 -0
  35. package/dist/types/renderer/gltf/parser.d.ts +49 -0
  36. package/dist/{webgpu/types/3d → types/renderer/gltf}/skeletal-animation.d.ts +8 -2
  37. package/dist/{webgpu/types/3d/gltf-skin-parser.d.ts → types/renderer/gltf/skin-parser.d.ts} +11 -5
  38. package/dist/types/renderer/index.d.ts +14 -3
  39. package/dist/types/renderer/prefab-bucket/concrete.d.ts +55 -0
  40. package/dist/types/renderer/prefab-bucket/index.d.ts +113 -0
  41. package/dist/types/renderer/prefab-bucket/parsers.d.ts +8 -0
  42. package/dist/types/renderer/prefab-bucket/specs.d.ts +166 -0
  43. package/dist/types/renderer/spritesheet/helpers.d.ts +38 -0
  44. package/dist/types/renderer/spritesheet/parser.d.ts +21 -0
  45. package/dist/types/renderer/types.d.ts +17 -7
  46. package/dist/webgpu/cjs/index.js +479 -1082
  47. package/dist/webgpu/esm/index.js +485 -1079
  48. package/dist/webgpu/types/2d/renderer.d.ts +34 -3
  49. package/dist/webgpu/types/2d/sprite-accessor.d.ts +1 -1
  50. package/dist/webgpu/types/3d/clip-resync-coordinator.d.ts +20 -0
  51. package/dist/webgpu/types/3d/renderer.d.ts +64 -14
  52. package/dist/webgpu/types/3d/skeletal-animation-compute/index.d.ts +1 -1
  53. package/dist/webgpu/types/3d/skeletal-animation-compute/kernel.d.ts +19 -2
  54. package/dist/webgpu/types/3d/skeletal-animation-compute/packer.d.ts +1 -1
  55. package/dist/webgpu/types/camera/camera-2d.d.ts +1 -1
  56. package/dist/webgpu/types/camera/camera-3d.d.ts +1 -1
  57. package/dist/webgpu/types/index.d.ts +15 -12
  58. package/dist/webgpu/types/particle/emitter.d.ts +1 -1
  59. package/dist/webgpu/types/spritesheet/spritesheet.d.ts +5 -34
  60. package/package.json +1 -1
  61. package/dist/esm/renderer/base-2d-renderer.js +0 -1
  62. package/dist/esm/renderer/base-3d-renderer.js +0 -1
  63. /package/dist/cjs/renderer/{base-renderer.js → base/renderer.js} +0 -0
  64. /package/dist/esm/renderer/{base-renderer.js → base/renderer.js} +0 -0
  65. /package/dist/{webgpu/types/core → types/renderer}/math.d.ts +0 -0
@@ -8,7 +8,7 @@ var std = { ..._std };
8
8
  import tgpu4 from "typegpu";
9
9
  import * as d4 from "typegpu/data";
10
10
  import { FreeList as FreeList2 } from "murow/core/free-list";
11
- import { Base2DRenderer } from "murow/renderer/base-2d-renderer";
11
+ import { Base2DRenderer } from "murow/renderer";
12
12
 
13
13
  // src/core/constants.ts
14
14
  var DYNAMIC_OFFSET_PREV_X = 0;
@@ -527,6 +527,11 @@ function createSpriteFragment(_spriteLayout, textureLayout) {
527
527
  }
528
528
 
529
529
  // src/spritesheet/spritesheet.ts
530
+ import {
531
+ computeGridUVs,
532
+ computeTexturePackerUVs,
533
+ loadImage
534
+ } from "murow/renderer";
530
535
  var Spritesheet = class {
531
536
  constructor(id, texture, textureView, sampler, uvs, width, height) {
532
537
  this.id = id;
@@ -551,41 +556,6 @@ var Spritesheet = class {
551
556
  return this._height;
552
557
  }
553
558
  };
554
- function computeGridUVs(imageWidth, imageHeight, frameWidth, frameHeight) {
555
- const cols = Math.floor(imageWidth / frameWidth);
556
- const rows = Math.floor(imageHeight / frameHeight);
557
- const uvs = [];
558
- for (let row = 0; row < rows; row++) {
559
- for (let col = 0; col < cols; col++) {
560
- uvs.push({
561
- minX: col * frameWidth / imageWidth,
562
- minY: row * frameHeight / imageHeight,
563
- maxX: (col + 1) * frameWidth / imageWidth,
564
- maxY: (row + 1) * frameHeight / imageHeight
565
- });
566
- }
567
- }
568
- return uvs;
569
- }
570
- function computeTexturePackerUVs(data) {
571
- const { w, h } = data.meta.size;
572
- const uvs = [];
573
- for (const key of Object.keys(data.frames)) {
574
- const frame = data.frames[key].frame;
575
- uvs.push({
576
- minX: frame.x / w,
577
- minY: frame.y / h,
578
- maxX: (frame.x + frame.w) / w,
579
- maxY: (frame.y + frame.h) / h
580
- });
581
- }
582
- return uvs;
583
- }
584
- async function loadImage(url) {
585
- const response = await fetch(url);
586
- const blob = await response.blob();
587
- return createImageBitmap(blob);
588
- }
589
559
  function createTextureFromBitmap(device, bitmap) {
590
560
  const texture = device.createTexture({
591
561
  size: [bitmap.width, bitmap.height, 1],
@@ -600,6 +570,9 @@ function createTextureFromBitmap(device, bitmap) {
600
570
  return { texture, view: texture.createView() };
601
571
  }
602
572
 
573
+ // src/2d/renderer.ts
574
+ import { parseSpritesheet } from "murow/renderer";
575
+
603
576
  // src/geometry/geometry-builder.ts
604
577
  import tgpu2 from "typegpu";
605
578
 
@@ -2078,9 +2051,23 @@ var ComputeBuilder = class {
2078
2051
  };
2079
2052
 
2080
2053
  // src/2d/renderer.ts
2054
+ var prefab2DHandles = /* @__PURE__ */ new WeakMap();
2055
+ function isPrefab2D(value) {
2056
+ return value.type === "spritesheet";
2057
+ }
2058
+ function resolveSpritePrefabHandle(prefab) {
2059
+ const h = prefab2DHandles.get(prefab);
2060
+ if (!h) {
2061
+ throw new Error(
2062
+ `Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
2063
+ );
2064
+ }
2065
+ return h;
2066
+ }
2081
2067
  var WebGPU2DRenderer = class extends Base2DRenderer {
2082
2068
  constructor(canvas, options) {
2083
- super(canvas, options);
2069
+ const resolvedMaxSprites = options.maxSprites ?? options.maxInstances ?? 1024;
2070
+ super(canvas, { ...options, maxSprites: resolvedMaxSprites });
2084
2071
  this.staticDirty = false;
2085
2072
  // Per-sheet bind groups
2086
2073
  this.sheetBindGroups = /* @__PURE__ */ new Map();
@@ -2089,12 +2076,13 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2089
2076
  this.uniformData = new Float32Array(20);
2090
2077
  this.resizeObserver = null;
2091
2078
  this.resizeCallbacks = [];
2079
+ this._prefabs = options.prefabs ?? null;
2092
2080
  this.camera = new Camera2D(canvas.width || 800, canvas.height || 600);
2093
- this.freeList = new FreeList2(options.maxSprites);
2094
- this.batcher = new SparseBatcher(options.maxSprites);
2095
- this.dynamicData = new Float32Array(options.maxSprites * DYNAMIC_FLOATS_PER_SPRITE);
2096
- this.staticData = new Float32Array(options.maxSprites * STATIC_FLOATS_PER_SPRITE);
2097
- this.slotIndexData = new Uint32Array(options.maxSprites);
2081
+ this.freeList = new FreeList2(resolvedMaxSprites);
2082
+ this.batcher = new SparseBatcher(resolvedMaxSprites);
2083
+ this.dynamicData = new Float32Array(resolvedMaxSprites * DYNAMIC_FLOATS_PER_SPRITE);
2084
+ this.staticData = new Float32Array(resolvedMaxSprites * STATIC_FLOATS_PER_SPRITE);
2085
+ this.slotIndexData = new Uint32Array(resolvedMaxSprites);
2098
2086
  }
2099
2087
  get device() {
2100
2088
  return this._device;
@@ -2148,9 +2136,25 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2148
2136
  this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
2149
2137
  this.rawSlotIndexBuffer = this.root.unwrap(this.slotIndexBuffer);
2150
2138
  this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
2139
+ if (this._prefabs) {
2140
+ this.uploadPrefabBucket(this._prefabs);
2141
+ }
2151
2142
  this.setupResizeObserver();
2152
2143
  this._initialized = true;
2153
2144
  }
2145
+ /**
2146
+ * Upload every prefab in the bucket to the GPU and stash the resulting
2147
+ * SpritesheetHandle on each prefab so `bucket.get(id)` returns something
2148
+ * usable as a sprite source.
2149
+ */
2150
+ uploadPrefabBucket(bucket) {
2151
+ for (const prefab of bucket.entries()) {
2152
+ if (prefab.type === "spritesheet") {
2153
+ const handle = this.uploadParsedSpritesheet(prefab.parsed);
2154
+ prefab2DHandles.set(prefab, handle);
2155
+ }
2156
+ }
2157
+ }
2154
2158
  setupResizeObserver() {
2155
2159
  const supportsDevicePixelBox = (() => {
2156
2160
  try {
@@ -2204,24 +2208,22 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2204
2208
  this.resizeCallbacks.push(callback);
2205
2209
  }
2206
2210
  async loadSpritesheet(source) {
2207
- const bitmap = await loadImage(source.image);
2208
- const { texture, view } = createTextureFromBitmap(this._device, bitmap);
2211
+ const parsed = await parseSpritesheet(source);
2212
+ return this.uploadParsedSpritesheet(parsed);
2213
+ }
2214
+ /**
2215
+ * Upload a previously-parsed spritesheet to the GPU. Returns a SpritesheetHandle.
2216
+ * Splitting parse (CPU) from upload (GPU) lets callers parse spritesheets in parallel
2217
+ * before a renderer exists.
2218
+ */
2219
+ uploadParsedSpritesheet(parsed) {
2220
+ const { texture, view } = createTextureFromBitmap(this._device, parsed.bitmap);
2209
2221
  const sampler = this._device.createSampler({
2210
2222
  magFilter: "nearest",
2211
2223
  minFilter: "nearest"
2212
2224
  });
2213
- let uvs;
2214
- if (source.data) {
2215
- const resp = await fetch(source.data);
2216
- const json = await resp.json();
2217
- uvs = computeTexturePackerUVs(json);
2218
- } else if (source.frameWidth && source.frameHeight) {
2219
- uvs = computeGridUVs(bitmap.width, bitmap.height, source.frameWidth, source.frameHeight);
2220
- } else {
2221
- uvs = [{ minX: 0, minY: 0, maxX: 1, maxY: 1 }];
2222
- }
2223
2225
  const id = this.nextSheetId++;
2224
- const sheet = new Spritesheet(id, texture, view, sampler, uvs, bitmap.width, bitmap.height);
2226
+ const sheet = new Spritesheet(id, texture, view, sampler, parsed.uvs, parsed.width, parsed.height);
2225
2227
  this.sheets.set(id, sheet);
2226
2228
  const bindGroup = this._device.createBindGroup({
2227
2229
  layout: this.rawTextureLayout,
@@ -2239,18 +2241,20 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2239
2241
  throw new Error(`Max sprites (${this.maxSprites}) reached`);
2240
2242
  const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
2241
2243
  const statBase = slot * STATIC_FLOATS_PER_SPRITE;
2242
- const x = opts.x ?? 0;
2243
- const y = opts.y ?? 0;
2244
- this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] = x;
2245
- this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] = y;
2246
- this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_X] = x;
2247
- this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_Y] = y;
2244
+ const sheet = isPrefab2D(opts.sheet) ? resolveSpritePrefabHandle(opts.sheet) : opts.sheet;
2245
+ const [px, py] = opts.position ?? [0, 0];
2246
+ this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] = px;
2247
+ this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] = py;
2248
+ this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_X] = px;
2249
+ this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_Y] = py;
2248
2250
  const rotation = opts.rotation ?? 0;
2249
2251
  this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_ROTATION] = rotation;
2250
2252
  this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_ROTATION] = rotation;
2251
- this.staticData[statBase + STATIC_OFFSET_SCALE_X] = opts.scaleX ?? 1;
2252
- this.staticData[statBase + STATIC_OFFSET_SCALE_Y] = opts.scaleY ?? 1;
2253
- const uv = opts.sheet.getUV(opts.sprite ?? 0);
2253
+ const s = opts.scale;
2254
+ const [sx, sy] = typeof s === "number" ? [s, s] : s ?? [1, 1];
2255
+ this.staticData[statBase + STATIC_OFFSET_SCALE_X] = sx;
2256
+ this.staticData[statBase + STATIC_OFFSET_SCALE_Y] = sy;
2257
+ const uv = sheet.getUV(opts.sprite ?? 0);
2254
2258
  this.staticData[statBase + STATIC_OFFSET_UV_MIN_X] = uv.minX;
2255
2259
  this.staticData[statBase + STATIC_OFFSET_UV_MIN_Y] = uv.minY;
2256
2260
  this.staticData[statBase + STATIC_OFFSET_UV_MAX_X] = uv.maxX;
@@ -2265,12 +2269,12 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2265
2269
  this.staticData[statBase + STATIC_OFFSET_TINT_B] = tint[2];
2266
2270
  this.staticData[statBase + STATIC_OFFSET_TINT_A] = tint[3];
2267
2271
  this.staticDirty = true;
2268
- this.batcher.add(opts.layer ?? 0, opts.sheet.id, slot);
2272
+ this.batcher.add(opts.layer ?? 0, sheet.id, slot);
2269
2273
  return new SpriteAccessor(
2270
2274
  this.dynamicData,
2271
2275
  this.staticData,
2272
2276
  slot,
2273
- opts.sheet.id,
2277
+ sheet.id,
2274
2278
  () => {
2275
2279
  this.staticDirty = true;
2276
2280
  }
@@ -2392,9 +2396,112 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
2392
2396
  }
2393
2397
  };
2394
2398
 
2399
+ // src/2d/animation.ts
2400
+ var AnimationController = class {
2401
+ constructor() {
2402
+ this.clips = [];
2403
+ this.clipsByName = /* @__PURE__ */ new Map();
2404
+ }
2405
+ /**
2406
+ * Register an animation clip. Returns the clip ID.
2407
+ */
2408
+ loadClip(config) {
2409
+ const id = this.clips.length;
2410
+ const clip = {
2411
+ id,
2412
+ name: config.name,
2413
+ frames: new Uint16Array(config.frames),
2414
+ durations: new Float32Array(config.durations),
2415
+ frameCount: config.frames.length,
2416
+ totalDuration: config.durations.reduce((sum, d7) => sum + d7, 0),
2417
+ loop: config.loop
2418
+ };
2419
+ this.clips.push(clip);
2420
+ this.clipsByName.set(config.name, id);
2421
+ return id;
2422
+ }
2423
+ /**
2424
+ * Get a clip by name.
2425
+ */
2426
+ getClipId(name) {
2427
+ const id = this.clipsByName.get(name);
2428
+ if (id === void 0)
2429
+ throw new Error(`Animation clip "${name}" not found`);
2430
+ return id;
2431
+ }
2432
+ /**
2433
+ * Get a clip by ID.
2434
+ */
2435
+ getClip(id) {
2436
+ return this.clips[id];
2437
+ }
2438
+ /**
2439
+ * Create a new animation state for an entity. This is the per-entity data
2440
+ * that tracks playback progress. Store it however you like (flat array, component, etc).
2441
+ */
2442
+ createState(clipId, speed = 1, playing = true) {
2443
+ return { clipId, frame: 0, time: 0, speed, playing };
2444
+ }
2445
+ /**
2446
+ * Advance an animation state by deltaTime (in seconds).
2447
+ * Returns the current sprite frame ID, or -1 if not playing.
2448
+ * Zero allocations.
2449
+ */
2450
+ update(state, deltaTime) {
2451
+ if (!state.playing) {
2452
+ return this.clips[state.clipId].frames[state.frame];
2453
+ }
2454
+ const clip = this.clips[state.clipId];
2455
+ state.time += deltaTime * state.speed * 1e3;
2456
+ while (state.time >= clip.durations[state.frame]) {
2457
+ state.time -= clip.durations[state.frame];
2458
+ state.frame++;
2459
+ if (state.frame >= clip.frameCount) {
2460
+ if (clip.loop) {
2461
+ state.frame = 0;
2462
+ } else {
2463
+ state.frame = clip.frameCount - 1;
2464
+ state.playing = false;
2465
+ break;
2466
+ }
2467
+ }
2468
+ }
2469
+ return clip.frames[state.frame];
2470
+ }
2471
+ /**
2472
+ * Play a different clip on an existing state. Resets frame and time.
2473
+ */
2474
+ play(state, clipId, speed) {
2475
+ state.clipId = clipId;
2476
+ state.frame = 0;
2477
+ state.time = 0;
2478
+ state.playing = true;
2479
+ if (speed !== void 0)
2480
+ state.speed = speed;
2481
+ }
2482
+ /**
2483
+ * Stop playback.
2484
+ */
2485
+ stop(state) {
2486
+ state.playing = false;
2487
+ }
2488
+ /**
2489
+ * Resume playback.
2490
+ */
2491
+ resume(state) {
2492
+ state.playing = true;
2493
+ }
2494
+ /**
2495
+ * Number of loaded clips.
2496
+ */
2497
+ get clipCount() {
2498
+ return this.clips.length;
2499
+ }
2500
+ };
2501
+
2395
2502
  // src/3d/renderer.ts
2396
2503
  import tgpu6 from "typegpu";
2397
- import { Base3DRenderer } from "murow/renderer/base-3d-renderer";
2504
+ import { Base3DRenderer } from "murow/renderer";
2398
2505
  import { FreeList as FreeList3 } from "murow/core/free-list";
2399
2506
  import { SparseBatcher as SparseBatcher2 } from "murow/core/sparse-batcher";
2400
2507
 
@@ -3039,661 +3146,13 @@ function createSkinnedMeshVertex(layout) {
3039
3146
  });
3040
3147
  }
3041
3148
 
3042
- // src/core/math.ts
3043
- function mat4IdentityNew() {
3044
- const m = new Float32Array(16);
3045
- m[0] = 1;
3046
- m[5] = 1;
3047
- m[10] = 1;
3048
- m[15] = 1;
3049
- return m;
3050
- }
3051
- function trsToMat4(tx, ty, tz, qx, qy, qz, qw, sx, sy, sz, dst, offset) {
3052
- const xx = qx * qx, yy = qy * qy, zz = qz * qz;
3053
- const xy = qx * qy, xz = qx * qz, yz = qy * qz;
3054
- const wx = qw * qx, wy = qw * qy, wz = qw * qz;
3055
- dst[offset] = (1 - 2 * (yy + zz)) * sx;
3056
- dst[offset + 1] = 2 * (xy + wz) * sx;
3057
- dst[offset + 2] = 2 * (xz - wy) * sx;
3058
- dst[offset + 3] = 0;
3059
- dst[offset + 4] = 2 * (xy - wz) * sy;
3060
- dst[offset + 5] = (1 - 2 * (xx + zz)) * sy;
3061
- dst[offset + 6] = 2 * (yz + wx) * sy;
3062
- dst[offset + 7] = 0;
3063
- dst[offset + 8] = 2 * (xz + wy) * sz;
3064
- dst[offset + 9] = 2 * (yz - wx) * sz;
3065
- dst[offset + 10] = (1 - 2 * (xx + yy)) * sz;
3066
- dst[offset + 11] = 0;
3067
- dst[offset + 12] = tx;
3068
- dst[offset + 13] = ty;
3069
- dst[offset + 14] = tz;
3070
- dst[offset + 15] = 1;
3071
- }
3072
- function nodeToMat4(node) {
3073
- const m = new Float32Array(16);
3074
- if (node.matrix) {
3075
- m.set(node.matrix);
3076
- return m;
3077
- }
3078
- const t = node.translation ?? [0, 0, 0];
3079
- const r = node.rotation ?? [0, 0, 0, 1];
3080
- const s = node.scale ?? [1, 1, 1];
3081
- trsToMat4(t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2], m, 0);
3082
- return m;
3083
- }
3084
- var _mulTemp = new Float32Array(16);
3085
- function mat4Mul(a, aO, b, bO, dst, dO) {
3086
- for (let i = 0; i < 4; i++) {
3087
- for (let j = 0; j < 4; j++) {
3088
- _mulTemp[j * 4 + i] = a[aO + i] * b[bO + j * 4] + a[aO + 4 + i] * b[bO + j * 4 + 1] + a[aO + 8 + i] * b[bO + j * 4 + 2] + a[aO + 12 + i] * b[bO + j * 4 + 3];
3089
- }
3090
- }
3091
- dst.set(_mulTemp, dO);
3092
- }
3093
- function mat4MulNew(a, b) {
3094
- const o = new Float32Array(16);
3095
- for (let i = 0; i < 4; i++) {
3096
- for (let j = 0; j < 4; j++) {
3097
- o[j * 4 + i] = a[i] * b[j * 4] + a[4 + i] * b[j * 4 + 1] + a[8 + i] * b[j * 4 + 2] + a[12 + i] * b[j * 4 + 3];
3098
- }
3099
- }
3100
- return o;
3101
- }
3102
-
3103
- // src/3d/gltf-skin-parser.ts
3104
- function parseSkin(gltf, skinIndex, getAccessorData) {
3105
- const skin = gltf.skins[skinIndex];
3106
- const joints = skin.joints;
3107
- const jointCount = joints.length;
3108
- const ibmAccess = getAccessorData(skin.inverseBindMatrices);
3109
- const inverseBindMatrices = new Float32Array(ibmAccess.data);
3110
- const jointNodeIndices = new Uint16Array(joints);
3111
- const nodeToJoint = /* @__PURE__ */ new Map();
3112
- for (let j = 0; j < jointCount; j++) {
3113
- nodeToJoint.set(joints[j], j);
3114
- }
3115
- const nodeParent = /* @__PURE__ */ new Map();
3116
- for (let i = 0; i < gltf.nodes.length; i++) {
3117
- const children = gltf.nodes[i].children;
3118
- if (children) {
3119
- for (const child of children) {
3120
- nodeParent.set(child, i);
3121
- }
3122
- }
3123
- }
3124
- const parentJointIndices = new Int16Array(jointCount).fill(-1);
3125
- for (let j = 0; j < jointCount; j++) {
3126
- let parentNode = nodeParent.get(joints[j]);
3127
- while (parentNode !== void 0) {
3128
- const parentJoint = nodeToJoint.get(parentNode);
3129
- if (parentJoint !== void 0) {
3130
- parentJointIndices[j] = parentJoint;
3131
- break;
3132
- }
3133
- parentNode = nodeParent.get(parentNode);
3134
- }
3135
- }
3136
- let skeletonRootMatrix = null;
3137
- for (let j = 0; j < jointCount; j++) {
3138
- if (parentJointIndices[j] !== -1)
3139
- continue;
3140
- let ancestorNode = nodeParent.get(joints[j]);
3141
- const ancestorChain = [];
3142
- while (ancestorNode !== void 0 && !nodeToJoint.has(ancestorNode)) {
3143
- ancestorChain.push(ancestorNode);
3144
- ancestorNode = nodeParent.get(ancestorNode);
3145
- }
3146
- if (ancestorChain.length > 0) {
3147
- skeletonRootMatrix = mat4IdentityNew();
3148
- for (let i = ancestorChain.length - 1; i >= 0; i--) {
3149
- const nodeMat = nodeToMat4(gltf.nodes[ancestorChain[i]]);
3150
- skeletonRootMatrix = mat4MulNew(skeletonRootMatrix, nodeMat);
3151
- }
3152
- }
3153
- break;
3154
- }
3155
- return {
3156
- jointCount,
3157
- jointNodeIndices,
3158
- inverseBindMatrices,
3159
- parentJointIndices,
3160
- skeletonRootMatrix
3161
- };
3162
- }
3163
- function parseAnimations(gltf, skinData, getAccessorData) {
3164
- if (!gltf.animations?.length)
3165
- return [];
3166
- const nodeToJoint = /* @__PURE__ */ new Map();
3167
- for (let j = 0; j < skinData.jointCount; j++) {
3168
- nodeToJoint.set(skinData.jointNodeIndices[j], j);
3169
- }
3170
- const clips = [];
3171
- for (const anim of gltf.animations) {
3172
- const channels = [];
3173
- let maxTime = 0;
3174
- for (const channel of anim.channels) {
3175
- const targetNode = channel.target.node;
3176
- const jointIndex = nodeToJoint.get(targetNode);
3177
- if (jointIndex === void 0)
3178
- continue;
3179
- const path = channel.target.path;
3180
- if (path !== "translation" && path !== "rotation" && path !== "scale")
3181
- continue;
3182
- const sampler = anim.samplers[channel.sampler];
3183
- const interpolation = sampler.interpolation ?? "LINEAR";
3184
- if (interpolation !== "LINEAR" && interpolation !== "STEP")
3185
- continue;
3186
- const inputAccess = getAccessorData(sampler.input);
3187
- const timestamps = new Float32Array(inputAccess.data);
3188
- const outputAccess = getAccessorData(sampler.output);
3189
- const values = new Float32Array(outputAccess.data);
3190
- if (timestamps.length > 0) {
3191
- const lastTime = timestamps[timestamps.length - 1];
3192
- if (lastTime > maxTime)
3193
- maxTime = lastTime;
3194
- }
3195
- channels.push({
3196
- jointIndex,
3197
- path,
3198
- timestamps,
3199
- values,
3200
- interpolation
3201
- });
3202
- }
3203
- if (channels.length > 0) {
3204
- clips.push({
3205
- name: anim.name ?? `animation_${clips.length}`,
3206
- duration: maxTime,
3207
- channels
3208
- });
3209
- }
3210
- }
3211
- return clips;
3212
- }
3213
- function parsePrimitiveSkinAttributes(primitive, getAccessorData) {
3214
- if (primitive.attributes.JOINTS_0 === void 0 || primitive.attributes.WEIGHTS_0 === void 0) {
3215
- return null;
3216
- }
3217
- const jointsAccess = getAccessorData(primitive.attributes.JOINTS_0);
3218
- const weightsAccess = getAccessorData(primitive.attributes.WEIGHTS_0);
3219
- const joints = jointsAccess.data instanceof Uint16Array ? jointsAccess.data : new Uint16Array(jointsAccess.data);
3220
- let weights;
3221
- if (weightsAccess.data instanceof Float32Array) {
3222
- weights = weightsAccess.data;
3223
- } else if (weightsAccess.data instanceof Uint8Array) {
3224
- weights = new Float32Array(weightsAccess.data.length);
3225
- for (let i = 0; i < weightsAccess.data.length; i++) {
3226
- weights[i] = weightsAccess.data[i] / 255;
3227
- }
3228
- } else if (weightsAccess.data instanceof Uint16Array) {
3229
- weights = new Float32Array(weightsAccess.data.length);
3230
- for (let i = 0; i < weightsAccess.data.length; i++) {
3231
- weights[i] = weightsAccess.data[i] / 65535;
3232
- }
3233
- } else {
3234
- weights = new Float32Array(weightsAccess.data);
3235
- }
3236
- return { joints, weights };
3237
- }
3238
- function getNodeTRS(node) {
3239
- const trs = new Float32Array(10);
3240
- if (node.matrix) {
3241
- trs[0] = node.matrix[12];
3242
- trs[1] = node.matrix[13];
3243
- trs[2] = node.matrix[14];
3244
- trs[3] = 0;
3245
- trs[4] = 0;
3246
- trs[5] = 0;
3247
- trs[6] = 1;
3248
- trs[7] = 1;
3249
- trs[8] = 1;
3250
- trs[9] = 1;
3251
- return trs;
3252
- }
3253
- const t = node.translation ?? [0, 0, 0];
3254
- const r = node.rotation ?? [0, 0, 0, 1];
3255
- const s = node.scale ?? [1, 1, 1];
3256
- trs[0] = t[0];
3257
- trs[1] = t[1];
3258
- trs[2] = t[2];
3259
- trs[3] = r[0];
3260
- trs[4] = r[1];
3261
- trs[5] = r[2];
3262
- trs[6] = r[3];
3263
- trs[7] = s[0];
3264
- trs[8] = s[1];
3265
- trs[9] = s[2];
3266
- return trs;
3267
- }
3268
- function createPackedAnimationData() {
3269
- return {
3270
- channels: [],
3271
- keyframeData: [],
3272
- clips: [],
3273
- skins: [],
3274
- parentIndices: [],
3275
- topoOrder: [],
3276
- ibmData: [],
3277
- restTRS: [],
3278
- skelRootMats: [],
3279
- jointChannelLookup: []
3280
- };
3281
- }
3282
- function packSkinAndAnimations(packed, skinData, clips, gltfNodes) {
3283
- const skinIndex = packed.skins.length;
3284
- const jc = skinData.jointCount;
3285
- const parentOffset = packed.parentIndices.length;
3286
- const topoOffset = packed.topoOrder.length;
3287
- const ibmOffset = packed.ibmData.length / 16;
3288
- const restTRSOffset = packed.restTRS.length / 10;
3289
- const skelRootMatIndex = packed.skelRootMats.length / 16;
3290
- for (let j = 0; j < jc; j++) {
3291
- packed.parentIndices.push(skinData.parentJointIndices[j]);
3292
- }
3293
- const visited = new Uint8Array(jc);
3294
- const visit = (j) => {
3295
- if (visited[j])
3296
- return;
3297
- visited[j] = 1;
3298
- const parent = skinData.parentJointIndices[j];
3299
- if (parent !== -1)
3300
- visit(parent);
3301
- packed.topoOrder.push(j);
3302
- };
3303
- for (let j = 0; j < jc; j++)
3304
- visit(j);
3305
- for (let i = 0; i < skinData.inverseBindMatrices.length; i++) {
3306
- packed.ibmData.push(skinData.inverseBindMatrices[i]);
3307
- }
3308
- for (let j = 0; j < jc; j++) {
3309
- const trs = getNodeTRS(gltfNodes[skinData.jointNodeIndices[j]]);
3310
- for (let k = 0; k < 10; k++)
3311
- packed.restTRS.push(trs[k]);
3312
- }
3313
- if (skinData.skeletonRootMatrix) {
3314
- for (let i = 0; i < 16; i++)
3315
- packed.skelRootMats.push(skinData.skeletonRootMatrix[i]);
3316
- } else {
3317
- const id = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
3318
- for (let i = 0; i < 16; i++)
3319
- packed.skelRootMats.push(id[i]);
3320
- }
3321
- const clipOffset = packed.clips.length;
3322
- const jointLookupStart = packed.jointChannelLookup.length;
3323
- packed.skins.push({ jointCount: jc, parentOffset, topoOffset, ibmOffset, restTRSOffset, skelRootMatIndex, clipOffset, jointLookupStart });
3324
- const nodeToJoint = /* @__PURE__ */ new Map();
3325
- for (let j = 0; j < jc; j++) {
3326
- nodeToJoint.set(skinData.jointNodeIndices[j], j);
3327
- }
3328
- for (const clip of clips) {
3329
- const channelStart = packed.channels.length;
3330
- const sortedChannels = [...clip.channels].sort((a, b) => a.jointIndex - b.jointIndex);
3331
- for (const ch of sortedChannels) {
3332
- const pathCode = ch.path === "translation" ? 0 : ch.path === "rotation" ? 1 : 2;
3333
- const isStep = ch.interpolation === "STEP" ? 4 : 0;
3334
- const dataOffset = packed.keyframeData.length;
3335
- const n = ch.timestamps.length;
3336
- const compCount = ch.path === "rotation" ? 4 : 3;
3337
- for (let i = 0; i < n; i++)
3338
- packed.keyframeData.push(ch.timestamps[i]);
3339
- for (let i = 0; i < n * compCount; i++)
3340
- packed.keyframeData.push(ch.values[i]);
3341
- packed.channels.push({
3342
- jointIndex: ch.jointIndex,
3343
- pathAndInterp: pathCode | isStep,
3344
- keyframeCount: n,
3345
- dataOffset
3346
- });
3347
- }
3348
- const clipChannelCount = sortedChannels.length;
3349
- let ci = 0;
3350
- for (let j = 0; j < jc; j++) {
3351
- const lookupStart = ci;
3352
- while (ci < clipChannelCount && sortedChannels[ci].jointIndex === j)
3353
- ci++;
3354
- packed.jointChannelLookup.push(channelStart + lookupStart);
3355
- packed.jointChannelLookup.push(ci - lookupStart);
3356
- }
3357
- packed.clips.push({
3358
- channelStart,
3359
- channelCount: clipChannelCount,
3360
- duration: clip.duration,
3361
- looping: 1
3362
- });
3363
- }
3364
- return skinIndex;
3365
- }
3366
-
3367
- // src/3d/skeletal-animation.ts
3368
- var SkeletalAnimation = class {
3369
- // 16 floats, identity if no non-joint ancestors
3370
- constructor(skinData, clips, gltfNodes) {
3371
- this.clips = [];
3372
- this.clipsByName = /* @__PURE__ */ new Map();
3373
- this._scratchVec3 = new Float32Array(3);
3374
- this._scratchQuat = new Float32Array(4);
3375
- this.skinData = skinData;
3376
- const jc = skinData.jointCount;
3377
- this.localMatrices = new Float32Array(jc * 16);
3378
- this.worldMatrices = new Float32Array(jc * 16);
3379
- this.blendScratch = new Float32Array(jc * 16);
3380
- this.originalRestPoseTRS = new Float32Array(jc * 10);
3381
- this.currentTRS = new Float32Array(jc * 10);
3382
- for (let j = 0; j < jc; j++) {
3383
- const nodeTRS = getNodeTRS(gltfNodes[skinData.jointNodeIndices[j]]);
3384
- this.originalRestPoseTRS.set(nodeTRS, j * 10);
3385
- }
3386
- this.skelRootMat = new Float32Array(16);
3387
- if (skinData.skeletonRootMatrix) {
3388
- this.skelRootMat.set(skinData.skeletonRootMatrix);
3389
- } else {
3390
- this.skelRootMat[0] = 1;
3391
- this.skelRootMat[5] = 1;
3392
- this.skelRootMat[10] = 1;
3393
- this.skelRootMat[15] = 1;
3394
- }
3395
- this.topoOrder = new Uint16Array(jc);
3396
- const visited = new Uint8Array(jc);
3397
- let writeIdx = 0;
3398
- const visit = (j) => {
3399
- if (visited[j])
3400
- return;
3401
- visited[j] = 1;
3402
- const parent = skinData.parentJointIndices[j];
3403
- if (parent !== -1)
3404
- visit(parent);
3405
- this.topoOrder[writeIdx++] = j;
3406
- };
3407
- for (let j = 0; j < jc; j++)
3408
- visit(j);
3409
- for (const clip of clips) {
3410
- this.loadClip(clip);
3411
- }
3412
- }
3413
- loadClip(clip, loop = true) {
3414
- const id = this.clips.length;
3415
- this.clips.push({
3416
- id,
3417
- name: clip.name,
3418
- duration: clip.duration,
3419
- channels: clip.channels,
3420
- loop
3421
- });
3422
- this.clipsByName.set(clip.name, id);
3423
- return id;
3424
- }
3425
- getClipId(name) {
3426
- const id = this.clipsByName.get(name);
3427
- if (id === void 0)
3428
- throw new Error(`Skeletal clip "${name}" not found`);
3429
- return id;
3430
- }
3431
- getClipNames() {
3432
- return this.clips.map((c) => c.name);
3433
- }
3434
- getClip(id) {
3435
- return this.clips[id] ?? null;
3436
- }
3437
- get clipCount() {
3438
- return this.clips.length;
3439
- }
3440
- createState(clipIdOrName, speed = 1, playing = true) {
3441
- const clipId = typeof clipIdOrName === "string" ? this.getClipId(clipIdOrName) : clipIdOrName;
3442
- return { clipId, time: 0, speed, playing, loop: true, prevClipId: -1, prevTime: 0, prevSpeed: 1, blendWeight: 1, blendDuration: 0, onEnd: () => null };
3443
- }
3444
- play(state, clipIdOrName, opts) {
3445
- const newClipId = typeof clipIdOrName === "string" ? this.getClipId(clipIdOrName) : clipIdOrName;
3446
- const crossfade = opts?.crossfade ?? 0;
3447
- if (crossfade > 0 && state.playing) {
3448
- state.prevClipId = state.clipId;
3449
- state.prevTime = state.time;
3450
- state.prevSpeed = state.speed;
3451
- state.blendWeight = 0;
3452
- state.blendDuration = crossfade;
3453
- } else {
3454
- state.prevClipId = -1;
3455
- state.blendWeight = 1;
3456
- state.blendDuration = 0;
3457
- }
3458
- state.onEnd = opts?.onEnd ?? (() => null);
3459
- state.loop = opts?.loop ?? true;
3460
- state.clipId = newClipId;
3461
- state.time = 0;
3462
- state.playing = true;
3463
- if (opts?.speed !== void 0)
3464
- state.speed = opts.speed;
3465
- }
3466
- stop(state) {
3467
- state.playing = false;
3468
- }
3469
- resume(state) {
3470
- state.playing = true;
3471
- }
3472
- /**
3473
- * Advance time and compute bone matrices.
3474
- * Writes jointCount mat4s into `output` starting at float index `outputOffset`.
3475
- * Handles crossfade blending automatically.
3476
- */
3477
- update(state, deltaTime, output, outputOffset = 0) {
3478
- const jc = this.skinData.jointCount;
3479
- if (state.playing) {
3480
- this.advanceTime(state, deltaTime);
3481
- }
3482
- const blending = state.prevClipId !== -1 && state.blendDuration > 0 && state.blendWeight < 1;
3483
- if (blending) {
3484
- state.blendWeight += deltaTime / state.blendDuration;
3485
- if (state.blendWeight >= 1) {
3486
- state.blendWeight = 1;
3487
- state.prevClipId = -1;
3488
- state.blendDuration = 0;
3489
- }
3490
- }
3491
- if (blending && state.prevClipId !== -1) {
3492
- const prevClip = this.clips[state.prevClipId];
3493
- if (prevClip) {
3494
- state.prevTime += deltaTime * state.prevSpeed;
3495
- if (prevClip.loop && state.prevTime >= prevClip.duration) {
3496
- state.prevTime %= prevClip.duration;
3497
- }
3498
- }
3499
- this.evaluateClip(state.prevClipId, state.prevTime, this.blendScratch, 0);
3500
- this.evaluateClip(state.clipId, state.time, output, outputOffset);
3501
- const w = state.blendWeight;
3502
- const oneMinusW = 1 - w;
3503
- const scratch = this.blendScratch;
3504
- const floatCount = jc * 16;
3505
- for (let i = 0; i < floatCount; i++) {
3506
- output[outputOffset + i] = scratch[i] * oneMinusW + output[outputOffset + i] * w;
3507
- }
3508
- } else {
3509
- this.evaluateClip(state.clipId, state.time, output, outputOffset);
3510
- }
3511
- }
3512
- /** Advance clip time with looping/clamping. */
3513
- advanceTime(state, deltaTime) {
3514
- const clip = this.clips[state.clipId];
3515
- if (!clip || clip.duration <= 0)
3516
- return;
3517
- state.time += deltaTime * state.speed;
3518
- if (state.time >= clip.duration) {
3519
- if (clip.loop)
3520
- state.time %= clip.duration;
3521
- else {
3522
- state.time = clip.duration - 1e-4;
3523
- state.playing = false;
3524
- }
3525
- }
3526
- if (state.time < 0) {
3527
- if (clip.loop)
3528
- state.time = clip.duration + state.time % clip.duration;
3529
- else {
3530
- state.time = 0;
3531
- state.playing = false;
3532
- }
3533
- }
3534
- }
3535
- /** Evaluate a clip at a specific time and write bone matrices. */
3536
- evaluateClip(clipId, time, output, outputOffset) {
3537
- const clip = this.clips[clipId];
3538
- if (!clip)
3539
- return;
3540
- const skin = this.skinData;
3541
- const jc = skin.jointCount;
3542
- const local = this.localMatrices;
3543
- const world = this.worldMatrices;
3544
- const cur = this.currentTRS;
3545
- cur.set(this.originalRestPoseTRS);
3546
- for (const ch of clip.channels) {
3547
- const j = ch.jointIndex;
3548
- if (j >= jc)
3549
- continue;
3550
- const ro = j * 10;
3551
- const sampled = this.sampleChannel(ch, time);
3552
- if (ch.path === "translation") {
3553
- cur[ro] = sampled[0];
3554
- cur[ro + 1] = sampled[1];
3555
- cur[ro + 2] = sampled[2];
3556
- } else if (ch.path === "rotation") {
3557
- cur[ro + 3] = sampled[0];
3558
- cur[ro + 4] = sampled[1];
3559
- cur[ro + 5] = sampled[2];
3560
- cur[ro + 6] = sampled[3];
3561
- } else if (ch.path === "scale") {
3562
- cur[ro + 7] = sampled[0];
3563
- cur[ro + 8] = sampled[1];
3564
- cur[ro + 9] = sampled[2];
3565
- }
3566
- }
3567
- for (let j = 0; j < jc; j++) {
3568
- const ro = j * 10;
3569
- trsToMat4(
3570
- cur[ro],
3571
- cur[ro + 1],
3572
- cur[ro + 2],
3573
- cur[ro + 3],
3574
- cur[ro + 4],
3575
- cur[ro + 5],
3576
- cur[ro + 6],
3577
- cur[ro + 7],
3578
- cur[ro + 8],
3579
- cur[ro + 9],
3580
- local,
3581
- j * 16
3582
- );
3583
- }
3584
- const topo = this.topoOrder;
3585
- const srm = this.skelRootMat;
3586
- for (let i = 0; i < jc; i++) {
3587
- const j = topo[i];
3588
- const parentJ = skin.parentJointIndices[j];
3589
- if (parentJ === -1) {
3590
- mat4Mul(srm, 0, local, j * 16, world, j * 16);
3591
- } else {
3592
- mat4Mul(world, parentJ * 16, local, j * 16, world, j * 16);
3593
- }
3594
- }
3595
- const ibm = skin.inverseBindMatrices;
3596
- for (let j = 0; j < jc; j++) {
3597
- mat4Mul(world, j * 16, ibm, j * 16, output, outputOffset + j * 16);
3598
- }
3599
- }
3600
- /**
3601
- * Compute bone matrices for the rest/bind pose (no animation).
3602
- */
3603
- computeRestPose(output, outputOffset = 0) {
3604
- const skin = this.skinData;
3605
- const jc = skin.jointCount;
3606
- const local = this.localMatrices;
3607
- const world = this.worldMatrices;
3608
- const rest = this.originalRestPoseTRS;
3609
- for (let j = 0; j < jc; j++) {
3610
- const ro = j * 10;
3611
- trsToMat4(
3612
- rest[ro],
3613
- rest[ro + 1],
3614
- rest[ro + 2],
3615
- rest[ro + 3],
3616
- rest[ro + 4],
3617
- rest[ro + 5],
3618
- rest[ro + 6],
3619
- rest[ro + 7],
3620
- rest[ro + 8],
3621
- rest[ro + 9],
3622
- local,
3623
- j * 16
3624
- );
3625
- }
3626
- const topo = this.topoOrder;
3627
- const srm = this.skelRootMat;
3628
- for (let i = 0; i < jc; i++) {
3629
- const j = topo[i];
3630
- const parentJ = skin.parentJointIndices[j];
3631
- if (parentJ === -1) {
3632
- mat4Mul(srm, 0, local, j * 16, world, j * 16);
3633
- } else {
3634
- mat4Mul(world, parentJ * 16, local, j * 16, world, j * 16);
3635
- }
3636
- }
3637
- const ibm = skin.inverseBindMatrices;
3638
- for (let j = 0; j < jc; j++) {
3639
- mat4Mul(world, j * 16, ibm, j * 16, output, outputOffset + j * 16);
3640
- }
3641
- }
3642
- // --- Private helpers (zero-alloc, operate on Float32Array sub-regions) ---
3643
- /** Sample an animation channel at time t. Returns 3 or 4 floats. */
3644
- sampleChannel(ch, t) {
3645
- const ts = ch.timestamps;
3646
- const vals = ch.values;
3647
- const compCount = ch.path === "rotation" ? 4 : 3;
3648
- if (t <= ts[0])
3649
- return vals.subarray(0, compCount);
3650
- if (t >= ts[ts.length - 1])
3651
- return vals.subarray((ts.length - 1) * compCount, ts.length * compCount);
3652
- let lo = 0, hi = ts.length - 1;
3653
- while (lo < hi - 1) {
3654
- const mid = lo + hi >> 1;
3655
- if (ts[mid] <= t)
3656
- lo = mid;
3657
- else
3658
- hi = mid;
3659
- }
3660
- if (ch.interpolation === "STEP") {
3661
- return vals.subarray(lo * compCount, lo * compCount + compCount);
3662
- }
3663
- const t0 = ts[lo], t1 = ts[hi];
3664
- const f = t1 > t0 ? (t - t0) / (t1 - t0) : 0;
3665
- const a = lo * compCount;
3666
- const b = hi * compCount;
3667
- if (ch.path === "rotation") {
3668
- return this.nlerpQuat(vals, a, vals, b, f);
3669
- }
3670
- const out = this._scratchVec3;
3671
- out[0] = vals[a] + (vals[b] - vals[a]) * f;
3672
- out[1] = vals[a + 1] + (vals[b + 1] - vals[a + 1]) * f;
3673
- out[2] = vals[a + 2] + (vals[b + 2] - vals[a + 2]) * f;
3674
- return out;
3675
- }
3676
- /** Normalized lerp for quaternions (nlerp). */
3677
- nlerpQuat(a, ao, b, bo, t) {
3678
- const out = this._scratchQuat;
3679
- let dot2 = a[ao] * b[bo] + a[ao + 1] * b[bo + 1] + a[ao + 2] * b[bo + 2] + a[ao + 3] * b[bo + 3];
3680
- const sign = dot2 < 0 ? -1 : 1;
3681
- const oneMinusT = 1 - t;
3682
- out[0] = oneMinusT * a[ao] + t * b[bo] * sign;
3683
- out[1] = oneMinusT * a[ao + 1] + t * b[bo + 1] * sign;
3684
- out[2] = oneMinusT * a[ao + 2] + t * b[bo + 2] * sign;
3685
- out[3] = oneMinusT * a[ao + 3] + t * b[bo + 3] * sign;
3686
- const len = Math.sqrt(out[0] * out[0] + out[1] * out[1] + out[2] * out[2] + out[3] * out[3]);
3687
- if (len > 0) {
3688
- const inv = 1 / len;
3689
- out[0] *= inv;
3690
- out[1] *= inv;
3691
- out[2] *= inv;
3692
- out[3] *= inv;
3693
- }
3694
- return out;
3695
- }
3696
- };
3149
+ // src/3d/renderer.ts
3150
+ import {
3151
+ createPackedAnimationData,
3152
+ packSkinAndAnimations,
3153
+ parseGltf,
3154
+ SkeletalAnimation
3155
+ } from "murow/renderer";
3697
3156
 
3698
3157
  // src/3d/skeletal-animation-compute/packer.ts
3699
3158
  function packAnimationData(packed) {
@@ -3775,14 +3234,14 @@ function packAnimationData(packed) {
3775
3234
 
3776
3235
  // src/3d/skeletal-animation-compute/kernel.ts
3777
3236
  var WORKGROUP_SIZE = 64;
3778
- function buildAnimationKernel(root, packed, maxInstances, maxTotalBones) {
3237
+ function buildAnimationKernel(root, packed, maxInstances, maxTotalBones, budgets) {
3779
3238
  const pb = packAnimationData(packed);
3780
3239
  const kernel = new ComputeBuilder("skeletal-animation", { workgroupSize: WORKGROUP_SIZE }, root).buffers({
3781
3240
  uniforms: { uniform: AnimComputeUniforms },
3782
3241
  instances: { storage: d.arrayOf(InstanceAnimStateGPU, maxInstances) },
3783
- skelI32: { storage: d.arrayOf(d.i32, pb.skelI32.length) },
3784
- animF32: { storage: d.arrayOf(d.f32, pb.animF32.length) },
3785
- matrices: { storage: d.arrayOf(d.mat4x4f, pb.totalMats) },
3242
+ skelI32: { storage: d.arrayOf(d.i32, budgets.skelI32Capacity) },
3243
+ animF32: { storage: d.arrayOf(d.f32, budgets.animF32Capacity) },
3244
+ matrices: { storage: d.arrayOf(d.mat4x4f, budgets.matricesCapacity) },
3786
3245
  boneMatrices: { storage: d.arrayOf(d.mat4x4f, maxTotalBones), readwrite: true }
3787
3246
  }).shader(({ uniforms, instances, skelI32, animF32, matrices, boneMatrices }, { globalId }) => {
3788
3247
  const idx = globalId.x;
@@ -4124,13 +3583,50 @@ function buildAnimationKernel(root, packed, maxInstances, maxTotalBones) {
4124
3583
  }
4125
3584
  }
4126
3585
  }).build();
4127
- kernel.write("skelI32", Array.from(pb.skelI32));
4128
- kernel.write("animF32", Array.from(pb.animF32));
4129
- const matBuffer = kernel.getBuffer("matrices");
4130
- const rawMatBuffer = root.unwrap(matBuffer);
4131
- root.device.queue.writeBuffer(rawMatBuffer, 0, pb.matFloats);
4132
- return { kernel, packedBuffers: pb };
3586
+ uploadPackedToKernel(root, kernel, pb);
3587
+ return { kernel, packedBuffers: pb, budgets };
4133
3588
  }
3589
+ function uploadPackedToKernel(root, kernel, pb) {
3590
+ const queue = root.device.queue;
3591
+ const skelBuf = root.unwrap(kernel.getBuffer("skelI32"));
3592
+ queue.writeBuffer(skelBuf, 0, pb.skelI32);
3593
+ const animBuf = root.unwrap(kernel.getBuffer("animF32"));
3594
+ queue.writeBuffer(animBuf, 0, pb.animF32);
3595
+ const matBuf = root.unwrap(kernel.getBuffer("matrices"));
3596
+ queue.writeBuffer(matBuf, 0, pb.matFloats);
3597
+ }
3598
+
3599
+ // src/3d/clip-resync-coordinator.ts
3600
+ var GltfClipResyncCoordinator = class {
3601
+ constructor(bucket) {
3602
+ this.bucket = bucket;
3603
+ this.skinIndexByPrefabId = /* @__PURE__ */ new Map();
3604
+ this._pending = /* @__PURE__ */ new Set();
3605
+ this.bucket.events.on("clips-changed", ({ prefabId }) => {
3606
+ const skinIndex = this.skinIndexByPrefabId.get(prefabId);
3607
+ if (skinIndex !== void 0)
3608
+ this._pending.add(skinIndex);
3609
+ });
3610
+ }
3611
+ /** Map a prefab id to its index in the renderer's `skinnedModels` list. */
3612
+ registerSkin(prefabId, skinIndex) {
3613
+ this.skinIndexByPrefabId.set(prefabId, skinIndex);
3614
+ }
3615
+ /** Skin indices whose clip set has changed since the last `clear()`. */
3616
+ get pending() {
3617
+ return this._pending;
3618
+ }
3619
+ clear() {
3620
+ this._pending.clear();
3621
+ }
3622
+ /** Unsubscribe from the bucket and reset its animation clip sets. */
3623
+ dispose() {
3624
+ this.bucket.events.clear("clips-changed");
3625
+ this.bucket.resetAnimations();
3626
+ this.skinIndexByPrefabId.clear();
3627
+ this._pending.clear();
3628
+ }
3629
+ };
4134
3630
 
4135
3631
  // src/3d/renderer.ts
4136
3632
  var DYN_PREV_PX = 0;
@@ -4158,9 +3654,44 @@ var SSTAT_CR = 3;
4158
3654
  var SSTAT_CG = 4;
4159
3655
  var SSTAT_CB = 5;
4160
3656
  var SSTAT_BONE_OFFSET = 6;
3657
+ var prefabHandles = /* @__PURE__ */ new WeakMap();
3658
+ function isPrefab3D(value) {
3659
+ return value.type === "gltf" || value.type === "grid";
3660
+ }
3661
+ function resolveTransform(opts) {
3662
+ const [px, py, pz] = opts.position ?? [0, 0, 0];
3663
+ const [rx, ry, rz] = opts.rotation ?? [0, 0, 0];
3664
+ const s = opts.scale;
3665
+ const [sx, sy, sz] = typeof s === "number" ? [s, s, s] : s ?? [1, 1, 1];
3666
+ const [cr, cg, cb] = opts.color ?? [1, 1, 1];
3667
+ return { px, py, pz, rx, ry, rz, sx, sy, sz, cr, cg, cb };
3668
+ }
3669
+ function resolvePrefabHandle(prefab) {
3670
+ const h = prefabHandles.get(prefab);
3671
+ if (!h) {
3672
+ throw new Error(
3673
+ `Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
3674
+ );
3675
+ }
3676
+ return h;
3677
+ }
3678
+ function computeBucketStats(bucket) {
3679
+ let maxSkinnedParts = 0;
3680
+ let maxJointCount = 0;
3681
+ for (const prefab of bucket.entries()) {
3682
+ if (prefab.type === "gltf") {
3683
+ if (prefab.skinnedPartCount > maxSkinnedParts)
3684
+ maxSkinnedParts = prefab.skinnedPartCount;
3685
+ if (prefab.jointCount > maxJointCount)
3686
+ maxJointCount = prefab.jointCount;
3687
+ }
3688
+ }
3689
+ return { maxSkinnedParts, maxJointCount };
3690
+ }
4161
3691
  var WebGPU3DRenderer = class extends Base3DRenderer {
4162
3692
  constructor(canvas, options) {
4163
- super(canvas, options);
3693
+ const resolvedMaxModels = options.maxModels ?? (options.prefabs ? options.prefabs.size + 16 : 32);
3694
+ super(canvas, { ...options, maxModels: resolvedMaxModels });
4164
3695
  this.resizeObserver = null;
4165
3696
  this.resizeCallbacks = [];
4166
3697
  // layer=0, sheetId=modelId
@@ -4170,11 +3701,15 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4170
3701
  this.nextModelId = 0;
4171
3702
  // Skinned model data
4172
3703
  this.skinnedModels = [];
3704
+ // Null when constructed without a `prefabs` bucket; lazy load/unload requires it as event source.
3705
+ this.clipResync = null;
4173
3706
  this.boneMatrixDirty = true;
4174
3707
  // GPU animation compute
4175
3708
  this.packedAnimData = createPackedAnimationData();
4176
3709
  this.animComputeKernel = null;
4177
3710
  this.animComputeNeedsRebuild = false;
3711
+ // Capacities the active kernel was built with — used to gate the in-place upload path on resync.
3712
+ this.animKernelBudgets = null;
4178
3713
  this.animClipTableOffset = 0;
4179
3714
  this.animChannelTableOffset = 0;
4180
3715
  this.animJointLookupOffset = 0;
@@ -4188,16 +3723,20 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4188
3723
  // mat4x4 (16) + alpha (1) + lightDir (3) + padding (4)
4189
3724
  this.lastRenderTime = 0;
4190
3725
  this.camera = new Camera3D();
4191
- this.maxSkinnedInstances = options.maxSkinnedInstances ?? 5e3;
4192
- this.maxBonesPerSkin = options.maxBonesPerSkin ?? 64;
3726
+ this._prefabs = options.prefabs ?? null;
3727
+ const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
3728
+ const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
3729
+ const maxInstances = options.maxInstances ?? resolvedMaxModels;
3730
+ this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? maxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
3731
+ this.maxBonesPerSkin = options.maxBonesPerSkin ?? (bucketStats ? Math.max(1, bucketStats.maxJointCount) : 64);
4193
3732
  this.maxTotalBones = this.maxSkinnedInstances * this.maxBonesPerSkin * 2;
4194
3733
  this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
4195
- this.freeList = new FreeList3(options.maxModels);
4196
- this.batcher = new SparseBatcher2(options.maxModels);
4197
- this.dynamicData = new Float32Array(options.maxModels * DYNAMIC_MESH_FLOATS);
4198
- this.staticData = new Float32Array(options.maxModels * STATIC_MESH_FLOATS);
4199
- this.slotIndexData = new Uint32Array(options.maxModels);
4200
- this.instanceModelIds = new Uint8Array(options.maxModels);
3734
+ this.freeList = new FreeList3(resolvedMaxModels);
3735
+ this.batcher = new SparseBatcher2(resolvedMaxModels);
3736
+ this.dynamicData = new Float32Array(resolvedMaxModels * DYNAMIC_MESH_FLOATS);
3737
+ this.staticData = new Float32Array(resolvedMaxModels * STATIC_MESH_FLOATS);
3738
+ this.slotIndexData = new Uint32Array(resolvedMaxModels);
3739
+ this.instanceModelIds = new Uint8Array(resolvedMaxModels);
4201
3740
  const msi = this.maxSkinnedInstances;
4202
3741
  this.skinnedFreeList = new FreeList3(msi);
4203
3742
  this.skinnedBatcher = new SparseBatcher2(msi);
@@ -4353,9 +3892,38 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4353
3892
  { binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } }
4354
3893
  ]
4355
3894
  });
3895
+ if (this._prefabs) {
3896
+ this.uploadPrefabBucket(this._prefabs);
3897
+ }
4356
3898
  this.setupResizeObserver();
4357
3899
  this._initialized = true;
4358
3900
  }
3901
+ /**
3902
+ * Upload every prefab in the bucket to the GPU and stash the handle on
3903
+ * each prefab so `bucket.get(id)` resolves to a usable model. Also
3904
+ * subscribes the resync coordinator to the bucket's `clips-changed`
3905
+ * channel for lazy load/unload.
3906
+ */
3907
+ uploadPrefabBucket(bucket) {
3908
+ this.clipResync = new GltfClipResyncCoordinator(bucket);
3909
+ for (const prefab of bucket.entries()) {
3910
+ if (prefab.type === "gltf") {
3911
+ const beforeSkinCount = this.skinnedModels.length;
3912
+ const model = this.uploadParsedGltf(prefab.parsed);
3913
+ prefabHandles.set(prefab, model);
3914
+ if (this.skinnedModels.length > beforeSkinCount) {
3915
+ this.clipResync.registerSkin(prefab.id, beforeSkinCount);
3916
+ }
3917
+ } else if (prefab.type === "grid") {
3918
+ const model = this.createGrid({
3919
+ size: prefab.size,
3920
+ step: prefab.step,
3921
+ lineWidth: prefab.lineWidth
3922
+ });
3923
+ prefabHandles.set(prefab, model);
3924
+ }
3925
+ }
3926
+ }
4359
3927
  setupResizeObserver() {
4360
3928
  const supportsDevicePixelBox = (() => {
4361
3929
  try {
@@ -4689,108 +4257,20 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4689
4257
  * ```
4690
4258
  */
4691
4259
  async loadGltf(url, opts) {
4692
- const response = await fetch(url);
4693
- const baseUrl = url.substring(0, url.lastIndexOf("/") + 1);
4694
- let gltf;
4695
- let glbBinaryChunk = null;
4696
- const arrayBuffer = await response.arrayBuffer();
4697
- const magic = new Uint32Array(arrayBuffer, 0, 1)[0];
4698
- if (magic === 1179937895) {
4699
- let offset = 12;
4700
- while (offset < arrayBuffer.byteLength) {
4701
- const chunkLength = new Uint32Array(arrayBuffer, offset, 1)[0];
4702
- const chunkType = new Uint32Array(arrayBuffer, offset + 4, 1)[0];
4703
- offset += 8;
4704
- if (chunkType === 1313821514) {
4705
- const jsonBytes = new Uint8Array(arrayBuffer, offset, chunkLength);
4706
- gltf = JSON.parse(new TextDecoder().decode(jsonBytes));
4707
- } else if (chunkType === 5130562) {
4708
- glbBinaryChunk = arrayBuffer.slice(offset, offset + chunkLength);
4709
- }
4710
- offset += chunkLength;
4711
- }
4712
- if (!gltf)
4713
- throw new Error(`Invalid GLB: no JSON chunk in ${url}`);
4714
- } else {
4715
- gltf = JSON.parse(new TextDecoder().decode(arrayBuffer));
4716
- }
4717
- if (!gltf.meshes?.length)
4718
- throw new Error(`No meshes found in ${url}`);
4719
- const buffers = [];
4720
- for (let i = 0; i < (gltf.buffers?.length ?? 0); i++) {
4721
- const buf = gltf.buffers[i];
4722
- if (glbBinaryChunk && (!buf.uri || buf.uri === "")) {
4723
- buffers.push(glbBinaryChunk);
4724
- } else if (buf.uri) {
4725
- const r = await fetch(baseUrl + buf.uri);
4726
- buffers.push(await r.arrayBuffer());
4727
- }
4728
- }
4729
- const getAccessorData = (accessorIndex) => {
4730
- const accessor = gltf.accessors[accessorIndex];
4731
- const bufferView = gltf.bufferViews[accessor.bufferView];
4732
- const buffer = buffers[bufferView.buffer];
4733
- const typeMap = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array };
4734
- const byteSizeMap = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
4735
- const sizeMap = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
4736
- const TypedArray = typeMap[accessor.componentType];
4737
- const componentBytes = byteSizeMap[accessor.componentType];
4738
- const elementSize = sizeMap[accessor.type] ?? 1;
4739
- const baseOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
4740
- const stride = bufferView.byteStride ?? componentBytes * elementSize;
4741
- const tightStride = componentBytes * elementSize;
4742
- if (stride === tightStride) {
4743
- const data = new TypedArray(buffer, baseOffset, accessor.count * elementSize);
4744
- return { data, count: accessor.count, elementSize };
4745
- }
4746
- const out = new TypedArray(accessor.count * elementSize);
4747
- const src = new Uint8Array(buffer);
4748
- const dst = new Uint8Array(out.buffer);
4749
- for (let i = 0; i < accessor.count; i++) {
4750
- const srcOff = baseOffset + i * stride;
4751
- const dstOff = i * tightStride;
4752
- for (let b = 0; b < tightStride; b++) {
4753
- dst[dstOff + b] = src[srcOff + b];
4754
- }
4755
- }
4756
- return { data: out, count: accessor.count, elementSize };
4757
- };
4758
- const textureCache = /* @__PURE__ */ new Map();
4759
- const loadTexture = async (imageIndex) => {
4760
- if (textureCache.has(imageIndex))
4761
- return textureCache.get(imageIndex);
4762
- const image = gltf.images?.[imageIndex];
4763
- if (!image)
4764
- return void 0;
4765
- let blob;
4766
- if (image.bufferView !== void 0) {
4767
- const bv = gltf.bufferViews[image.bufferView];
4768
- const buf = buffers[bv.buffer];
4769
- const data = new Uint8Array(buf, bv.byteOffset ?? 0, bv.byteLength);
4770
- blob = new Blob([data], { type: image.mimeType ?? "image/png" });
4771
- } else if (image.uri) {
4772
- const imgUrl = image.uri.startsWith("data:") ? image.uri : baseUrl + image.uri;
4773
- blob = await (await fetch(imgUrl)).blob();
4774
- }
4775
- if (blob) {
4776
- const bmp = await createImageBitmap(blob);
4777
- textureCache.set(imageIndex, bmp);
4778
- return bmp;
4779
- }
4780
- return void 0;
4781
- };
4782
- const meshNodeIndex = gltf.nodes?.findIndex((n) => n.mesh !== void 0) ?? -1;
4783
- const skinIndex = meshNodeIndex !== -1 ? gltf.nodes?.[meshNodeIndex]?.skin : void 0;
4784
- let skinData = null;
4785
- let animClips = [];
4260
+ const parsed = await parseGltf(url, opts);
4261
+ return this.uploadParsedGltf(parsed);
4262
+ }
4263
+ /**
4264
+ * Upload a previously-parsed glTF to the GPU. Returns a GltfModel handle.
4265
+ * Splitting parse (CPU) from upload (GPU) lets callers parse models in parallel
4266
+ * before a renderer exists and inspect joint counts / vertex totals to size the
4267
+ * renderer appropriately.
4268
+ */
4269
+ uploadParsedGltf(parsed) {
4786
4270
  let skinnedModelSkinIndex = -1;
4787
- if (skinIndex !== void 0 && gltf.skins?.[skinIndex]) {
4788
- skinData = parseSkin(gltf, skinIndex, getAccessorData);
4789
- animClips = parseAnimations(gltf, skinData, getAccessorData);
4790
- if (opts?.animations) {
4791
- animClips = animClips.filter((clip) => opts.animations.includes(clip.name));
4792
- }
4793
- const animation = new SkeletalAnimation(skinData, animClips, gltf.nodes);
4271
+ if (parsed.skin) {
4272
+ const { data: skinData, animClips } = parsed.skin;
4273
+ const animation = new SkeletalAnimation(skinData, animClips);
4794
4274
  const ibm = skinData.inverseBindMatrices;
4795
4275
  let maxRadSq = 0;
4796
4276
  for (let j = 0; j < skinData.jointCount; j++) {
@@ -4804,91 +4284,34 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4804
4284
  this.skinnedModels.push({
4805
4285
  animation,
4806
4286
  jointCount: skinData.jointCount,
4807
- boundingRadius: skinnedRadius
4287
+ boundingRadius: skinnedRadius,
4288
+ parsedSkin: parsed.skin
4808
4289
  });
4809
- packSkinAndAnimations(this.packedAnimData, skinData, animClips, gltf.nodes);
4290
+ packSkinAndAnimations(this.packedAnimData, skinData, animClips);
4810
4291
  this.animComputeNeedsRebuild = true;
4811
4292
  }
4812
4293
  const handles = [];
4813
- const meshNodeIndices = [];
4814
- for (let i = 0; i < gltf.nodes.length; i++) {
4815
- if (gltf.nodes[i].mesh !== void 0)
4816
- meshNodeIndices.push(i);
4817
- }
4818
- const meshIndicesToLoad = meshNodeIndices.length > 0 ? meshNodeIndices.map((ni) => gltf.nodes[ni].mesh) : [0];
4819
- for (const meshIdx of meshIndicesToLoad) {
4820
- const mesh = gltf.meshes[meshIdx];
4821
- if (!mesh)
4822
- continue;
4823
- const meshNodeForThis = gltf.nodes.find((n) => n.mesh === meshIdx);
4824
- const meshSkinIndex = meshNodeForThis?.skin;
4825
- const isSkinned = skinData && meshSkinIndex !== void 0;
4826
- let thisMeshNodeMatrix = null;
4827
- if (meshNodeForThis && !isSkinned) {
4828
- if (meshNodeForThis.scale || meshNodeForThis.rotation || meshNodeForThis.translation || meshNodeForThis.matrix) {
4829
- thisMeshNodeMatrix = nodeToMat4(meshNodeForThis);
4830
- }
4831
- }
4832
- for (const primitive of mesh.primitives) {
4833
- const posAccess = getAccessorData(primitive.attributes.POSITION);
4834
- const positions = new Float32Array(posAccess.data);
4835
- let normals;
4836
- if (primitive.attributes.NORMAL !== void 0) {
4837
- normals = new Float32Array(getAccessorData(primitive.attributes.NORMAL).data);
4838
- }
4839
- let uvs;
4840
- if (primitive.attributes.TEXCOORD_0 !== void 0) {
4841
- uvs = new Float32Array(getAccessorData(primitive.attributes.TEXCOORD_0).data);
4842
- }
4843
- let indices;
4844
- if (primitive.indices !== void 0) {
4845
- const idxAccess = getAccessorData(primitive.indices);
4846
- indices = idxAccess.data.length > 65535 ? new Uint32Array(idxAccess.data) : new Uint16Array(idxAccess.data);
4847
- }
4848
- if (thisMeshNodeMatrix && !isSkinned) {
4849
- const meshNodeMatrix = thisMeshNodeMatrix;
4850
- const mm = meshNodeMatrix;
4851
- const vertexCount = positions.length / 3;
4852
- for (let v = 0; v < vertexCount; v++) {
4853
- const o = v * 3;
4854
- const px = positions[o], py = positions[o + 1], pz = positions[o + 2];
4855
- positions[o] = mm[0] * px + mm[4] * py + mm[8] * pz + mm[12];
4856
- positions[o + 1] = mm[1] * px + mm[5] * py + mm[9] * pz + mm[13];
4857
- positions[o + 2] = mm[2] * px + mm[6] * py + mm[10] * pz + mm[14];
4858
- if (normals) {
4859
- const nx = normals[o], ny = normals[o + 1], nz = normals[o + 2];
4860
- const tnx = mm[0] * nx + mm[4] * ny + mm[8] * nz;
4861
- const tny = mm[1] * nx + mm[5] * ny + mm[9] * nz;
4862
- const tnz = mm[2] * nx + mm[6] * ny + mm[10] * nz;
4863
- const len = Math.sqrt(tnx * tnx + tny * tny + tnz * tnz);
4864
- if (len > 0) {
4865
- normals[o] = tnx / len;
4866
- normals[o + 1] = tny / len;
4867
- normals[o + 2] = tnz / len;
4868
- }
4869
- }
4870
- }
4871
- }
4872
- let texture;
4873
- if (primitive.material !== void 0) {
4874
- const material = gltf.materials?.[primitive.material];
4875
- const texIndex = material?.pbrMetallicRoughness?.baseColorTexture?.index;
4876
- if (texIndex !== void 0 && gltf.textures?.[texIndex]) {
4877
- texture = await loadTexture(gltf.textures[texIndex].source);
4878
- }
4879
- }
4880
- if (isSkinned) {
4881
- const skinAttrs = parsePrimitiveSkinAttributes(primitive, getAccessorData);
4882
- if (skinAttrs) {
4883
- handles.push(this.loadSkinnedModel(
4884
- { positions, normals, uvs, indices, texture },
4885
- skinAttrs,
4886
- skinnedModelSkinIndex
4887
- ));
4888
- continue;
4889
- }
4890
- }
4891
- handles.push(this.loadModel({ positions, normals, uvs, indices, texture }));
4294
+ for (const prim of parsed.primitives) {
4295
+ if (prim.skinned && prim.skinAttrs) {
4296
+ handles.push(this.loadSkinnedModel(
4297
+ {
4298
+ positions: prim.positions,
4299
+ normals: prim.normals,
4300
+ uvs: prim.uvs,
4301
+ indices: prim.indices,
4302
+ texture: prim.texture
4303
+ },
4304
+ prim.skinAttrs,
4305
+ skinnedModelSkinIndex
4306
+ ));
4307
+ } else {
4308
+ handles.push(this.loadModel({
4309
+ positions: prim.positions,
4310
+ normals: prim.normals,
4311
+ uvs: prim.uvs,
4312
+ indices: prim.indices,
4313
+ texture: prim.texture
4314
+ }));
4892
4315
  }
4893
4316
  }
4894
4317
  let totalVertexCount = 0;
@@ -4900,7 +4323,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4900
4323
  totalVertexCount,
4901
4324
  skinned: handles.some((h) => h.skinned),
4902
4325
  animations: animNames,
4903
- src: url
4326
+ src: parsed.src
4904
4327
  };
4905
4328
  }
4906
4329
  /**
@@ -4908,7 +4331,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4908
4331
  * with another instance (e.g., when spawning all parts of a character).
4909
4332
  */
4910
4333
  addInstance(opts) {
4911
- const modelOrGltf = opts.model;
4334
+ const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
4912
4335
  if ("parts" in modelOrGltf) {
4913
4336
  return this.addGltfInstance(opts, modelOrGltf);
4914
4337
  }
@@ -4922,27 +4345,25 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4922
4345
  throw new Error(`Max instances (${this.maxModels}) reached`);
4923
4346
  const dynBase = slot * DYNAMIC_MESH_FLOATS;
4924
4347
  const statBase = slot * STATIC_MESH_FLOATS;
4925
- const x = opts.x ?? 0, y = opts.y ?? 0, z = opts.z ?? 0;
4926
- this.dynamicData[dynBase + DYN_PREV_PX] = x;
4927
- this.dynamicData[dynBase + DYN_PREV_PY] = y;
4928
- this.dynamicData[dynBase + DYN_PREV_PZ] = z;
4929
- this.dynamicData[dynBase + DYN_CURR_PX] = x;
4930
- this.dynamicData[dynBase + DYN_CURR_PY] = y;
4931
- this.dynamicData[dynBase + DYN_CURR_PZ] = z;
4932
- const rx = opts.rotX ?? 0, ry = opts.rotY ?? 0, rz = opts.rotZ ?? 0;
4933
- this.dynamicData[dynBase + DYN_PREV_RX] = rx;
4934
- this.dynamicData[dynBase + DYN_PREV_RY] = ry;
4935
- this.dynamicData[dynBase + DYN_PREV_RZ] = rz;
4936
- this.dynamicData[dynBase + DYN_CURR_RX] = rx;
4937
- this.dynamicData[dynBase + DYN_CURR_RY] = ry;
4938
- this.dynamicData[dynBase + DYN_CURR_RZ] = rz;
4939
- this.staticData[statBase + STAT_SX] = opts.scaleX ?? 1;
4940
- this.staticData[statBase + STAT_SY] = opts.scaleY ?? 1;
4941
- this.staticData[statBase + STAT_SZ] = opts.scaleZ ?? 1;
4942
- const color = opts.color ?? [1, 1, 1];
4943
- this.staticData[statBase + STAT_CR] = color[0];
4944
- this.staticData[statBase + STAT_CG] = color[1];
4945
- this.staticData[statBase + STAT_CB] = color[2];
4348
+ const t = resolveTransform(opts);
4349
+ this.dynamicData[dynBase + DYN_PREV_PX] = t.px;
4350
+ this.dynamicData[dynBase + DYN_PREV_PY] = t.py;
4351
+ this.dynamicData[dynBase + DYN_PREV_PZ] = t.pz;
4352
+ this.dynamicData[dynBase + DYN_CURR_PX] = t.px;
4353
+ this.dynamicData[dynBase + DYN_CURR_PY] = t.py;
4354
+ this.dynamicData[dynBase + DYN_CURR_PZ] = t.pz;
4355
+ this.dynamicData[dynBase + DYN_PREV_RX] = t.rx;
4356
+ this.dynamicData[dynBase + DYN_PREV_RY] = t.ry;
4357
+ this.dynamicData[dynBase + DYN_PREV_RZ] = t.rz;
4358
+ this.dynamicData[dynBase + DYN_CURR_RX] = t.rx;
4359
+ this.dynamicData[dynBase + DYN_CURR_RY] = t.ry;
4360
+ this.dynamicData[dynBase + DYN_CURR_RZ] = t.rz;
4361
+ this.staticData[statBase + STAT_SX] = t.sx;
4362
+ this.staticData[statBase + STAT_SY] = t.sy;
4363
+ this.staticData[statBase + STAT_SZ] = t.sz;
4364
+ this.staticData[statBase + STAT_CR] = t.cr;
4365
+ this.staticData[statBase + STAT_CG] = t.cg;
4366
+ this.staticData[statBase + STAT_CB] = t.cb;
4946
4367
  this.staticDirty = true;
4947
4368
  this.instanceModelIds[slot] = modelHandle.id;
4948
4369
  this.batcher.add(0, modelHandle.id, slot);
@@ -5028,27 +4449,25 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5028
4449
  this.skinnedAnimStates[slot] = animState;
5029
4450
  const dynBase = slot * DYNAMIC_MESH_FLOATS;
5030
4451
  const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
5031
- const x = opts.x ?? 0, y = opts.y ?? 0, z = opts.z ?? 0;
5032
- this.skinnedDynamicData[dynBase + DYN_PREV_PX] = x;
5033
- this.skinnedDynamicData[dynBase + DYN_PREV_PY] = y;
5034
- this.skinnedDynamicData[dynBase + DYN_PREV_PZ] = z;
5035
- this.skinnedDynamicData[dynBase + DYN_CURR_PX] = x;
5036
- this.skinnedDynamicData[dynBase + DYN_CURR_PY] = y;
5037
- this.skinnedDynamicData[dynBase + DYN_CURR_PZ] = z;
5038
- const rx = opts.rotX ?? 0, ry = opts.rotY ?? 0, rz = opts.rotZ ?? 0;
5039
- this.skinnedDynamicData[dynBase + DYN_PREV_RX] = rx;
5040
- this.skinnedDynamicData[dynBase + DYN_PREV_RY] = ry;
5041
- this.skinnedDynamicData[dynBase + DYN_PREV_RZ] = rz;
5042
- this.skinnedDynamicData[dynBase + DYN_CURR_RX] = rx;
5043
- this.skinnedDynamicData[dynBase + DYN_CURR_RY] = ry;
5044
- this.skinnedDynamicData[dynBase + DYN_CURR_RZ] = rz;
5045
- this.skinnedStaticData[statBase + SSTAT_SX] = opts.scaleX ?? 1;
5046
- this.skinnedStaticData[statBase + SSTAT_SY] = opts.scaleY ?? 1;
5047
- this.skinnedStaticData[statBase + SSTAT_SZ] = opts.scaleZ ?? 1;
5048
- const color = opts.color ?? [1, 1, 1];
5049
- this.skinnedStaticData[statBase + SSTAT_CR] = color[0];
5050
- this.skinnedStaticData[statBase + SSTAT_CG] = color[1];
5051
- this.skinnedStaticData[statBase + SSTAT_CB] = color[2];
4452
+ const t = resolveTransform(opts);
4453
+ this.skinnedDynamicData[dynBase + DYN_PREV_PX] = t.px;
4454
+ this.skinnedDynamicData[dynBase + DYN_PREV_PY] = t.py;
4455
+ this.skinnedDynamicData[dynBase + DYN_PREV_PZ] = t.pz;
4456
+ this.skinnedDynamicData[dynBase + DYN_CURR_PX] = t.px;
4457
+ this.skinnedDynamicData[dynBase + DYN_CURR_PY] = t.py;
4458
+ this.skinnedDynamicData[dynBase + DYN_CURR_PZ] = t.pz;
4459
+ this.skinnedDynamicData[dynBase + DYN_PREV_RX] = t.rx;
4460
+ this.skinnedDynamicData[dynBase + DYN_PREV_RY] = t.ry;
4461
+ this.skinnedDynamicData[dynBase + DYN_PREV_RZ] = t.rz;
4462
+ this.skinnedDynamicData[dynBase + DYN_CURR_RX] = t.rx;
4463
+ this.skinnedDynamicData[dynBase + DYN_CURR_RY] = t.ry;
4464
+ this.skinnedDynamicData[dynBase + DYN_CURR_RZ] = t.rz;
4465
+ this.skinnedStaticData[statBase + SSTAT_SX] = t.sx;
4466
+ this.skinnedStaticData[statBase + SSTAT_SY] = t.sy;
4467
+ this.skinnedStaticData[statBase + SSTAT_SZ] = t.sz;
4468
+ this.skinnedStaticData[statBase + SSTAT_CR] = t.cr;
4469
+ this.skinnedStaticData[statBase + SSTAT_CG] = t.cg;
4470
+ this.skinnedStaticData[statBase + SSTAT_CB] = t.cb;
5052
4471
  new DataView(this.skinnedStaticData.buffer).setUint32(
5053
4472
  (statBase + SSTAT_BONE_OFFSET) * 4,
5054
4473
  boneOffset,
@@ -5092,16 +4511,102 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5092
4511
  }
5093
4512
  };
5094
4513
  }
4514
+ /**
4515
+ * Drain pending resyncs from the coordinator. Per affected skin: rebuild
4516
+ * its `SkeletalAnimation` clip list densely, remap in-flight animStates,
4517
+ * then full-repack `packedAnimData`. Falls back to a kernel rebuild only
4518
+ * when the new data exceeds the kernel's allocated budgets.
4519
+ *
4520
+ * The dense renumbering is load-bearing: the kernel reads each clip by
4521
+ * its per-skin index, which must equal `SkeletalAnimation`'s clip id.
4522
+ */
4523
+ syncLazyAnimationChanges() {
4524
+ const resync = this.clipResync;
4525
+ if (!resync || resync.pending.size === 0)
4526
+ return;
4527
+ const remapsBySkin = /* @__PURE__ */ new Map();
4528
+ for (const skinIndex of resync.pending) {
4529
+ const sm = this.skinnedModels[skinIndex];
4530
+ if (!sm)
4531
+ continue;
4532
+ remapsBySkin.set(skinIndex, sm.animation.replaceClips(sm.parsedSkin.animClips));
4533
+ }
4534
+ resync.clear();
4535
+ for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
4536
+ const animState = this.skinnedAnimStates[slot];
4537
+ if (!animState)
4538
+ continue;
4539
+ const modelId = this.skinnedInstanceModelIds[slot];
4540
+ const model = this.models[modelId];
4541
+ if (!model || model.skinIndex < 0)
4542
+ continue;
4543
+ const remap3 = remapsBySkin.get(model.skinIndex);
4544
+ if (!remap3)
4545
+ continue;
4546
+ if (animState.clipId >= 0 && animState.clipId < remap3.length) {
4547
+ const next = remap3[animState.clipId];
4548
+ if (next < 0) {
4549
+ animState.clipId = -1;
4550
+ animState.playing = false;
4551
+ } else {
4552
+ animState.clipId = next;
4553
+ }
4554
+ }
4555
+ if (animState.prevClipId >= 0 && animState.prevClipId < remap3.length) {
4556
+ animState.prevClipId = remap3[animState.prevClipId];
4557
+ }
4558
+ }
4559
+ this.packedAnimData = createPackedAnimationData();
4560
+ for (const sm of this.skinnedModels) {
4561
+ packSkinAndAnimations(this.packedAnimData, sm.parsedSkin.data, sm.parsedSkin.animClips);
4562
+ }
4563
+ if (this.tryUploadInPlace())
4564
+ return;
4565
+ this.animComputeNeedsRebuild = true;
4566
+ }
4567
+ /** Doubling-growth budgets for kernel storage buffers, with sensible floors. */
4568
+ growBudgetsForPacked(packed, previous) {
4569
+ const pb = packAnimationData(packed);
4570
+ const grow = (cur, prev, floor) => Math.max(cur * 2, prev, floor);
4571
+ return {
4572
+ skelI32Capacity: grow(pb.skelI32.length, previous?.skelI32Capacity ?? 0, 256),
4573
+ animF32Capacity: grow(pb.animF32.length, previous?.animF32Capacity ?? 0, 4096),
4574
+ matricesCapacity: grow(pb.totalMats, previous?.matricesCapacity ?? 0, 8)
4575
+ };
4576
+ }
4577
+ /** Upload `packedAnimData` to the existing kernel iff it fits the current budgets. Returns false to force a rebuild. */
4578
+ tryUploadInPlace() {
4579
+ const kernel = this.animComputeKernel;
4580
+ const budgets = this.animKernelBudgets;
4581
+ if (!kernel || !budgets)
4582
+ return false;
4583
+ const pb = packAnimationData(this.packedAnimData);
4584
+ if (pb.skelI32.length > budgets.skelI32Capacity)
4585
+ return false;
4586
+ if (pb.animF32.length > budgets.animF32Capacity)
4587
+ return false;
4588
+ if (pb.totalMats > budgets.matricesCapacity)
4589
+ return false;
4590
+ uploadPackedToKernel(this.root, kernel, pb);
4591
+ this.animClipTableOffset = pb.clipTableOffset;
4592
+ this.animChannelTableOffset = pb.channelTableOffset;
4593
+ this.animJointLookupOffset = pb.jointLookupOffset;
4594
+ return true;
4595
+ }
5095
4596
  updateAnimations(deltaTime) {
4597
+ this.syncLazyAnimationChanges();
5096
4598
  if (this.animComputeNeedsRebuild && this.packedAnimData.clips.length > 0) {
5097
4599
  this.animComputeKernel?.destroy();
4600
+ const budgets = this.growBudgetsForPacked(this.packedAnimData, null);
5098
4601
  const { kernel, packedBuffers } = buildAnimationKernel(
5099
4602
  this.root,
5100
4603
  this.packedAnimData,
5101
4604
  this.maxSkinnedInstances,
5102
- this.maxTotalBones
4605
+ this.maxTotalBones,
4606
+ budgets
5103
4607
  );
5104
4608
  this.animComputeKernel = kernel;
4609
+ this.animKernelBudgets = budgets;
5105
4610
  this.animClipTableOffset = packedBuffers.clipTableOffset;
5106
4611
  this.animChannelTableOffset = packedBuffers.channelTableOffset;
5107
4612
  this.animJointLookupOffset = packedBuffers.jointLookupOffset;
@@ -5118,6 +4623,14 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5118
4623
  { binding: 4, resource: { buffer: rawBoneBuffer } }
5119
4624
  ]
5120
4625
  });
4626
+ this.device.queue.writeBuffer(
4627
+ this.rawBoneMatrixBuffer,
4628
+ 0,
4629
+ this.boneMatrixData.buffer,
4630
+ this.boneMatrixData.byteOffset,
4631
+ this.boneMatrixData.byteLength
4632
+ );
4633
+ this.boneMatrixDirty = false;
5121
4634
  this.animComputeNeedsRebuild = false;
5122
4635
  }
5123
4636
  this.updatedBoneOffsets.fill(0);
@@ -5518,6 +5031,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
5518
5031
  this.resizeObserver?.disconnect();
5519
5032
  this.resizeObserver = null;
5520
5033
  this.resizeCallbacks.length = 0;
5034
+ this.clipResync?.dispose();
5035
+ this.clipResync = null;
5521
5036
  this.dynamicBuffer?.destroy();
5522
5037
  this.staticBuffer?.destroy();
5523
5038
  this.uniformBuffer?.destroy();
@@ -5665,10 +5180,8 @@ var ParticleEmitter = class {
5665
5180
  const sprite = this.renderer.addSprite({
5666
5181
  sheet: this.config.sheet,
5667
5182
  sprite: this.config.sprite ?? 0,
5668
- x,
5669
- y,
5670
- scaleX: size,
5671
- scaleY: size,
5183
+ position: [x, y],
5184
+ scale: size,
5672
5185
  opacity: 1,
5673
5186
  tint: this.config.color,
5674
5187
  layer: 255
@@ -5720,109 +5233,6 @@ var ParticleEmitter = class {
5720
5233
  }
5721
5234
  };
5722
5235
 
5723
- // src/2d/animation.ts
5724
- var AnimationController = class {
5725
- constructor() {
5726
- this.clips = [];
5727
- this.clipsByName = /* @__PURE__ */ new Map();
5728
- }
5729
- /**
5730
- * Register an animation clip. Returns the clip ID.
5731
- */
5732
- loadClip(config) {
5733
- const id = this.clips.length;
5734
- const clip = {
5735
- id,
5736
- name: config.name,
5737
- frames: new Uint16Array(config.frames),
5738
- durations: new Float32Array(config.durations),
5739
- frameCount: config.frames.length,
5740
- totalDuration: config.durations.reduce((sum, d7) => sum + d7, 0),
5741
- loop: config.loop
5742
- };
5743
- this.clips.push(clip);
5744
- this.clipsByName.set(config.name, id);
5745
- return id;
5746
- }
5747
- /**
5748
- * Get a clip by name.
5749
- */
5750
- getClipId(name) {
5751
- const id = this.clipsByName.get(name);
5752
- if (id === void 0)
5753
- throw new Error(`Animation clip "${name}" not found`);
5754
- return id;
5755
- }
5756
- /**
5757
- * Get a clip by ID.
5758
- */
5759
- getClip(id) {
5760
- return this.clips[id];
5761
- }
5762
- /**
5763
- * Create a new animation state for an entity. This is the per-entity data
5764
- * that tracks playback progress. Store it however you like (flat array, component, etc).
5765
- */
5766
- createState(clipId, speed = 1, playing = true) {
5767
- return { clipId, frame: 0, time: 0, speed, playing };
5768
- }
5769
- /**
5770
- * Advance an animation state by deltaTime (in seconds).
5771
- * Returns the current sprite frame ID, or -1 if not playing.
5772
- * Zero allocations.
5773
- */
5774
- update(state, deltaTime) {
5775
- if (!state.playing) {
5776
- return this.clips[state.clipId].frames[state.frame];
5777
- }
5778
- const clip = this.clips[state.clipId];
5779
- state.time += deltaTime * state.speed * 1e3;
5780
- while (state.time >= clip.durations[state.frame]) {
5781
- state.time -= clip.durations[state.frame];
5782
- state.frame++;
5783
- if (state.frame >= clip.frameCount) {
5784
- if (clip.loop) {
5785
- state.frame = 0;
5786
- } else {
5787
- state.frame = clip.frameCount - 1;
5788
- state.playing = false;
5789
- break;
5790
- }
5791
- }
5792
- }
5793
- return clip.frames[state.frame];
5794
- }
5795
- /**
5796
- * Play a different clip on an existing state. Resets frame and time.
5797
- */
5798
- play(state, clipId, speed) {
5799
- state.clipId = clipId;
5800
- state.frame = 0;
5801
- state.time = 0;
5802
- state.playing = true;
5803
- if (speed !== void 0)
5804
- state.speed = speed;
5805
- }
5806
- /**
5807
- * Stop playback.
5808
- */
5809
- stop(state) {
5810
- state.playing = false;
5811
- }
5812
- /**
5813
- * Resume playback.
5814
- */
5815
- resume(state) {
5816
- state.playing = true;
5817
- }
5818
- /**
5819
- * Number of loaded clips.
5820
- */
5821
- get clipCount() {
5822
- return this.clips.length;
5823
- }
5824
- };
5825
-
5826
5236
  // src/shaders/utils.ts
5827
5237
  import tgpu7 from "typegpu";
5828
5238
  import * as d6 from "typegpu/data";
@@ -5944,7 +5354,6 @@ export {
5944
5354
  STATIC_OFFSET_UV_MAX_Y,
5945
5355
  STATIC_OFFSET_UV_MIN_X,
5946
5356
  STATIC_OFFSET_UV_MIN_Y,
5947
- SkeletalAnimation,
5948
5357
  SkinnedStaticMesh,
5949
5358
  SpriteAccessor,
5950
5359
  SpriteUniforms,
@@ -5954,14 +5363,11 @@ export {
5954
5363
  StaticSprite,
5955
5364
  WebGPU2DRenderer,
5956
5365
  WebGPU3DRenderer,
5957
- computeGridUVs,
5958
- computeTexturePackerUVs,
5959
5366
  createGeometryDataLayout,
5960
5367
  createTextureFromBitmap,
5961
5368
  d,
5962
5369
  getFieldFloats,
5963
5370
  inverseLerp,
5964
- loadImage,
5965
5371
  remap,
5966
5372
  resolveBuiltInGeometry,
5967
5373
  rotate2d,