@adatechnology/game-vfx-three 0.3.4 → 0.3.6

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.
package/dist/index.d.ts CHANGED
@@ -49,6 +49,12 @@ interface VfxRendererOptions {
49
49
  pointScale?: number;
50
50
  /** Multiplies the blast and fire lights. Default `Math.PI`: three.js r155+ lights are physical, the Lab's were not (r128). */
51
51
  lightScale?: number;
52
+ /** Caps on the marks kept on the ground; once one is full the oldest is reused for the next, so nothing grows and nothing is allocated. Defaults: 64 scorch and dirt, 512 rocks, 256 blood. */
53
+ markLimits?: {
54
+ readonly maxGroundMarks?: number;
55
+ readonly maxRocks?: number;
56
+ readonly maxBlood?: number;
57
+ };
52
58
  }
53
59
  /** The world's counters flattened (`lastBlast` becomes `last_puffs`, `last_sparks`, `last_chunks`, `last_lights`), plus `drawCalls`. */
54
60
  type VfxRendererStats = Readonly<Record<string, number>>;
@@ -146,7 +152,9 @@ interface MarkLayer {
146
152
  /** The shared rock and plate shapes, so the debris and the rim rocks use the same geometry. */
147
153
  handle(event: MarkEvent): void;
148
154
  update(dt: number): void;
149
- /** The meshes in the group right now. */
155
+ /** Puts the instanced meshes in the scene now, so their shader programs link while loading instead of at the first mark. */
156
+ prepare(): void;
157
+ /** The draw calls the marks cost right now. */
150
158
  count(): number;
151
159
  dispose(): void;
152
160
  }
@@ -155,6 +163,12 @@ interface MarkLayerOptions {
155
163
  readonly createCanvas: (() => CanvasLike) | undefined;
156
164
  readonly rockGeometry: THREE_NS.BufferGeometry;
157
165
  readonly plateGeometry: THREE_NS.BufferGeometry;
166
+ /** Scorch and dirt meshes alive at once; the oldest is reused for the next. Default 64. */
167
+ readonly maxGroundMarks?: number;
168
+ /** Rim rocks and plates alive at once (each shape has its own instanced mesh of this size); the oldest is overwritten. Default 512. */
169
+ readonly maxRocks?: number;
170
+ /** Blood pools alive at once; the oldest is overwritten. Default 256. */
171
+ readonly maxBlood?: number;
158
172
  }
