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.
@@ -3656,7 +3656,8 @@ var SSTAT_CB = 5;
3656
3656
  var SSTAT_BONE_OFFSET = 6;
3657
3657
  var prefabHandles = /* @__PURE__ */ new WeakMap();
3658
3658
  function isPrefab3D(value) {
3659
- return value.type === "gltf" || value.type === "grid";
3659
+ const t = value.type;
3660
+ return t === "gltf" || t === "grid" || t === "cube" || t === "composite";
3660
3661
  }
3661
3662
  function resolveTransform(opts) {
3662
3663
  const [px, py, pz] = opts.position ?? [0, 0, 0];
@@ -3690,8 +3691,8 @@ function computeBucketStats(bucket) {
3690
3691
  }
3691
3692
  var WebGPU3DRenderer = class extends Base3DRenderer {
3692
3693
  constructor(canvas, options) {
3693
- const resolvedMaxModels = options.maxModels ?? (options.prefabs ? options.prefabs.size + 16 : 32);
3694
- super(canvas, { ...options, maxModels: resolvedMaxModels });
3694
+ const resolvedMaxInstances = options.maxInstances ?? (options.prefabs ? options.prefabs.size + 16 : 32);
3695
+ super(canvas, { ...options, maxInstances: resolvedMaxInstances });
3695
3696
  this.resizeObserver = null;
3696
3697
  this.resizeCallbacks = [];
3697
3698
  // layer=0, sheetId=modelId
@@ -3717,6 +3718,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3717
3718
  this.skinnedStaticDirty = false;
3718
3719
  this.skinnedAnimStates = [];
3719
3720
  this.nextBoneOffset = 0;
3721
+ /** Reusable bone-offset blocks per skinIndex. Pushed on remove, popped on add. Indexed by skinIndex (low cardinality), so a Map of lists is fine. */
3722
+ this.freedBoneOffsets = /* @__PURE__ */ new Map();
3720
3723
  // Frustum planes (6 planes × 4 floats each), extracted from VP matrix
3721
3724
  this.frustumPlanes = new Float32Array(24);
3722
3725
  this.uniformData = new Float32Array(24);
@@ -3726,22 +3729,26 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3726
3729
  this._prefabs = options.prefabs ?? null;
3727
3730
  const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
3728
3731
  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);
3732
+ this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? resolvedMaxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
3731
3733
  this.maxBonesPerSkin = options.maxBonesPerSkin ?? (bucketStats ? Math.max(1, bucketStats.maxJointCount) : 64);
3734
+ const cullDist = options.animationCullDistance ?? 50;
3735
+ this.animationCullDistanceSq = cullDist * cullDist;
3732
3736
  this.maxTotalBones = this.maxSkinnedInstances * this.maxBonesPerSkin * 2;
3733
3737
  this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
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);
3738
+ this.boneOffsetRefcount = new Uint32Array(this.maxTotalBones);
3739
+ this.boneOffsetSkinIndex = new Uint32Array(this.maxTotalBones);
3740
+ this.freeList = new FreeList3(resolvedMaxInstances);
3741
+ this.batcher = new SparseBatcher2(resolvedMaxInstances);
3742
+ this.dynamicData = new Float32Array(resolvedMaxInstances * DYNAMIC_MESH_FLOATS);
3743
+ this.staticData = new Float32Array(resolvedMaxInstances * STATIC_MESH_FLOATS);
3744
+ this.slotIndexData = new Uint32Array(resolvedMaxInstances);
3745
+ this.instanceModelIds = new Uint8Array(resolvedMaxInstances);
3740
3746
  const msi = this.maxSkinnedInstances;
3741
3747
  this.skinnedFreeList = new FreeList3(msi);
3742
3748
  this.skinnedBatcher = new SparseBatcher2(msi);
3743
3749
  this.skinnedDynamicData = new Float32Array(msi * DYNAMIC_MESH_FLOATS);
3744
3750
  this.skinnedStaticData = new Float32Array(msi * SKINNED_STATIC_MESH_FLOATS);
3751
+ this.skinnedStaticDV = new DataView(this.skinnedStaticData.buffer);
3745
3752
  this.skinnedSlotIndexData = new Uint32Array(msi);
3746
3753
  this.skinnedInstanceModelIds = new Uint8Array(msi);
3747
3754
  this.skinnedInstanceBoneOffsets = new Uint32Array(msi);
@@ -3752,7 +3759,19 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3752
3759
  this.gpuInstDV = new DataView(this.gpuInstData.buffer);
3753
3760
  }
