murow 0.0.72 → 0.0.73

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.
@@ -3768,7 +3768,8 @@ var SSTAT_CB = 5;
3768
3768
  var SSTAT_BONE_OFFSET = 6;
3769
3769
  var prefabHandles = /* @__PURE__ */ new WeakMap();
3770
3770
  function isPrefab3D(value) {
3771
- return value.type === "gltf" || value.type === "grid";
3771
+ const t = value.type;
3772
+ return t === "gltf" || t === "grid" || t === "cube" || t === "composite";
3772
3773
  }
3773
3774
  function resolveTransform(opts) {
3774
3775
  const [px, py, pz] = opts.position ?? [0, 0, 0];
@@ -3802,8 +3803,8 @@ function computeBucketStats(bucket) {
3802
3803
  }
3803
3804
  var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3804
3805
  constructor(canvas, options) {
3805
- const resolvedMaxModels = options.maxModels ?? (options.prefabs ? options.prefabs.size + 16 : 32);
3806
- super(canvas, { ...options, maxModels: resolvedMaxModels });
3806
+ const resolvedMaxInstances = options.maxInstances ?? (options.prefabs ? options.prefabs.size + 16 : 32);
3807
+ super(canvas, { ...options, maxInstances: resolvedMaxInstances });
3807
3808
  this.resizeObserver = null;
3808
3809
  this.resizeCallbacks = [];
3809
3810
  // layer=0, sheetId=modelId
@@ -3829,6 +3830,8 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3829
3830
  this.skinnedStaticDirty = false;
3830
3831
  this.skinnedAnimStates = [];
3831
3832
  this.nextBoneOffset = 0;
3833
+ /** Reusable bone-offset blocks per skinIndex. Pushed on remove, popped on add. Indexed by skinIndex (low cardinality), so a Map of lists is fine. */
3834
+ this.freedBoneOffsets = /* @__PURE__ */ new Map();
3832
3835
  // Frustum planes (6 planes × 4 floats each), extracted from VP matrix
3833
3836
  this.frustumPlanes = new Float32Array(24);
3834
3837
  this.uniformData = new Float32Array(24);
@@ -3838,22 +3841,26 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3838
3841
  this._prefabs = options.prefabs ?? null;
3839
3842
  const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
3840
3843
  const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
3841
- const maxInstances = options.maxInstances ?? resolvedMaxModels;
3842
- this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? maxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
3844
+ this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? resolvedMaxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
3843
3845
  this.maxBonesPerSkin = options.maxBonesPerSkin ?? (bucketStats ? Math.max(1, bucketStats.maxJointCount) : 64);
3846
+ const cullDist = options.animationCullDistance ?? 50;
3847
+ this.animationCullDistanceSq = cullDist * cullDist;
3844
3848
  this.maxTotalBones = this.maxSkinnedInstances * this.maxBonesPerSkin * 2;
3845
3849
  this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
3846
- this.freeList = new import_free_list3.FreeList(resolvedMaxModels);
3847
- this.batcher = new import_sparse_batcher2.SparseBatcher(resolvedMaxModels);
3848
- this.dynamicData = new Float32Array(resolvedMaxModels * DYNAMIC_MESH_FLOATS);
3849
- this.staticData = new Float32Array(resolvedMaxModels * STATIC_MESH_FLOATS);
3850
- this.slotIndexData = new Uint32Array(resolvedMaxModels);
3851
- this.instanceModelIds = new Uint8Array(resolvedMaxModels);
3850
+ this.boneOffsetRefcount = new Uint32Array(this.maxTotalBones);
3851
+ this.boneOffsetSkinIndex = new Uint32Array(this.maxTotalBones);
3852
+ this.freeList = new import_free_list3.FreeList(resolvedMaxInstances);
3853
+ this.batcher = new import_sparse_batcher2.SparseBatcher(resolvedMaxInstances);
3854
+ this.dynamicData = new Float32Array(resolvedMaxInstances * DYNAMIC_MESH_FLOATS);
3855
+ this.staticData = new Float32Array(resolvedMaxInstances * STATIC_MESH_FLOATS);
3856
+ this.slotIndexData = new Uint32Array(resolvedMaxInstances);
3857
+ this.instanceModelIds = new Uint8Array(resolvedMaxInstances);
3852
3858
  const msi = this.maxSkinnedInstances;
3853
3859
  this.skinnedFreeList = new import_free_list3.FreeList(msi);
3854
3860
  this.skinnedBatcher = new import_sparse_batcher2.SparseBatcher(msi);
3855
3861
  this.skinnedDynamicData = new Float32Array(msi * DYNAMIC_MESH_FLOATS);
3856
3862
  this.skinnedStaticData = new Float32Array(msi * SKINNED_STATIC_MESH_FLOATS);
3863
+ this.skinnedStaticDV = new DataView(this.skinnedStaticData.buffer);
3857
3864
  this.skinnedSlotIndexData = new Uint32Array(msi);
3858
3865
  this.skinnedInstanceModelIds = new Uint8Array(msi);
3859
3866
  this.skinnedInstanceBoneOffsets = new Uint32Array(msi);
@@ -3864,7 +3871,19 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3864
3871
  this.gpuInstDV = new DataView(this.gpuInstData.buffer);
3865
3872
  }
3866
3873
  async init() {
3867
- this.root = await import_typegpu9.default.init();
3874
+ const adapter = await navigator.gpu.requestAdapter();
3875
+ if (!adapter)
3876
+ throw new Error("WebGPU3DRenderer: no GPU adapter available");
3877
+ const a = adapter.limits;
3878
+ const requiredLimits = {
3879
+ maxBufferSize: a.maxBufferSize,
3880
+ maxStorageBufferBindingSize: a.maxStorageBufferBindingSize,
3881
+ maxStorageBuffersPerShaderStage: a.maxStorageBuffersPerShaderStage,
3882
+ maxComputeWorkgroupStorageSize: a.maxComputeWorkgroupStorageSize,
3883
+ maxComputeInvocationsPerWorkgroup: a.maxComputeInvocationsPerWorkgroup
3884
+ };
3885
+ const device = await adapter.requestDevice({ requiredLimits });
3886
+ this.root = import_typegpu9.default.initFromDevice({ device });
3868
3887
  this.device = this.root.device;
3869
3888
  this.context = this.canvas.getContext("webgpu");
3870
3889
  this.format = navigator.gpu.getPreferredCanvasFormat();
@@ -3881,7 +3900,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3881
3900
  format: "depth24plus",
3882
3901
  usage: GPUTextureUsage.RENDER_ATTACHMENT
3883
3902
  });