159
173
  /**
160
174
  * The scorch, dirt, rim rocks and blood of the Lab as meshes (L443-476). A scorch or dirt mesh follows the ground as it was the moment the mark was
@@ -6377,6 +6377,24 @@ var GameVfxThree = (() => {
6377
6377
  for (const decal2 of this.decals) decal2.on = false;
6378
6378
  this.count = 0;
6379
6379
  }
6380
+ /**
6381
+ * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
6382
+ * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
6383
+ */
6384
+ clearInside(center, half, rotation = [0, 0, 0, 1]) {
6385
+ const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
6386
+ let gone = 0;
6387
+ for (const decal2 of this.decals) {
6388
+ if (!decal2.on) continue;
6389
+ const vx2 = decal2.x - center[0], vy2 = decal2.y - center[1], vz2 = decal2.z - center[2];
6390
+ const tx = 2 * (qy * vz2 - qz * vy2), ty = 2 * (qz * vx2 - qx * vz2), tz = 2 * (qx * vy2 - qy * vx2);
6391
+ const lx = vx2 + qw * tx + (qy * tz - qz * ty), ly = vy2 + qw * ty + (qz * tx - qx * tz), lz = vz2 + qw * tz + (qx * ty - qy * tx);
6392
+ if (Math.abs(lx) > half[0] || Math.abs(ly) > half[1] || Math.abs(lz) > half[2]) continue;
6393
+ decal2.on = false;
6394
+ gone++;
6395
+ }
6396
+ return gone;
6397
+ }
6380
6398
  update(dt) {
6381
6399
  this.count = 0;
6382
6400
  for (const decal2 of this.decals) {
@@ -8505,6 +8523,11 @@ void main(){
8505
8523
  var MAX_CONFORM_STEPS = 48;
8506
8524
  var MIN_CONFORM_STEPS = 16;
8507
8525
  var BLOOD_GROW_RATE = 1.2;
8526
+ var BLOOD_START_GROWTH = 0.05;
8527
+ var DEFAULT_MAX_GROUND_MARKS = 64;
8528
+ var DEFAULT_MAX_ROCKS = 512;
8529
+ var DEFAULT_MAX_BLOOD = 256;
8530
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
8508
8531
  function createMarkLayer(THREE, options) {
8509
8532
  const group = new THREE.Group();
8510
8533
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -8528,92 +8551,146 @@ void main(){
8528
8551
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
8529
8552
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
8530
8553
  owned.push(bloodMaterial, bloodGeometry);
8531
- const rockMaterials = /* @__PURE__ */ new Map();
8532
- const blood = [];
8533
- let meshCount = 0;
8534
- function rockMaterial(color) {
8535
- let material = rockMaterials.get(color);
8536
- if (!material) {
8537
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
8538
- rockMaterials.set(color, material);
8539
- owned.push(material);
8554
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
8555
+ const instanced = {};
8556
+ function rockMaterial() {
8557
+ if (!instanced.rockMaterial) {
8558
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
8559
+ owned.push(instanced.rockMaterial);
8540
8560
  }
8541
- return material;
8561
+ return instanced.rockMaterial;
8562
+ }
8563
+ function bloodMesh() {
8564
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
8565
+ }
8566
+ function rockMesh() {
8567
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
8568
+ }
8569
+ function plateMesh() {
8570
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
8571
+ }
8572
+ function attach(mesh2, castShadow = false) {
8573
+ mesh2.castShadow = castShadow;
8574
+ group.add(mesh2);
8575
+ return mesh2;
8576
+ }
8577
+ const groundMarks = [];
8578
+ const spareGeometries = [];
8579
+ const blood = [];
8580
+ const placed = { blood: 0, rock: 0, plate: 0 };
8581
+ const matrix = new THREE.Matrix4();
8582
+ const position = new THREE.Vector3();
8583
+ const quaternion = new THREE.Quaternion();
8584
+ const euler = new THREE.Euler();
8585
+ const scale = new THREE.Vector3();
8586
+ const color = new THREE.Color();
8587
+ const up = new THREE.Vector3(0, 1, 0);
8588
+ function createInstanced(geometry, material, capacity) {
8589
+ const mesh2 = new THREE.InstancedMesh(geometry, material, capacity);
8590
+ mesh2.count = 0;
8591
+ mesh2.frustumCulled = false;
8592
+ mesh2.receiveShadow = true;
8593
+ return mesh2;
8594
+ }
8595
+ function takeSpare(vertexCount) {
8596
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
8597
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
8598
+ }
8599
+ function evictOldestGroundMark() {
8600
+ const oldest = groundMarks.shift();
8601
+ group.remove(oldest.mesh);
8602
+ spareGeometries.push(oldest.mesh.geometry);
8603
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
8542
8604
  }
8543
8605
  function conform(mark, material) {
8544
8606
  const radius = mark.radius;
8545
8607
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
8546
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8547
- const position = geometry.attributes.position;
8608
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
8609
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8610
+ const grid = geometry.attributes.position;
8611
+ const cell = 2 / steps;
8548
8612
  const cos2 = Math.cos(mark.rotation);
8549
8613
  const sin2 = Math.sin(mark.rotation);
8550
- for (let index = 0; index < position.count; index++) {
8551
- const localX = position.getX(index) * radius;
8552
- const localZ = position.getZ(index) * radius;
8614
+ for (let index = 0; index < grid.count; index++) {
8615
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
8616
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
8553
8617
  const worldX = mark.x + localX * cos2 - localZ * sin2;
8554
8618
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
8555
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8619
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8556
8620
  }
8621
+ grid.needsUpdate = true;
8557
8622
  geometry.computeVertexNormals();
8623
+ geometry.attributes.normal.needsUpdate = true;
8624
+ geometry.boundingSphere = null;
8625
+ geometry.boundingBox = null;
8558
8626
  const mesh2 = new THREE.Mesh(geometry, material);
8559
8627
  mesh2.receiveShadow = true;
8560
- return mesh2;
8628
+ group.add(mesh2);
8629
+ groundMarks.push({ mesh: mesh2, steps });
8561
8630
  }
8562
8631
  function addRock(mark) {
8563
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
8564
- mesh2.position.set(mark.x, mark.y, mark.z);
8565
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
8566
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
8567
- mesh2.castShadow = true;
8568
- mesh2.receiveShadow = true;
8569
- return mesh2;
8632
+ const mesh2 = mark.plate ? plateMesh() : rockMesh();
8633
+ const slot = mark.plate ? "plate" : "rock";
8634
+ const index = placed[slot]++ % mesh2.instanceMatrix.count;
8635
+ quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
8636
+ mesh2.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
8637
+ mesh2.setColorAt(index, color.set(mark.color));
8638
+ mesh2.count = Math.min(placed[slot], mesh2.instanceMatrix.count);
8639
+ mesh2.instanceMatrix.needsUpdate = true;
8640
+ mesh2.instanceColor.needsUpdate = true;
8641
+ }
8642
+ function writeBlood(index, drawn, growth) {
8643
+ quaternion.setFromAxisAngle(up, drawn.rotation);
8644
+ const mesh2 = bloodMesh();
8645
+ mesh2.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion, scale.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
8646
+ mesh2.instanceMatrix.needsUpdate = true;
8570
8647
  }
8571
8648
  function addBlood(mark) {
8572
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
8573
- mesh2.position.set(mark.x, mark.y, mark.z);
8574
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
8575
- mesh2.rotation.y = mark.rotation;
8576
- mesh2.receiveShadow = true;
8577
- mesh2.scale.multiplyScalar(0.05);
8578
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
8579
- return mesh2;
8649
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
8650
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
8651
+ blood[index] = drawn;
8652
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
8653
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
8580
8654
  }
8581
8655
  function build(mark) {
8582
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
8583
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
8584
- if (mark.kind === "rock") return addRock(mark);
8585
- return addBlood(mark);
8586
- }
8587
- function clearMeshes() {
8588
- for (const child of [...group.children]) {
8589
- group.remove(child);
8590
- const geometry = child.geometry;
8591
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
8592
- }
8656
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
8657
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
8658
+ else if (mark.kind === "rock") addRock(mark);
8659
+ else addBlood(mark);
8660
+ }
8661
+ function clearMarks() {
8662
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
8663
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
8664
+ groundMarks.length = 0;
8665
+ for (const geometry of spareGeometries) geometry.dispose();
8666
+ spareGeometries.length = 0;
8593
8667
  blood.length = 0;
8594
- meshCount = 0;
8668
+ placed.blood = placed.rock = placed.plate = 0;
8669
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh2) mesh2.count = 0;
8595
8670
  }
8596
8671
  return {
8597
8672
  group,
8598
8673
  handle(event) {
8599
- if (event.type === "clear") {
8600
- clearMeshes();
8601
- return;
8602
- }
8603
- group.add(build(event.mark));
8604
- meshCount++;
8674
+ if (event.type === "clear") clearMarks();
8675
+ else build(event.mark);
8676
+ },
8677
+ prepare() {
8678
+ bloodMesh();
8679
+ rockMesh();
8680
+ plateMesh();
8605
8681
  },
8606
8682
  update(dt) {
8607
- for (const drawn of blood) {
8608
- if (drawn.age > 1) continue;
8683
+ for (let index = 0; index < blood.length; index++) {
8684
+ const drawn = blood[index];
8685
+ if (!drawn || drawn.age > 1) continue;
8609
8686
  drawn.age += dt * BLOOD_GROW_RATE;
8610
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
8611
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
8687
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
8612
8688
  }
8613
8689
  },
8614
- count: () => meshCount,
8690
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
8615
8691
  dispose() {
8616
- clearMeshes();
8692
+ clearMarks();
8693
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
8617
8694
  for (const item of owned) item.dispose();
8618
8695
  }
8619
8696
  };
@@ -8622,18 +8699,22 @@ void main(){
8622
8699
  // src/mesh-layer.ts
8623
8700
  var SPHERE_DEFAULT_WIDTH = 8;
8624
8701
  var SPHERE_DEFAULT_HEIGHT = 6;
8702
+ var MAX_PARKED_MESHES_PER_KIND = 256;
8625
8703
  function geometryOf(THREE, particle) {
8626
8704
  switch (particle.shape) {
8627
8705
  case "cone":
8628
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
8706
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
8629
8707
  case "cone_open":
8630
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
8708
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
8631
8709
  case "sphere":
8632
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8710
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8633
8711
  case "icosahedron":
8634
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8712
+ return new THREE.IcosahedronGeometry(1, particle.segments);
8635
8713
  }
8636
8714
  }
8715
+ function isCone(particle) {
8716
+ return particle.shape === "cone" || particle.shape === "cone_open";
8717
+ }
8637
8718
  function materialOf(THREE, particle) {
8638
8719
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
8639
8720
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -8644,51 +8725,82 @@ void main(){
8644
8725
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
8645
8726
  );
8646
8727
  }
8728
+ function geometryKeyOf(particle) {
8729
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
8730
+ }
8647
8731
  function createMeshLayer(THREE, pool) {
8648
8732
  const group = new THREE.Group();
8649
8733
  group.name = "workshop-meshes";
8650
8734
  const drawn = /* @__PURE__ */ new Map();
8735
+ const geometries = /* @__PURE__ */ new Map();
8736
+ const parked = /* @__PURE__ */ new Map();
8651
8737
  const frame = new THREE.Quaternion();
8652
8738
  const euler = new THREE.Euler();
8653
8739
  const turn = new THREE.Quaternion();
8654
8740
  const offset = new THREE.Vector3();
8655
- function release(mesh2) {
8656
- mesh2.removeFromParent();
8657
- mesh2.geometry.dispose();
8658
- mesh2.material.dispose();
8741
+ let stamp = 0;
8742
+ function geometryFor(particle) {
8743
+ const key = geometryKeyOf(particle);
8744
+ let geometry = geometries.get(key);
8745
+ if (!geometry) {
8746
+ geometry = geometryOf(THREE, particle);
8747
+ geometries.set(key, geometry);
8748
+ }
8749
+ return geometry;
8750
+ }
8751
+ function acquire(particle) {
8752
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
8753
+ const mesh2 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
8754
+ mesh2.material.color.set(particle.color);
8755
+ mesh2.frustumCulled = false;
8756
+ mesh2.renderOrder = particle.additive ? 5 : 3;
8757
+ group.add(mesh2);
8758
+ return { mesh: mesh2, parkingKey, stamp };
8759
+ }
8760
+ function park(entry) {
8761
+ entry.mesh.removeFromParent();
8762
+ let free = parked.get(entry.parkingKey);
8763
+ if (!free) {
8764
+ free = [];
8765
+ parked.set(entry.parkingKey, free);
8766
+ }
8767
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
8768
+ else entry.mesh.material.dispose();
8659
8769
  }
8660
8770
  return {
8661
8771
  group,
8662
8772
  sync() {
8663
- const live = /* @__PURE__ */ new Set();
8773
+ stamp++;
8664
8774
  for (const particle of pool.particles) {
8665
- live.add(particle.id);
8666
- let mesh2 = drawn.get(particle.id);
8667
- if (!mesh2) {
8668
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
8669
- mesh2.frustumCulled = false;
8670
- mesh2.renderOrder = particle.additive ? 5 : 3;
8671
- drawn.set(particle.id, mesh2);
8672
- group.add(mesh2);
8775
+ let entry = drawn.get(particle.id);
8776
+ if (!entry) {
8777
+ entry = acquire(particle);
8778
+ drawn.set(particle.id, entry);
8673
8779
  }
8780
+ entry.stamp = stamp;
8781
+ const mesh2 = entry.mesh;
8674
8782
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
8675
8783
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
8676
8784
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
8677
8785
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
8678
8786
  mesh2.quaternion.copy(frame).multiply(turn);
8679
- mesh2.scale.setScalar(particle.scale);
8787
+ mesh2.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
8680
8788
  mesh2.material.opacity = particle.opacity;
8681
8789
  }
8682
- for (const [id, mesh2] of drawn) {
8683
- if (live.has(id)) continue;
8684
- release(mesh2);
8790
+ for (const [id, entry] of drawn) {
8791
+ if (entry.stamp === stamp) continue;
8792
+ park(entry);
8685
8793
  drawn.delete(id);
8686
8794
  }
8687
8795
  return pool.count;
8688
8796
  },
8689
8797
  dispose() {
8690
- for (const mesh2 of drawn.values()) release(mesh2);
8798
+ for (const entry of drawn.values()) park(entry);
8691
8799
  drawn.clear();
8800
+ for (const free of parked.values()) for (const mesh2 of free) mesh2.material.dispose();
8801
+ parked.clear();
8802
+ for (const geometry of geometries.values()) geometry.dispose();
8803
+ geometries.clear();
8692
8804
  group.removeFromParent();
8693
8805
  }
8694
8806
  };
@@ -9273,7 +9385,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9273
9385
  const shocks = createShockLayer(THREE, pools.shocks);
9274
9386
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
9275
9387
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
9276
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
9388
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
9277
9389
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
9278
9390
  const stopMarks = world.onMarks((event) => marks.handle(event));
9279
9391
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -9299,6 +9411,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9299
9411
  const position = options.camera.position;
9300
9412
  world.setCamera([position.x, position.y, position.z]);
9301
9413
  }
9414
+ function attachWorkshopLayers() {
9415
+ const engine = world.peekWorkshop();
9416
+ if (!engine || engine === workshopEngine) return;
9417
+ workshopLayers?.dispose();
9418
+ if (workshopLayers) root.remove(workshopLayers.group);
9419
+ workshopEngine = engine;
9420
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
9421
+ root.add(workshopLayers.group);
9422
+ applyLayers();
9423
+ }
9302
9424
  function syncAll() {
9303
9425
  lastDrawn.puffs = puffs.sync();
9304
9426
  lastDrawn.sparks = sparks.sync();
@@ -9307,15 +9429,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9307
9429
  lastDrawn.shocks = shocks.sync();
9308
9430
  lastDrawn.sprites = sprites.sync();
9309
9431
  lights.sync();
9310
- const engine = world.peekWorkshop();
9311
- if (engine && engine !== workshopEngine) {
9312
- workshopLayers?.dispose();
9313
- if (workshopLayers) root.remove(workshopLayers.group);
9314
- workshopEngine = engine;
9315
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
9316
- root.add(workshopLayers.group);
9317
- applyLayers();
9318
- }
9432
+ attachWorkshopLayers();
9319
9433
  const workshopCalls = workshopLayers?.sync() ?? 0;
9320
9434
  const glow = pools.lights.glow;
9321
9435
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -9368,6 +9482,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9368
9482
  },
9369
9483
  warmUpProbes() {
9370
9484
  releaseAnchors();
9485
+ marks.prepare();
9486
+ world.workshop();
9487
+ attachWorkshopLayers();
9371
9488
  const meshes = createMeshLayerProbes(THREE);
9372
9489
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
9373
9490
  const group = new THREE.Group();