3754
3761
  async init() {
3755
- this.root = await tgpu6.init();
3762
+ const adapter = await navigator.gpu.requestAdapter();
3763
+ if (!adapter)
3764
+ throw new Error("WebGPU3DRenderer: no GPU adapter available");
3765
+ const a = adapter.limits;
3766
+ const requiredLimits = {
3767
+ maxBufferSize: a.maxBufferSize,
3768
+ maxStorageBufferBindingSize: a.maxStorageBufferBindingSize,
3769
+ maxStorageBuffersPerShaderStage: a.maxStorageBuffersPerShaderStage,
3770
+ maxComputeWorkgroupStorageSize: a.maxComputeWorkgroupStorageSize,
3771
+ maxComputeInvocationsPerWorkgroup: a.maxComputeInvocationsPerWorkgroup
3772
+ };
3773
+ const device = await adapter.requestDevice({ requiredLimits });
3774
+ this.root = tgpu6.initFromDevice({ device });
3756
3775
  this.device = this.root.device;
3757
3776
  this.context = this.canvas.getContext("webgpu");
3758
3777
  this.format = navigator.gpu.getPreferredCanvasFormat();
@@ -3769,7 +3788,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3769
3788
  format: "depth24plus",
3770
3789
  usage: GPUTextureUsage.RENDER_ATTACHMENT
3771
3790
  });
3772
- this.meshLayout = createMeshLayout(this.maxModels);
3791
+ this.meshLayout = createMeshLayout(this.maxInstances);
3773
3792
  const depthStencil = {
3774
3793
  format: "depth24plus",
3775
3794
  depthWriteEnabled: true,
@@ -3816,10 +3835,10 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3816
3835
  primitive,
3817
3836
  depthStencil
3818
3837
  });
3819
- this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.maxModels)).$usage("storage");
3820
- this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxModels)).$usage("storage");
3838
+ this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.maxInstances)).$usage("storage");
3839
+ this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxInstances)).$usage("storage");
3821
3840
  this.uniformBuffer = this.root.createBuffer(MeshUniforms).$usage("uniform");
3822
- this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxModels)).$usage("storage");
3841
+ this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxInstances)).$usage("storage");
3823
3842
  this.rawDynamicBuffer = this.root.unwrap(this.dynamicBuffer);
3824
3843
  this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
3825
3844
  this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
@@ -3921,6 +3940,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3921
3940
  lineWidth: prefab.lineWidth
3922
3941
  });
3923
3942
  prefabHandles.set(prefab, model);
3943
+ } else if (prefab.type === "cube") {
3944
+ const model = this.createCube({ size: prefab.size });
3945
+ prefabHandles.set(prefab, model);
3924
3946
  }
3925
3947
  }
3926
3948
  }
@@ -3985,6 +4007,36 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
3985
4007
  createCompute(name, options) {
3986
4008
  return new ComputeBuilder(name, options, this.root);
3987
4009
  }