3884
- this.meshLayout = createMeshLayout(this.maxModels);
3903
+ this.meshLayout = createMeshLayout(this.maxInstances);
3885
3904
  const depthStencil = {
3886
3905
  format: "depth24plus",
3887
3906
  depthWriteEnabled: true,
@@ -3928,10 +3947,10 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
3928
3947
  primitive,
3929
3948
  depthStencil
3930
3949
  });
3931
- this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.maxModels)).$usage("storage");
3932
- this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxModels)).$usage("storage");
3950
+ this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.maxInstances)).$usage("storage");
3951
+ this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxInstances)).$usage("storage");
3933
3952
  this.uniformBuffer = this.root.createBuffer(MeshUniforms).$usage("uniform");
3934
- this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxModels)).$usage("storage");
3953
+ this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxInstances)).$usage("storage");
3935
3954
  this.rawDynamicBuffer = this.root.unwrap(this.dynamicBuffer);
3936
3955
  this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
3937
3956
  this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
@@ -4033,6 +4052,9 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4033
4052
  lineWidth: prefab.lineWidth
4034
4053
  });
4035
4054
  prefabHandles.set(prefab, model);
4055
+ } else if (prefab.type === "cube") {
4056
+ const model = this.createCube({ size: prefab.size });
4057
+ prefabHandles.set(prefab, model);
4036
4058
  }
4037
4059
  }
4038
4060
  }
@@ -4097,6 +4119,36 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4097
4119
  createCompute(name, options) {
4098
4120
  return new ComputeBuilder(name, options, this.root);
4099
4121
  }
