@adatechnology/game-vfx-three 0.3.5 → 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
@@ -8523,6 +8523,11 @@ void main(){
8523
8523
  var MAX_CONFORM_STEPS = 48;
8524
8524
  var MIN_CONFORM_STEPS = 16;
8525
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;
8526
8531
  function createMarkLayer(THREE, options) {
8527
8532
  const group = new THREE.Group();
8528
8533
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -8546,92 +8551,146 @@ void main(){
8546
8551
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
8547
8552
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
8548
8553
  owned.push(bloodMaterial, bloodGeometry);
8549
- const rockMaterials = /* @__PURE__ */ new Map();
8550
- const blood = [];
8551
- let meshCount = 0;
8552
- function rockMaterial(color) {
8553
- let material = rockMaterials.get(color);
8554
- if (!material) {
8555
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
8556
- rockMaterials.set(color, material);
8557
- 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);
8558
8560
  }
8559
- 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();
8560
8604
  }
8561
8605
  function conform(mark, material) {
8562
8606
  const radius = mark.radius;
8563
8607
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
8564
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8565
- 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;
8566
8612
  const cos2 = Math.cos(mark.rotation);
8567
8613
  const sin2 = Math.sin(mark.rotation);
8568
- for (let index = 0; index < position.count; index++) {
8569
- const localX = position.getX(index) * radius;
8570
- 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;
8571
8617
  const worldX = mark.x + localX * cos2 - localZ * sin2;
8572
8618
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
8573
- 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);
8574
8620
  }
8621
+ grid.needsUpdate = true;
8575
8622
  geometry.computeVertexNormals();
8623
+ geometry.attributes.normal.needsUpdate = true;
8624
+ geometry.boundingSphere = null;
8625
+ geometry.boundingBox = null;
8576
8626
  const mesh2 = new THREE.Mesh(geometry, material);
8577
8627
  mesh2.receiveShadow = true;
8578
- return mesh2;
8628
+ group.add(mesh2);
8629
+ groundMarks.push({ mesh: mesh2, steps });
8579
8630
  }
8580
8631
  function addRock(mark) {
8581
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
8582
- mesh2.position.set(mark.x, mark.y, mark.z);
8583
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
8584
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
8585
- mesh2.castShadow = true;
8586
- mesh2.receiveShadow = true;
8587
- 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;
8588
8647
  }
8589
8648
  function addBlood(mark) {
8590
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
8591
- mesh2.position.set(mark.x, mark.y, mark.z);
8592
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
8593
- mesh2.rotation.y = mark.rotation;
8594
- mesh2.receiveShadow = true;
8595
- mesh2.scale.multiplyScalar(0.05);
8596
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
8597
- 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);
8598
8654
  }
8599
8655
  function build(mark) {
8600
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
8601
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
8602
- if (mark.kind === "rock") return addRock(mark);
8603
- return addBlood(mark);
8604
- }
8605
- function clearMeshes() {
8606
- for (const child of [...group.children]) {
8607
- group.remove(child);
8608
- const geometry = child.geometry;
8609
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
8610
- }
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;
8611
8667
  blood.length = 0;
8612
- 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;
8613
8670
  }
8614
8671
  return {
8615
8672
  group,
8616
8673
  handle(event) {
8617
- if (event.type === "clear") {
8618
- clearMeshes();
8619
- return;
8620
- }
8621
- group.add(build(event.mark));
8622
- meshCount++;
8674
+ if (event.type === "clear") clearMarks();
8675
+ else build(event.mark);
8676
+ },
8677
+ prepare() {
8678
+ bloodMesh();
8679
+ rockMesh();
8680
+ plateMesh();
8623
8681
  },
8624
8682
  update(dt) {
8625
- for (const drawn of blood) {
8626
- 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;
8627
8686
  drawn.age += dt * BLOOD_GROW_RATE;
8628
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
8629
- 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)));
8630
8688
  }
8631
8689
  },
8632
- count: () => meshCount,
8690
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
8633
8691
  dispose() {
8634
- clearMeshes();
8692
+ clearMarks();
8693
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
8635
8694
  for (const item of owned) item.dispose();
8636
8695
  }
8637
8696
  };
@@ -8640,18 +8699,22 @@ void main(){
8640
8699
  // src/mesh-layer.ts
8641
8700
  var SPHERE_DEFAULT_WIDTH = 8;
8642
8701
  var SPHERE_DEFAULT_HEIGHT = 6;
8702
+ var MAX_PARKED_MESHES_PER_KIND = 256;
8643
8703
  function geometryOf(THREE, particle) {
8644
8704
  switch (particle.shape) {
8645
8705
  case "cone":
8646
- 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);
8647
8707
  case "cone_open":
8648
- 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);
8649
8709
  case "sphere":
8650
- 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);
8651
8711
  case "icosahedron":