4010
+ /**
4011
+ * Set the maximum distance (in world units) at which the renderer keeps
4012
+ * computing skeletal animation. Skinned instances farther than this are
4013
+ * still drawn, but with their last-computed bone matrices instead of
4014
+ * fresh ones, which saves GPU compute work. Their internal animation
4015
+ * clocks keep ticking on CPU, so when they come back into range they
4016
+ * resume in sync.
4017
+ *
4018
+ * Lower values trade visual smoothness on distant characters for FPS.
4019
+ * Pass `Infinity` to disable culling entirely (always animate).
4020
+ *
4021
+ * Safe to call any time; takes effect on the next frame.
4022
+ *
4023
+ * @param distance Max distance to animate at, in world units.
4024
+ */
4025
+ setAnimationCullDistance(distance) {
4026
+ this.animationCullDistanceSq = distance * distance;
4027
+ }
4028
+ /** Current animation cull distance (in world units). See `setAnimationCullDistance`. */
4029
+ get animationCullDistance() {
4030
+ return Math.sqrt(this.animationCullDistanceSq);
4031
+ }
4032
+ /**
4033
+ * Max skinned instances the renderer was sized for at construction.
4034
+ * Independent budget from `maxInstances` since skinned characters use a
4035
+ * separate set of GPU buffers. Read-only.
4036
+ */
4037
+ get maxSkinned() {
4038
+ return this.maxSkinnedInstances;
4039
+ }
3988
4040
  /**
3989
4041
  * Create a flat grid mesh on the XZ plane at Y=0.
3990
4042
  *
@@ -4016,6 +4068,245 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4016
4068
  indices: new Uint16Array(indices)
4017
4069
  });
4018
4070
  }
4071
+ /**
4072
+ * Create a unit-cube mesh centered at the origin. Pass `size` to scale the
4073
+ * edge length, or keep size = 1 and scale at the instance level.
4074
+ *
4075
+ * ```ts
4076
+ * const cube = renderer.createCube();
4077
+ * renderer.addInstance({ model: cube, color: [1, 0.5, 0.2], scale: 2 });
4078
+ * ```
4079
+ */
4080
+ createCube(opts = {}) {
4081
+ const h = (opts.size ?? 1) / 2;
4082
+ const positions = new Float32Array([
4083
+ // +Z
4084
+ -h,
4085
+ -h,
4086
+ h,
4087
+ h,
4088
+ -h,
4089
+ h,
4090
+ h,
4091
+ h,
4092
+ h,
4093
+ -h,
4094
+ -h,
4095
+ h,
4096
+ h,
4097
+ h,
4098
+ h,
4099
+ -h,
4100
+ h,
4101
+ h,
4102
+ // -Z
4103
+ h,
4104
+ -h,
4105
+ -h,
4106
+ -h,
4107
+ -h,
4108
+ -h,
4109
+ -h,
4110
+ h,
4111
+ -h,
4112
+ h,
4113
+ -h,
4114
+ -h,
4115
+ -h,
4116
+ h,
4117
+ -h,
4118
+ h,
4119
+ h,
4120
+ -h,
4121
+ // +Y
4122
+ -h,
4123
+ h,
4124
+ h,
4125
+ h,
4126
+ h,
4127
+ h,
4128
+ h,
4129
+ h,
4130
+ -h,
4131
+ -h,
4132
+ h,
4133
+ h,
4134
+ h,
4135
+ h,
4136
+ -h,
4137
+ -h,
4138
+ h,
4139
+ -h,
4140
+ // -Y
4141
+ -h,
4142
+ -h,
4143
+ -h,
4144
+ h,
4145
+ -h,
4146
+ -h,
4147
+ h,
4148
+ -h,
4149
+ h,
4150
+ -h,
4151
+ -h,
4152
+ -h,
4153
+ h,
4154
+ -h,
4155
+ h,
4156
+ -h,
4157
+ -h,
4158
+ h,
4159
+ // +X
4160
+ h,
4161
+ -h,
4162
+ h,
4163
+ h,
4164
+ -h,
4165
+ -h,
4166
+ h,
4167
+ h,
4168
+ -h,
4169
+ h,
4170
+ -h,
4171
+ h,
4172
+ h,
4173
+ h,
4174
+ -h,
4175
+ h,
4176
+ h,
4177
+ h,
4178
+ // -X
4179
+ -h,
4180
+ -h,
4181
+ -h,
4182
+ -h,
4183
+ -h,
4184
+ h,
4185
+ -h,
4186
+ h,
4187
+ h,
4188
+ -h,
4189
+ -h,
4190
+ -h,
4191
+ -h,
4192
+ h,
4193
+ h,
4194
+ -h,
4195
+ h,
4196
+ -h
4197
+ ]);
4198
+ const normals = new Float32Array([
4199
+ 0,
4200
+ 0,
4201
+ 1,
4202
+ 0,
4203
+ 0,
4204
+ 1,
4205
+ 0,
4206
+ 0,
4207
+ 1,
4208
+ 0,
4209
+ 0,
4210
+ 1,
4211
+ 0,
4212
+ 0,
4213
+ 1,
4214
+ 0,
4215
+ 0,
4216
+ 1,
4217
+ 0,
4218
+ 0,
4219
+ -1,
4220
+ 0,
4221
+ 0,
4222
+ -1,
4223
+ 0,
4224
+ 0,
4225
+ -1,
4226
+ 0,
4227
+ 0,
4228
+ -1,
4229
+ 0,
4230
+ 0,
4231
+ -1,
4232
+ 0,
4233
+ 0,
4234
+ -1,
4235
+ 0,
4236
+ 1,
4237
+ 0,
4238
+ 0,
4239
+ 1,
4240
+ 0,
4241
+ 0,
4242
+ 1,
4243
+ 0,
4244
+ 0,
4245
+ 1,
4246
+ 0,
4247
+ 0,
4248
+ 1,
4249
+ 0,
4250
+ 0,
4251
+ 1,
4252
+ 0,
4253
+ 0,
4254
+ -1,
4255
+ 0,
4256
+ 0,
4257
+ -1,
4258
+ 0,
4259
+ 0,
4260
+ -1,
4261
+ 0,
4262
+ 0,
4263
+ -1,
4264
+ 0,
4265
+ 0,
4266
+ -1,
4267
+ 0,
4268
+ 0,
4269
+ -1,
4270
+ 0,
4271
+ 1,
4272
+ 0,
4273
+ 0,
4274
+ 1,
4275
+ 0,
4276
+ 0,
4277
+ 1,
4278
+ 0,
4279
+ 0,
4280
+ 1,
4281
+ 0,
4282
+ 0,
4283
+ 1,
4284
+ 0,
4285
+ 0,
4286
+ 1,
4287
+ 0,
4288
+ 0,
4289
+ -1,
4290
+ 0,
4291
+ 0,
4292
+ -1,
4293
+ 0,
4294
+ 0,
4295
+ -1,
4296
+ 0,
4297
+ 0,
4298
+ -1,
4299
+ 0,
4300
+ 0,
4301
+ -1,
4302
+ 0,
4303
+ 0,
4304
+ -1,
4305
+ 0,
4306
+ 0
4307
+ ]);
4308
+ return this.loadModel({ positions, normals });
4309
+ }
4019
4310
  /**
4020
4311
  * Register a model. Returns a handle for addInstance().
4021
4312
  *
@@ -4331,6 +4622,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4331
4622
  * with another instance (e.g., when spawning all parts of a character).
4332
4623
  */