4122
+ /**
4123
+ * Set the maximum distance (in world units) at which the renderer keeps
4124
+ * computing skeletal animation. Skinned instances farther than this are
4125
+ * still drawn, but with their last-computed bone matrices instead of
4126
+ * fresh ones, which saves GPU compute work. Their internal animation
4127
+ * clocks keep ticking on CPU, so when they come back into range they
4128
+ * resume in sync.
4129
+ *
4130
+ * Lower values trade visual smoothness on distant characters for FPS.
4131
+ * Pass `Infinity` to disable culling entirely (always animate).
4132
+ *
4133
+ * Safe to call any time; takes effect on the next frame.
4134
+ *
4135
+ * @param distance Max distance to animate at, in world units.
4136
+ */
4137
+ setAnimationCullDistance(distance) {
4138
+ this.animationCullDistanceSq = distance * distance;
4139
+ }
4140
+ /** Current animation cull distance (in world units). See `setAnimationCullDistance`. */
4141
+ get animationCullDistance() {
4142
+ return Math.sqrt(this.animationCullDistanceSq);
4143
+ }
4144
+ /**
4145
+ * Max skinned instances the renderer was sized for at construction.
4146
+ * Independent budget from `maxInstances` since skinned characters use a
4147
+ * separate set of GPU buffers. Read-only.
4148
+ */
4149
+ get maxSkinned() {
4150
+ return this.maxSkinnedInstances;
4151
+ }
4100
4152
  /**
4101
4153
  * Create a flat grid mesh on the XZ plane at Y=0.
4102
4154
  *
@@ -4128,6 +4180,245 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4128
4180
  indices: new Uint16Array(indices)
4129
4181
  });
4130
4182
  }
4183
+ /**
4184
+ * Create a unit-cube mesh centered at the origin. Pass `size` to scale the
4185
+ * edge length, or keep size = 1 and scale at the instance level.
4186
+ *
4187
+ * ```ts
4188
+ * const cube = renderer.createCube();
4189
+ * renderer.addInstance({ model: cube, color: [1, 0.5, 0.2], scale: 2 });
4190
+ * ```
4191
+ */
4192
+ createCube(opts = {}) {
4193
+ const h = (opts.size ?? 1) / 2;
4194
+ const positions = new Float32Array([
4195
+ // +Z
4196
+ -h,
4197
+ -h,
4198
+ h,
4199
+ h,
4200
+ -h,
4201
+ h,
4202
+ h,
4203
+ h,
4204
+ h,
4205
+ -h,
4206
+ -h,
4207
+ h,
4208
+ h,
4209
+ h,
4210
+ h,
4211
+ -h,
4212
+ h,
4213
+ h,
4214
+ // -Z
4215
+ h,
4216
+ -h,
4217
+ -h,
4218
+ -h,
4219
+ -h,
4220
+ -h,
4221
+ -h,
4222
+ h,
4223
+ -h,
4224
+ h,
4225
+ -h,
4226
+ -h,
4227
+ -h,
4228
+ h,
4229
+ -h,
4230
+ h,
4231
+ h,
4232
+ -h,
4233
+ // +Y
4234
+ -h,
4235
+ h,
4236
+ h,
4237
+ h,
4238
+ h,
4239
+ h,
4240
+ h,
4241
+ h,
4242
+ -h,
4243
+ -h,
4244
+ h,
4245
+ h,
4246
+ h,
4247
+ h,
4248
+ -h,
4249
+ -h,
4250
+ h,
4251
+ -h,
4252
+ // -Y
4253
+ -h,
4254
+ -h,
4255
+ -h,
4256
+ h,
4257
+ -h,
4258
+ -h,
4259
+ h,
4260
+ -h,
4261
+ h,
4262
+ -h,
4263
+ -h,
4264
+ -h,
4265
+ h,
4266
+ -h,
4267
+ h,
4268
+ -h,
4269
+ -h,
4270
+ h,
4271
+ // +X
4272
+ h,
4273
+ -h,
4274
+ h,
4275
+ h,
4276
+ -h,
4277
+ -h,
4278
+ h,
4279
+ h,
4280
+ -h,
4281
+ h,
4282
+ -h,
4283
+ h,
4284
+ h,
4285
+ h,
4286
+ -h,
4287
+ h,
4288
+ h,
4289
+ h,
4290
+ // -X
4291
+ -h,
4292
+ -h,
4293
+ -h,
4294
+ -h,
4295
+ -h,
4296
+ h,
4297
+ -h,
4298
+ h,
4299
+ h,
4300
+ -h,
4301
+ -h,
4302
+ -h,
4303
+ -h,
4304
+ h,
4305
+ h,
4306
+ -h,
4307
+ h,
4308
+ -h
4309
+ ]);
4310
+ const normals = new Float32Array([
4311
+ 0,
4312
+ 0,
4313
+ 1,
4314
+ 0,
4315
+ 0,
4316
+ 1,
4317
+ 0,
4318
+ 0,
4319
+ 1,
4320
+ 0,
4321
+ 0,
4322
+ 1,
4323
+ 0,
4324
+ 0,
4325
+ 1,
4326
+ 0,
4327
+ 0,
4328
+ 1,
4329
+ 0,
4330
+ 0,
4331
+ -1,
4332
+ 0,
4333
+ 0,
4334
+ -1,
4335
+ 0,
4336
+ 0,
4337
+ -1,
4338
+ 0,
4339
+ 0,
4340
+ -1,
4341
+ 0,
4342
+ 0,
4343
+ -1,
4344
+ 0,
4345
+ 0,
4346
+ -1,
4347
+ 0,
4348
+ 1,
4349
+ 0,
4350
+ 0,
4351
+ 1,
4352
+ 0,
4353
+ 0,
4354
+ 1,
4355
+ 0,
4356
+ 0,
4357
+ 1,
4358
+ 0,
4359
+ 0,
4360
+ 1,
4361
+ 0,
4362
+ 0,
4363
+ 1,
4364
+ 0,
4365
+ 0,
4366
+ -1,
4367
+ 0,
4368
+ 0,
4369
+ -1,
4370
+ 0,
4371
+ 0,
4372
+ -1,
4373
+ 0,
4374
+ 0,
4375
+ -1,
4376
+ 0,
4377
+ 0,
4378
+ -1,
4379
+ 0,
4380
+ 0,
4381
+ -1,
4382
+ 0,
4383
+ 1,
4384
+ 0,
4385
+ 0,
4386
+ 1,
4387
+ 0,
4388
+ 0,
4389
+ 1,
4390
+ 0,
4391
+ 0,
4392
+ 1,
4393
+ 0,
4394
+ 0,
4395
+ 1,
4396
+ 0,
4397
+ 0,
4398
+ 1,
4399
+ 0,
4400
+ 0,
4401
+ -1,
4402
+ 0,
4403
+ 0,
4404
+ -1,
4405
+ 0,
4406
+ 0,
4407
+ -1,
4408
+ 0,
4409
+ 0,
4410
+ -1,
4411
+ 0,
4412
+ 0,
4413
+ -1,
4414
+ 0,
4415
+ 0,
4416
+ -1,
4417
+ 0,
4418
+ 0
4419
+ ]);
4420
+ return this.loadModel({ positions, normals });
4421
+ }
4131
4422
  /**
4132
4423
  * Register a model. Returns a handle for addInstance().
4133
4424
  *
@@ -4443,6 +4734,9 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4443
4734
  * with another instance (e.g., when spawning all parts of a character).
4444
4735
  */