8652
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8712
+ return new THREE.IcosahedronGeometry(1, particle.segments);
8653
8713
  }
8654
8714
  }
8715
+ function isCone(particle) {
8716
+ return particle.shape === "cone" || particle.shape === "cone_open";
8717
+ }
8655
8718
  function materialOf(THREE, particle) {
8656
8719
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
8657
8720
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -8662,51 +8725,82 @@ void main(){
8662
8725
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
8663
8726
  );
8664
8727
  }
8728
+ function geometryKeyOf(particle) {
8729
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
8730
+ }
8665
8731
  function createMeshLayer(THREE, pool) {
8666
8732
  const group = new THREE.Group();
8667
8733
  group.name = "workshop-meshes";
8668
8734
  const drawn = /* @__PURE__ */ new Map();
8735
+ const geometries = /* @__PURE__ */ new Map();
8736
+ const parked = /* @__PURE__ */ new Map();
8669
8737
  const frame = new THREE.Quaternion();
8670
8738
  const euler = new THREE.Euler();
8671
8739
  const turn = new THREE.Quaternion();
8672
8740
  const offset = new THREE.Vector3();
8673
- function release(mesh2) {
8674
- mesh2.removeFromParent();
8675
- mesh2.geometry.dispose();
8676
- 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();
8677
8769
  }
8678
8770
  return {
8679
8771
  group,
8680
8772
  sync() {
8681
- const live = /* @__PURE__ */ new Set();
8773
+ stamp++;
8682
8774
  for (const particle of pool.particles) {
8683
- live.add(particle.id);
8684
- let mesh2 = drawn.get(particle.id);
8685
- if (!mesh2) {
8686
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
8687
- mesh2.frustumCulled = false;
8688
- mesh2.renderOrder = particle.additive ? 5 : 3;
8689
- drawn.set(particle.id, mesh2);
8690
- group.add(mesh2);
8775
+ let entry = drawn.get(particle.id);
8776
+ if (!entry) {
8777
+ entry = acquire(particle);
8778
+ drawn.set(particle.id, entry);
8691
8779
  }
8780
+ entry.stamp = stamp;
8781
+ const mesh2 = entry.mesh;
8692
8782
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
8693
8783
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
8694
8784
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
8695
8785
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
8696
8786
  mesh2.quaternion.copy(frame).multiply(turn);
8697
- 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);
8698
8788
  mesh2.material.opacity = particle.opacity;
8699
8789
  }
8700
- for (const [id, mesh2] of drawn) {
8701
- if (live.has(id)) continue;
8702
- release(mesh2);
8790
+ for (const [id, entry] of drawn) {
8791
+ if (entry.stamp === stamp) continue;
8792
+ park(entry);
8703
8793
  drawn.delete(id);
8704
8794
  }
8705
8795
  return pool.count;
8706
8796
  },
8707
8797
  dispose() {
8708
- for (const mesh2 of drawn.values()) release(mesh2);
8798
+ for (const entry of drawn.values()) park(entry);
8709
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();
8710
8804
  group.removeFromParent();
8711
8805
  }
8712
8806
  };
@@ -9291,7 +9385,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9291
9385
  const shocks = createShockLayer(THREE, pools.shocks);
9292
9386
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
9293
9387
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
9294
- 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 });
9295
9389
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
9296
9390
  const stopMarks = world.onMarks((event) => marks.handle(event));
9297
9391
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -9317,6 +9411,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9317
9411
  const position = options.camera.position;
9318
9412
  world.setCamera([position.x, position.y, position.z]);
9319
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
+ }
9320
9424
  function syncAll() {
9321
9425
  lastDrawn.puffs = puffs.sync();
9322
9426
  lastDrawn.sparks = sparks.sync();
@@ -9325,15 +9429,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9325
9429
  lastDrawn.shocks = shocks.sync();
9326
9430
  lastDrawn.sprites = sprites.sync();
9327
9431
  lights.sync();
9328
- const engine = world.peekWorkshop();
9329
- if (engine && engine !== workshopEngine) {
9330
- workshopLayers?.dispose();
9331
- if (workshopLayers) root.remove(workshopLayers.group);
9332
- workshopEngine = engine;
9333
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
9334
- root.add(workshopLayers.group);
9335
- applyLayers();
9336
- }
9432
+ attachWorkshopLayers();
9337
9433
  const workshopCalls = workshopLayers?.sync() ?? 0;
9338
9434
  const glow = pools.lights.glow;
9339
9435
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -9386,6 +9482,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9386
9482
  },
9387
9483
  warmUpProbes() {
9388
9484
  releaseAnchors();
9485
+ marks.prepare();
9486
+ world.workshop();
9487
+ attachWorkshopLayers();
9389
9488
  const meshes = createMeshLayerProbes(THREE);
9390
9489
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
9391
9490
  const group = new THREE.Group();