4333
4624
  addInstance(opts) {
4625
+ if (isPrefab3D(opts.model) && opts.model.type === "composite") {
4626
+ return this.addCompositeInstance(opts, opts.model);
4627
+ }
4334
4628
  const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
4335
4629
  if ("parts" in modelOrGltf) {
4336
4630
  return this.addGltfInstance(opts, modelOrGltf);
@@ -4342,7 +4636,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4342
4636
  }
4343
4637
  const slot = this.freeList.allocate();
4344
4638
  if (slot === -1)
4345
- throw new Error(`Max instances (${this.maxModels}) reached`);
4639
+ throw new Error(`Max instances (${this.maxInstances}) reached`);
4346
4640
  const dynBase = slot * DYNAMIC_MESH_FLOATS;
4347
4641
  const statBase = slot * STATIC_MESH_FLOATS;
4348
4642
  const t = resolveTransform(opts);
@@ -4369,7 +4663,11 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4369
4663
  this.batcher.add(0, modelHandle.id, slot);
4370
4664
  const dynamicData = this.dynamicData;
4371
4665
  const staticData = this.staticData;
4372
- return {
4666
+ const self = this;
4667
+ let destroyed = false;
4668
+ const handle = {
4669
+ slot,
4670
+ modelId: modelHandle.id,
4373
4671
  skinned: false,
4374
4672
  setPosition(nx, ny, nz) {
4375
4673
  dynamicData[dynBase + DYN_CURR_PX] = nx;
@@ -4385,8 +4683,19 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4385
4683
  staticData[statBase + STAT_SX] = nx;
4386
4684
  staticData[statBase + STAT_SY] = ny;
4387
4685
  staticData[statBase + STAT_SZ] = nz;
4686
+ },
4687
+ destroy() {
4688
+ if (destroyed)
4689
+ return;
4690
+ destroyed = true;
4691
+ self.batcher.remove(0, modelHandle.id, slot);
4692
+ self.freeList.free(slot);
4693
+ dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4694
+ staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
4695
+ self.staticDirty = true;
4388
4696
  }
4389
4697
  };
4698
+ return handle;
4390
4699
  }
4391
4700
  addGltfInstance(opts, gltf) {
4392
4701
  const childHandles = [];
@@ -4424,7 +4733,71 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4424
4733
  } : void 0,
4425
4734
  stop: skinnedHandle?.stop ? () => {
4426
4735
  skinnedHandle.stop();
4427
- } : void 0
4736
+ } : void 0,
4737
+ destroy() {
4738
+ for (const h of childHandles)
4739
+ h.destroy();
4740
+ }
4741
+ };
4742
+ }
4743
+ /**
4744
+ * Spawn a composite prefab by spawning each of its parts at the composed
4745
+ * (instance + offset) transform. The returned handle broadcasts subsequent
4746
+ * `setPosition` / `setRotation` to every child, keeping each child's
4747
+ * baked offset applied on top of the new value.
4748
+ */
4749
+ addCompositeInstance(opts, composite) {
4750
+ const bucket = this._prefabs;
4751
+ if (!bucket) {
4752
+ throw new Error(
4753
+ `addInstance: composite '${composite.id}' requires the renderer to be constructed with the bucket (\`prefabs\`).`
4754
+ );
4755
+ }
4756
+ const basePos = opts.position ?? [0, 0, 0];
4757
+ const baseRot = opts.rotation ?? [0, 0, 0];
4758
+ const offsets = composite.parts.map((p) => ({
4759
+ px: p.offset?.position?.[0] ?? 0,
4760
+ py: p.offset?.position?.[1] ?? 0,
4761
+ pz: p.offset?.position?.[2] ?? 0,
4762
+ rx: p.offset?.rotation?.[0] ?? 0,
4763
+ ry: p.offset?.rotation?.[1] ?? 0,
4764
+ rz: p.offset?.rotation?.[2] ?? 0
4765
+ }));
4766
+ const childHandles = [];
4767
+ for (let i = 0; i < composite.parts.length; i++) {
4768
+ const part = composite.parts[i];
4769
+ const off = offsets[i];
4770
+ const partPrefab = bucket.get(part.partId);
4771
+ const partOpts = {
4772
+ ...opts,
4773
+ model: partPrefab,
4774
+ position: [basePos[0] + off.px, basePos[1] + off.py, basePos[2] + off.pz],
4775
+ rotation: [baseRot[0] + off.rx, baseRot[1] + off.ry, baseRot[2] + off.rz]
4776
+ };
4777
+ childHandles.push(this.addInstance(partOpts));
4778
+ }
4779
+ return {
4780
+ skinned: childHandles.some((h) => h.skinned),
4781
+ setPosition(x, y, z) {
4782
+ for (let i = 0; i < childHandles.length; i++) {
4783
+ const o = offsets[i];
4784
+ childHandles[i].setPosition(x + o.px, y + o.py, z + o.pz);
4785
+ }
4786
+ },
4787
+ setRotation(x, y, z) {
4788
+ for (let i = 0; i < childHandles.length; i++) {
4789
+ const o = offsets[i];
4790
+ childHandles[i].setRotation(x + o.rx, y + o.ry, z + o.rz);
4791
+ }
4792
+ },
4793
+ setScale(x, y, z) {
4794
+ for (const h of childHandles)
4795
+ h.setScale(x, y, z);
4796
+ },
4797
+ destroy() {
4798
+ for (const h of childHandles)
4799
+ h.destroy();
4800
+ }
4428
4801
  };
4429
4802
  }