4445
4736
  addInstance(opts) {
4737
+ if (isPrefab3D(opts.model) && opts.model.type === "composite") {
4738
+ return this.addCompositeInstance(opts, opts.model);
4739
+ }
4446
4740
  const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
4447
4741
  if ("parts" in modelOrGltf) {
4448
4742
  return this.addGltfInstance(opts, modelOrGltf);
@@ -4454,7 +4748,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4454
4748
  }
4455
4749
  const slot = this.freeList.allocate();
4456
4750
  if (slot === -1)
4457
- throw new Error(`Max instances (${this.maxModels}) reached`);
4751
+ throw new Error(`Max instances (${this.maxInstances}) reached`);
4458
4752
  const dynBase = slot * DYNAMIC_MESH_FLOATS;
4459
4753
  const statBase = slot * STATIC_MESH_FLOATS;
4460
4754
  const t = resolveTransform(opts);
@@ -4481,7 +4775,11 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4481
4775
  this.batcher.add(0, modelHandle.id, slot);
4482
4776
  const dynamicData = this.dynamicData;
4483
4777
  const staticData = this.staticData;
4484
- return {
4778
+ const self = this;
4779
+ let destroyed = false;
4780
+ const handle = {
4781
+ slot,
4782
+ modelId: modelHandle.id,
4485
4783
  skinned: false,
4486
4784
  setPosition(nx, ny, nz) {
4487
4785
  dynamicData[dynBase + DYN_CURR_PX] = nx;
@@ -4497,8 +4795,19 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4497
4795
  staticData[statBase + STAT_SX] = nx;
4498
4796
  staticData[statBase + STAT_SY] = ny;
4499
4797
  staticData[statBase + STAT_SZ] = nz;
4798
+ },
4799
+ destroy() {
4800
+ if (destroyed)
4801
+ return;
4802
+ destroyed = true;
4803
+ self.batcher.remove(0, modelHandle.id, slot);
4804
+ self.freeList.free(slot);
4805
+ dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4806
+ staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
4807
+ self.staticDirty = true;
4500
4808
  }
4501
4809
  };
4810
+ return handle;
4502
4811
  }
4503
4812
  addGltfInstance(opts, gltf) {
4504
4813
  const childHandles = [];
@@ -4536,7 +4845,71 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4536
4845
  } : void 0,
4537
4846
  stop: skinnedHandle?.stop ? () => {
4538
4847
  skinnedHandle.stop();
4539
- } : void 0
4848
+ } : void 0,
4849
+ destroy() {
4850
+ for (const h of childHandles)
4851
+ h.destroy();
4852
+ }
4853
+ };
4854
+ }
4855
+ /**
4856
+ * Spawn a composite prefab by spawning each of its parts at the composed
4857
+ * (instance + offset) transform. The returned handle broadcasts subsequent
4858
+ * `setPosition` / `setRotation` to every child, keeping each child's
4859
+ * baked offset applied on top of the new value.
4860
+ */
4861
+ addCompositeInstance(opts, composite) {
4862
+ const bucket = this._prefabs;
4863
+ if (!bucket) {
4864
+ throw new Error(
4865
+ `addInstance: composite '${composite.id}' requires the renderer to be constructed with the bucket (\`prefabs\`).`
4866
+ );
4867
+ }
4868
+ const basePos = opts.position ?? [0, 0, 0];
4869
+ const baseRot = opts.rotation ?? [0, 0, 0];
4870
+ const offsets = composite.parts.map((p) => ({
4871
+ px: p.offset?.position?.[0] ?? 0,
4872
+ py: p.offset?.position?.[1] ?? 0,
4873
+ pz: p.offset?.position?.[2] ?? 0,
4874
+ rx: p.offset?.rotation?.[0] ?? 0,
4875
+ ry: p.offset?.rotation?.[1] ?? 0,
4876
+ rz: p.offset?.rotation?.[2] ?? 0
4877
+ }));
4878
+ const childHandles = [];
4879
+ for (let i = 0; i < composite.parts.length; i++) {
4880
+ const part = composite.parts[i];
4881
+ const off = offsets[i];
4882
+ const partPrefab = bucket.get(part.partId);
4883
+ const partOpts = {
4884
+ ...opts,
4885
+ model: partPrefab,
4886
+ position: [basePos[0] + off.px, basePos[1] + off.py, basePos[2] + off.pz],
4887
+ rotation: [baseRot[0] + off.rx, baseRot[1] + off.ry, baseRot[2] + off.rz]
4888
+ };
4889
+ childHandles.push(this.addInstance(partOpts));
4890
+ }
4891
+ return {
4892
+ skinned: childHandles.some((h) => h.skinned),
4893
+ setPosition(x, y, z) {
4894
+ for (let i = 0; i < childHandles.length; i++) {
4895
+ const o = offsets[i];
4896
+ childHandles[i].setPosition(x + o.px, y + o.py, z + o.pz);
4897
+ }
4898
+ },
4899
+ setRotation(x, y, z) {
4900
+ for (let i = 0; i < childHandles.length; i++) {
4901
+ const o = offsets[i];
4902
+ childHandles[i].setRotation(x + o.rx, y + o.ry, z + o.rz);
4903
+ }
4904
+ },
4905
+ setScale(x, y, z) {
4906
+ for (const h of childHandles)
4907
+ h.setScale(x, y, z);
4908
+ },
4909
+ destroy() {
4910
+ for (const h of childHandles)
4911
+ h.destroy();
4912
+ }
4540
4913
  };
4541
4914
  }