4430
4803
  addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot) {
@@ -4438,11 +4811,27 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4438
4811
  if (linkedSlot !== void 0) {
4439
4812
  boneOffset = this.skinnedInstanceBoneOffsets[linkedSlot];
4440
4813
  animState = this.skinnedAnimStates[linkedSlot];
4814
+ this.boneOffsetRefcount[boneOffset]++;
4441
4815
  } else {
4442
- boneOffset = this.nextBoneOffset + jointCount;
4443
- this.nextBoneOffset += jointCount * 2;
4444
- skinModel.animation.computeRestPose(this.boneMatrixData, boneOffset * 16);
4445
- this.boneMatrixDirty = true;
4816
+ const pool = this.freedBoneOffsets.get(skinIndex);
4817
+ if (pool && pool.length > 0) {
4818
+ boneOffset = pool.pop();
4819
+ } else {
4820
+ boneOffset = this.nextBoneOffset + jointCount;
4821
+ this.nextBoneOffset += jointCount * 2;
4822
+ }
4823
+ this.boneOffsetRefcount[boneOffset] = 1;
4824
+ this.boneOffsetSkinIndex[boneOffset] = skinIndex;
4825
+ const restOffsetFloats = boneOffset * 16;
4826
+ const restLengthFloats = jointCount * 16;
4827
+ skinModel.animation.computeRestPose(this.boneMatrixData, restOffsetFloats);
4828
+ this.device.queue.writeBuffer(
4829
+ this.rawBoneMatrixBuffer,
4830
+ restOffsetFloats * 4,
4831
+ this.boneMatrixData.buffer,
4832
+ this.boneMatrixData.byteOffset + restOffsetFloats * 4,
4833
+ restLengthFloats * 4
4834
+ );
4446
4835
  animState = skinModel.animation.clipCount > 0 ? skinModel.animation.createState(0, 1, true) : null;
4447
4836
  }
4448
4837
  this.skinnedInstanceBoneOffsets[slot] = boneOffset;
@@ -4468,11 +4857,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4468
4857
  this.skinnedStaticData[statBase + SSTAT_CR] = t.cr;
4469
4858
  this.skinnedStaticData[statBase + SSTAT_CG] = t.cg;
4470
4859
  this.skinnedStaticData[statBase + SSTAT_CB] = t.cb;
4471
- new DataView(this.skinnedStaticData.buffer).setUint32(
4472
- (statBase + SSTAT_BONE_OFFSET) * 4,
4473
- boneOffset,
4474
- true
4475
- );
4860
+ this.skinnedStaticDV.setUint32((statBase + SSTAT_BONE_OFFSET) * 4, boneOffset, true);
4476
4861
  this.skinnedStaticDirty = true;
4477
4862
  this.skinnedInstanceModelIds[slot] = modelHandle.id;
4478
4863
  this.skinnedBatcher.add(0, modelHandle.id, slot);
@@ -4480,6 +4865,10 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4480
4865
  const staticData = this.skinnedStaticData;
4481
4866
  const animStates = this.skinnedAnimStates;
4482
4867
  const animation = skinModel.animation;
4868
+ const self = this;
4869
+ const capturedBoneOffset = boneOffset;
4870
+ const capturedSkinIndex = skinIndex;
4871
+ let destroyed = false;
4483
4872
  return {
4484
4873
  slot,
4485
4874
  modelId: modelHandle.id,
@@ -4508,6 +4897,25 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4508
4897
  const state = animStates[slot];
4509
4898
  if (state)
4510
4899
  animation.stop(state);
4900
+ },
4901
+ destroy() {
4902
+ if (destroyed)
4903
+ return;
4904
+ destroyed = true;
4905
+ self.skinnedBatcher.remove(0, modelHandle.id, slot);
4906
+ self.skinnedFreeList.free(slot);
4907
+ dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4908
+ staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
4909
+ animStates[slot] = null;
4910
+ self.skinnedStaticDirty = true;
4911
+ if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
4912
+ let pool = self.freedBoneOffsets.get(capturedSkinIndex);
4913
+ if (!pool) {
4914
+ pool = [];
4915
+ self.freedBoneOffsets.set(capturedSkinIndex, pool);
4916
+ }
4917
+ pool.push(capturedBoneOffset);
4918
+ }
4511
4919
  }
4512
4920
  };
4513
4921
  }
@@ -4593,6 +5001,31 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4593
5001
  this.animJointLookupOffset = pb.jointLookupOffset;
4594
5002
  return true;
4595
5003
  }
5004
+ /**
5005
+ * Returns true if a skinned instance's bone-matrix compute should be
5006
+ * dispatched this frame. Combines frustum culling (scaled bounding sphere)
5007
+ * and a configurable distance cull from the camera. Callers pass the
5008
+ * camera position + cullDistSq once outside the loop to avoid re-reading.
5009
+ */
5010
+ shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq) {
5011
+ const skinModel = model && model.skinIndex >= 0 ? this.skinnedModels[model.skinIndex] : null;
5012
+ const baseRadius = skinModel?.boundingRadius ?? 10;
5013
+ const base = slot * DYNAMIC_MESH_FLOATS;
5014
+ const sBase = slot * SKINNED_STATIC_MESH_FLOATS;
5015
+ const cx = this.skinnedDynamicData[base + DYN_CURR_PX];
5016
+ const cy = this.skinnedDynamicData[base + DYN_CURR_PY];
5017
+ const cz = this.skinnedDynamicData[base + DYN_CURR_PZ];
5018
+ const sx = Math.abs(this.skinnedStaticData[sBase + SSTAT_SX]);
5019
+ const sy = Math.abs(this.skinnedStaticData[sBase + SSTAT_SY]);
5020
+ const sz = Math.abs(this.skinnedStaticData[sBase + SSTAT_SZ]);
5021
+ const maxScale = sx > sy ? sx > sz ? sx : sz : sy > sz ? sy : sz;
5022
+ if (!this.isInFrustum(cx, cy, cz, baseRadius * maxScale))
5023
+ return false;
5024
+ const dxv = cx - camX, dyv = cy - camY, dzv = cz - camZ;
5025
+ if (dxv * dxv + dyv * dyv + dzv * dzv > cullDistSq)
5026
+ return false;
5027
+ return true;
5028
+ }
4596
5029
  updateAnimations(deltaTime) {
4597
5030
  this.syncLazyAnimationChanges();
4598
5031
  if (this.animComputeNeedsRebuild && this.packedAnimData.clips.length > 0) {
@@ -4636,6 +5069,9 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4636
5069
  this.updatedBoneOffsets.fill(0);
4637
5070
  let count = 0;
4638
5071
  const dv = this.gpuInstDV;
5072
+ const camPos = this.camera.position;
5073
+ const camX = camPos[0], camY = camPos[1], camZ = camPos[2];
5074
+ const cullDistSq = this.animationCullDistanceSq;
4639
5075
  for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
4640
5076
  const animState = this.skinnedAnimStates[slot];
4641
5077
  if (!animState)
@@ -4674,6 +5110,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4674
5110
  const modelId = this.skinnedInstanceModelIds[slot];
4675
5111
  const model = this.models[modelId];
4676
5112
  const skinIdx = model?.skinIndex ?? 0;
5113
+ if (!this.shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq))
5114
+ continue;
4677
5115
  const off = count * 32;
4678
5116
  dv.setInt32(off, animState.clipId, true);
4679
5117
  dv.setFloat32(off + 4, animState.time, true);
@@ -4718,14 +5156,12 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
4718
5156
  }
4719
5157
  }
4720
5158
  }
5159
+ /**
5160
+ * Free an instance's renderer slot. Equivalent to `handle.destroy()` -
5161
+ * kept as a convenience for direct lookup. Safe to call multiple times.
5162
+ */
4721
5163
  removeInstance(handle) {
4722
- this.batcher.remove(0, handle.modelId, handle.slot);
4723
- this.freeList.free(handle.slot);
4724
- const dynBase = handle.slot * DYNAMIC_MESH_FLOATS;
4725
- const statBase = handle.slot * STATIC_MESH_FLOATS;
4726
- this.dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
4727
- this.staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
4728
- this.staticDirty = true;
5164
+ handle.destroy();
4729
5165
  }
4730
5166
  storePreviousState() {
4731
5167
  this.camera.storePrevious();