4542
4915
  addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot) {
@@ -4550,11 +4923,27 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4550
4923
  if (linkedSlot !== void 0) {
4551
4924
  boneOffset = this.skinnedInstanceBoneOffsets[linkedSlot];
4552
4925
  animState = this.skinnedAnimStates[linkedSlot];
4926
+ this.boneOffsetRefcount[boneOffset]++;
4553
4927
  } else {
4554
- boneOffset = this.nextBoneOffset + jointCount;
4555
- this.nextBoneOffset += jointCount * 2;
4556
- skinModel.animation.computeRestPose(this.boneMatrixData, boneOffset * 16);
4557
- this.boneMatrixDirty = true;
4928
+ const pool = this.freedBoneOffsets.get(skinIndex);
4929
+ if (pool && pool.length > 0) {
4930
+ boneOffset = pool.pop();
4931
+ } else {
4932
+ boneOffset = this.nextBoneOffset + jointCount;
4933
+ this.nextBoneOffset += jointCount * 2;
4934
+ }
4935
+ this.boneOffsetRefcount[boneOffset] = 1;
4936
+ this.boneOffsetSkinIndex[boneOffset] = skinIndex;
4937
+ const restOffsetFloats = boneOffset * 16;
4938
+ const restLengthFloats = jointCount * 16;
4939
+ skinModel.animation.computeRestPose(this.boneMatrixData, restOffsetFloats);
4940
+ this.device.queue.writeBuffer(
4941
+ this.rawBoneMatrixBuffer,
4942
+ restOffsetFloats * 4,
4943
+ this.boneMatrixData.buffer,
4944
+ this.boneMatrixData.byteOffset + restOffsetFloats * 4,
4945
+ restLengthFloats * 4
4946
+ );
4558
4947
  animState = skinModel.animation.clipCount > 0 ? skinModel.animation.createState(0, 1, true) : null;
4559
4948
  }
4560
4949
  this.skinnedInstanceBoneOffsets[slot] = boneOffset;
@@ -4580,11 +4969,7 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4580
4969
  this.skinnedStaticData[statBase + SSTAT_CR] = t.cr;
4581
4970
  this.skinnedStaticData[statBase + SSTAT_CG] = t.cg;
4582
4971
  this.skinnedStaticData[statBase + SSTAT_CB] = t.cb;
4583
- new DataView(this.skinnedStaticData.buffer).setUint32(
4584
- (statBase + SSTAT_BONE_OFFSET) * 4,
4585
- boneOffset,
4586
- true
4587
- );
4972
+ this.skinnedStaticDV.setUint32((statBase + SSTAT_BONE_OFFSET) * 4, boneOffset, true);
4588
4973
  this.skinnedStaticDirty = true;
4589
4974
  this.skinnedInstanceModelIds[slot] = modelHandle.id;
4590
4975
  this.skinnedBatcher.add(0, modelHandle.id, slot);
@@ -4592,6 +4977,10 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4592
4977
  const staticData = this.skinnedStaticData;
4593
4978
  const animStates = this.skinnedAnimStates;
4594
4979
  const animation = skinModel.animation;
4980
+ const self = this;
4981
+ const capturedBoneOffset = boneOffset;
4982
+ const capturedSkinIndex = skinIndex;
4983
+ let destroyed = false;
4595
4984
  return {
4596
4985
  slot,
4597
4986
  modelId: modelHandle.id,
@@ -4620,6 +5009,25 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4620
5009
  const state = animStates[slot];
4621
5010
  if (state)
4622
5011
  animation.stop(state);
5012
+ },
5013
+ destroy() {
5014
+ if (destroyed)
5015
+ return;
5016
+ destroyed = true;
5017
+ self.skinnedBatcher.remove(0, modelHandle.id, slot);
5018
+ self.skinnedFreeList.free(slot);
5019
+ dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
5020
+ staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
5021
+ animStates[slot] = null;
5022
+ self.skinnedStaticDirty = true;
5023
+ if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
5024
+ let pool = self.freedBoneOffsets.get(capturedSkinIndex);
5025
+ if (!pool) {
5026
+ pool = [];
5027
+ self.freedBoneOffsets.set(capturedSkinIndex, pool);
5028
+ }
5029
+ pool.push(capturedBoneOffset);
5030
+ }
4623
5031
  }
4624
5032
  };
4625
5033
  }
@@ -4705,6 +5113,31 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4705
5113
  this.animJointLookupOffset = pb.jointLookupOffset;
4706
5114
  return true;
4707
5115
  }
5116
+ /**
5117
+ * Returns true if a skinned instance's bone-matrix compute should be
5118
+ * dispatched this frame. Combines frustum culling (scaled bounding sphere)
5119
+ * and a configurable distance cull from the camera. Callers pass the
5120
+ * camera position + cullDistSq once outside the loop to avoid re-reading.
5121
+ */
5122
+ shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq) {
5123
+ const skinModel = model && model.skinIndex >= 0 ? this.skinnedModels[model.skinIndex] : null;
5124
+ const baseRadius = skinModel?.boundingRadius ?? 10;
5125
+ const base = slot * DYNAMIC_MESH_FLOATS;
5126
+ const sBase = slot * SKINNED_STATIC_MESH_FLOATS;
5127
+ const cx = this.skinnedDynamicData[base + DYN_CURR_PX];
5128
+ const cy = this.skinnedDynamicData[base + DYN_CURR_PY];
5129
+ const cz = this.skinnedDynamicData[base + DYN_CURR_PZ];
5130
+ const sx = Math.abs(this.skinnedStaticData[sBase + SSTAT_SX]);
5131
+ const sy = Math.abs(this.skinnedStaticData[sBase + SSTAT_SY]);
5132
+ const sz = Math.abs(this.skinnedStaticData[sBase + SSTAT_SZ]);
5133
+ const maxScale = sx > sy ? sx > sz ? sx : sz : sy > sz ? sy : sz;
5134
+ if (!this.isInFrustum(cx, cy, cz, baseRadius * maxScale))
5135
+ return false;
5136
+ const dxv = cx - camX, dyv = cy - camY, dzv = cz - camZ;
5137
+ if (dxv * dxv + dyv * dyv + dzv * dzv > cullDistSq)
5138
+ return false;
5139
+ return true;
5140
+ }
4708
5141
  updateAnimations(deltaTime) {
4709
5142
  this.syncLazyAnimationChanges();
4710
5143
  if (this.animComputeNeedsRebuild && this.packedAnimData.clips.length > 0) {
@@ -4748,6 +5181,9 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4748
5181
  this.updatedBoneOffsets.fill(0);
4749
5182
  let count = 0;
4750
5183
  const dv = this.gpuInstDV;
5184
+ const camPos = this.camera.position;
5185
+ const camX = camPos[0], camY = camPos[1], camZ = camPos[2];
5186
+ const cullDistSq = this.animationCullDistanceSq;
4751
5187
  for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
4752
5188
  const animState = this.skinnedAnimStates[slot];
4753
5189
  if (!animState)
@@ -4786,6 +5222,8 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4786
5222
  const modelId = this.skinnedInstanceModelIds[slot];
4787
5223
  const model = this.models[modelId];
4788
5224
  const skinIdx = model?.skinIndex ?? 0;
5225
+ if (!this.shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq))
5226
+ continue;
4789
5227
  const off = count * 32;
4790
5228
  dv.setInt32(off, animState.clipId, true);
4791
5229
  dv.setFloat32(off + 4, animState.time, true);
@@ -4830,14 +5268,12 @@ var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
4830
5268
  }
4831
5269
  }
4832
5270
  }
5271
+ /**
5272
+ * Free an instance's renderer slot. Equivalent to `handle.destroy()` -
5273
+ * kept as a convenience for direct lookup. Safe to call multiple times.
5274
+ */
4833
5275
  removeInstance(handle) {
4834
- this.batcher.remove(0, handle.modelId, handle.slot);
4835
- this.freeList.free(handle.slot);
4836
- const dynBase = handle.slot * DYNAMIC_MESH_FLOATS;
4837
- const statBase = handle.slot * STATIC_MESH_FLOATS;
4838
- this.dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4839
- this.staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
4840
- this.staticDirty = true;
5276
+ handle.destroy();
4841
5277
  }
4842
5278
  storePreviousState() {
4843
5279
  this.camera.storePrevious();