@adatechnology/game-fluids-three 0.3.0 → 0.4.0

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
@@ -1,4 +1,5 @@
1
1
  import * as THREE_NS from 'three';
2
+ import { WarmUpProbes } from '@adatechnology/game-contracts';
2
3
  import { VfxWorld } from '@adatechnology/game-vfx';
3
4
  import { ThreeLib as ThreeLib$1 } from '@adatechnology/game-vfx-three';
4
5
  import { Vec3, FluidWorld, Film, StainTint, FilmLayout, FilmProfile, FilmRandom, LiquidId, SurfaceSpec, FilmLook, FilmRegion } from '@adatechnology/game-fluids';
@@ -56,6 +57,8 @@ interface FluidRenderer {
56
57
  /** Draw the liquid of a world container inside `object` (a volume clipped by the liquid surface, plus steam). */
57
58
  bindContainer(containerId: string, object: THREE_NS.Object3D, options?: BindOptions): void;
58
59
  unbindContainer(containerId: string): void;
60
+ /** Meshes that use the materials of the liquid bodies, streams and stains, to render once so that the first leak does not build their shader programs in the middle of a frame. */
61
+ warmUpProbes(): WarmUpProbes;
59
62
  /** Advance the visuals (call it after `fluids.update(dt)`). */
60
63
  update(dt: number): void;
61
64
  /** Remove every stain, stream and particle (the bindings stay). */
@@ -3088,6 +3088,17 @@ var GameFluidsThree = (() => {
3088
3088
  ])
3089
3089
  ]
3090
3090
  };
3091
+ var FUEL_LEAK_ONLY = {
3092
+ id: "fuel_leak_only",
3093
+ pageId: "vazamento",
3094
+ team: "vehicles",
3095
+ budget: { particles: 200, lights: 0 },
3096
+ loop: 11,
3097
+ view: FUEL_LEAK.view,
3098
+ setup: FUEL_LEAK.setup,
3099
+ timeline: [decal(POOL[0], 0, POOL[2], 2.2, 2040368, { grow: 3, life: 60, ar: 0.6, fluid: "oil" })],
3100
+ tick: [emit("drip", "normal", 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 5921402, a: 0.9, g: 9.8, fi: 0, gnd: 1 })]
3101
+ };
3091
3102
  var CAR_EXPLOSION = {
3092
3103
  id: "car_explosion",
3093
3104
  pageId: "explosao",
@@ -3441,6 +3452,8 @@ var GameFluidsThree = (() => {
3441
3452
  ];
3442
3453
  var PI = Math.PI;
3443
3454
  var SMOKE = { color: 9079430, width: 10, height: 8, gravity: -0.08, drag: 2.2, growth: 2.1 };
3455
+ var DEFAULT_SMOKE = { puffs: 2, radius: 0.01, opacity: 0.14, life: 0.7, drift: 0.2, rise: 0.06, growth: 2.1, flash: 1 };
3456
+ var DEFAULT_SPREAD = { drift: 0.1, rise: 0.04, side: 0.06 };
3444
3457
  var FLASH_SECONDS = 0.055;
3445
3458
  var FLASH_RAMP = [0.6, 0.9];
3446
3459
  var FLASH_OPACITY = 0.95;
@@ -3448,8 +3461,10 @@ var GameFluidsThree = (() => {
3448
3461
  var FLAME_ADDITIVE = 3;
3449
3462
  var ORIGIN2 = [0, 0, 0];
3450
3463
  var SIZE = variable("size");
3464
+ var FLASH_SCALE = variable("flash");
3465
+ var fallback = (name, defaultValue) => ite(ne(variable(name), void 0), variable(name), defaultValue);
3451
3466
  var wide = (width) => mul(sub(rand(), 0.5), width);
3452
- var puff2 = (point, velocity, world, radius, life, opacity) => mesh({
3467
+ var puff2 = (point, velocity, world, radius, life, opacity, growth = SMOKE.growth) => mesh({
3453
3468
  shape: "sphere",
3454
3469
  p: point,
3455
3470
  v: velocity,
@@ -3464,7 +3479,7 @@ var GameFluidsThree = (() => {
3464
3479
  lit: true,
3465
3480
  g: SMOKE.gravity,
3466
3481
  drag: SMOKE.drag,
3467
- grow: SMOKE.growth
3482
+ grow: growth
3468
3483
  });
3469
3484
  var flameBit = (point, velocity, world, radius, life) => [
3470
3485
  set("bitPoint", point),
@@ -3497,37 +3512,55 @@ var GameFluidsThree = (() => {
3497
3512
  id: "viewmodel_muzzle_flash",
3498
3513
  pageId: "event:viewmodel_muzzle_flash",
3499
3514
  team: "weapons",
3500
- budget: { particles: 12, lights: 1 },
3515
+ budget: { particles: 14, lights: 1 },
3501
3516
  loop: 0.1,
3517
+ setup: [
3518
+ set("size", fallback("size", 1)),
3519
+ set("puffs", fallback("puffs", DEFAULT_SMOKE.puffs)),
3520
+ set("radius", fallback("radius", DEFAULT_SMOKE.radius)),
3521
+ set("opacity", fallback("opacity", DEFAULT_SMOKE.opacity)),
3522
+ set("life", fallback("life", DEFAULT_SMOKE.life)),
3523
+ set("drift", fallback("drift", DEFAULT_SMOKE.drift)),
3524
+ set("rise", fallback("rise", DEFAULT_SMOKE.rise)),
3525
+ set("growth", fallback("growth", DEFAULT_SMOKE.growth)),
3526
+ set("flash", fallback("flash", DEFAULT_SMOKE.flash)),
3527
+ set("driftSpread", fallback("driftSpread", DEFAULT_SPREAD.drift)),
3528
+ set("riseSpread", fallback("riseSpread", DEFAULT_SPREAD.rise)),
3529
+ set("sideSpread", fallback("sideSpread", DEFAULT_SPREAD.side)),
3530
+ set("lifeSpread", ite(ne(variable("lifeSpread"), void 0), variable("lifeSpread"), mul(variable("life"), 0.3)))
3531
+ ],
3502
3532
  timeline: [
3503
3533
  set("angles", []),
3504
3534
  repeat(4, append("angles", add(div(mul(INDEX, PI), 2), mul(rand(), 0.6)))),
3505
3535
  set("roll", mul(rand(), PI)),
3506
- flash(ORIGIN2, 16756832, mul(4, SIZE), FLASH_SECONDS, mul(2, SIZE), true, true),
3507
- flashPiece("cone_open", mul(0.013, SIZE), mul(0.085, SIZE), 10, [mul(0.042, SIZE), 0, 0], [0, 0, -PI / 2], 16757575),
3508
- repeat(
3509
- 4,
3510
- set("angle", pick(variable("angles"), INDEX)),
3511
- flashPiece(
3512
- "cone",
3513
- mul(5e-3, SIZE),
3514
- mul(0.04, SIZE),
3515
- 6,
3516
- [mul(0.012, SIZE), mul(cos(variable("angle")), 0.016, SIZE), mul(sin(variable("angle")), 0.016, SIZE)],
3517
- [variable("angle"), 0, -PI / 2],
3518
- 16757575
3519
- )
3520
- ),
3521
- flashPiece("sphere", mul(0.016, SIZE), void 0, 12, [mul(8e-3, SIZE), 0, 0], [0, 0, 0], 16773568),
3536
+ when(gt(FLASH_SCALE, 0), [
3537
+ flash(ORIGIN2, 16756832, mul(4, SIZE, FLASH_SCALE), FLASH_SECONDS, mul(2, SIZE, FLASH_SCALE), true, true),
3538
+ flashPiece("cone_open", mul(0.013, SIZE, FLASH_SCALE), mul(0.085, SIZE, FLASH_SCALE), 10, [mul(0.042, SIZE, FLASH_SCALE), 0, 0], [0, 0, -PI / 2], 16757575),
3539
+ repeat(
3540
+ 4,
3541
+ set("angle", pick(variable("angles"), INDEX)),
3542
+ flashPiece(
3543
+ "cone",
3544
+ mul(5e-3, SIZE, FLASH_SCALE),
3545
+ mul(0.04, SIZE, FLASH_SCALE),
3546
+ 6,
3547
+ [mul(0.012, SIZE, FLASH_SCALE), mul(cos(variable("angle")), 0.016, SIZE, FLASH_SCALE), mul(sin(variable("angle")), 0.016, SIZE, FLASH_SCALE)],
3548
+ [variable("angle"), 0, -PI / 2],
3549
+ 16757575
3550
+ )
3551
+ ),
3552
+ flashPiece("sphere", mul(0.016, SIZE, FLASH_SCALE), void 0, 12, [mul(8e-3, SIZE, FLASH_SCALE), 0, 0], [0, 0, 0], 16773568)
3553
+ ]),
3522
3554
  repeat(
3523
- 6,
3555
+ variable("puffs"),
3524
3556
  puff2(
3525
3557
  ORIGIN2,
3526
- [add(0.15, mul(rand(), 0.35)), add(0.03, mul(rand(), 0.12)), mul(sub(rand(), 0.5), 0.14)],
3558
+ [add(variable("drift"), mul(rand(), variable("driftSpread"))), add(variable("rise"), mul(rand(), variable("riseSpread"))), mul(sub(rand(), 0.5), variable("sideSpread"))],
3527
3559
  [0, 0, 0],
3528
- mul(0.013, SIZE),
3529
- add(1.3, mul(rand(), 0.6)),
3530
- 0.45
3560
+ mul(variable("radius"), SIZE),
3561
+ add(variable("life"), mul(rand(), variable("lifeSpread"))),
3562
+ variable("opacity"),
3563
+ variable("growth")
3531
3564
  )
3532
3565
  )
3533
3566
  ]
@@ -3575,9 +3608,20 @@ var GameFluidsThree = (() => {
3575
3608
  ]
3576
3609
  };
3577
3610
  var VIEWMODEL_EFFECTS = [MUZZLE_FLASH2, SMOKE_PUFF, BACKBLAST, FLAME_JET];
3611
+ var aliasOf = (base, id, changes) => ({ ...base, id, pageId: `vehicle:${id}`, anchor: CAR_ORIGIN2, ...changes });
3612
+ var VEHICLE_EFFECTS = [
3613
+ /** The engine on fire: `engine_smoke` at its worst condition (black smoke, flames in the hood, a light). */
3614
+ aliasOf(ENGINE_SMOKE, "vehicle_fire", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 3 }, loop: 12 }),
3615
+ /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */
3616
+ aliasOf(ENGINE_SMOKE, "vehicle_exhaust", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 0 }, loop: 0.5 }),
3617
+ /** A hole in the tank: the page's petrol leak without the fire and the blast (the pool and the drips). A normal bullet does only this. */
3618
+ aliasOf(FUEL_LEAK_ONLY, "vehicle_leak", {}),
3619
+ /** The leak caught fire (an incendiary or tracer round, a spark, a fire near): the page's whole sequence, the fire running along the pool to the tank and the blast. */
3620
+ aliasOf(FUEL_LEAK, "vehicle_leak_ignited", {})
3621
+ ];
3578
3622
  var ALL_EFFECTS = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH];
3579
3623
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
3580
- var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS].map((effect) => [effect.id, effect])) };
3624
+ var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
3581
3625
  var LEHMER_MULTIPLIER = 16807;
3582
3626
  var LEHMER_MODULUS = 2147483647;
3583
3627
  var HASH_MIX = 2654435761;
@@ -7730,6 +7774,79 @@ void main(){
7730
7774
  }
7731
7775
  };
7732
7776
  }
7777
+ var SPHERE_DEFAULT_WIDTH = 8;
7778
+ var SPHERE_DEFAULT_HEIGHT = 6;
7779
+ function geometryOf(THREE, particle) {
7780
+ switch (particle.shape) {
7781
+ case "cone":
7782
+ return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7783
+ case "cone_open":
7784
+ return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7785
+ case "sphere":
7786
+ return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7787
+ case "icosahedron":
7788
+ return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7789
+ }
7790
+ }
7791
+ function materialOf(THREE, particle) {
7792
+ if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7793
+ return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
7794
+ }
7795
+ function createMeshLayerProbes(THREE) {
7796
+ const geometry = new THREE.BoxGeometry(1e-3, 1e-3, 1e-3);
7797
+ return [{ lit: true, additive: false }, { lit: false, additive: false }, { lit: false, additive: true }].map(
7798
+ (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
7799
+ );
7800
+ }
7801
+ function createMeshLayer(THREE, pool) {
7802
+ const group = new THREE.Group();
7803
+ group.name = "workshop-meshes";
7804
+ const drawn = /* @__PURE__ */ new Map();
7805
+ const frame = new THREE.Quaternion();
7806
+ const euler = new THREE.Euler();
7807
+ const turn = new THREE.Quaternion();
7808
+ const offset = new THREE.Vector3();
7809
+ function release(mesh2) {
7810
+ mesh2.removeFromParent();
7811
+ mesh2.geometry.dispose();
7812
+ mesh2.material.dispose();
7813
+ }
7814
+ return {
7815
+ group,
7816
+ sync() {
7817
+ const live = /* @__PURE__ */ new Set();
7818
+ for (const particle of pool.particles) {
7819
+ live.add(particle.id);
7820
+ let mesh2 = drawn.get(particle.id);
7821
+ if (!mesh2) {
7822
+ mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7823
+ mesh2.frustumCulled = false;
7824
+ mesh2.renderOrder = particle.additive ? 5 : 3;
7825
+ drawn.set(particle.id, mesh2);
7826
+ group.add(mesh2);
7827
+ }
7828
+ frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7829
+ offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7830
+ mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7831
+ turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7832
+ mesh2.quaternion.copy(frame).multiply(turn);
7833
+ mesh2.scale.setScalar(particle.scale);
7834
+ mesh2.material.opacity = particle.opacity;
7835
+ }
7836
+ for (const [id, mesh2] of drawn) {
7837
+ if (live.has(id)) continue;
7838
+ release(mesh2);
7839
+ drawn.delete(id);
7840
+ }
7841
+ return pool.count;
7842
+ },
7843
+ dispose() {
7844
+ for (const mesh2 of drawn.values()) release(mesh2);
7845
+ drawn.clear();
7846
+ group.removeFromParent();
7847
+ }
7848
+ };
7849
+ }
7733
7850
  var clamp5 = (value, min2, max2) => value < min2 ? min2 : value > max2 ? max2 : value;
7734
7851
  function layer(container, name, blend) {
7735
7852
  const element = container.ownerDocument.createElement("div");
@@ -7899,73 +8016,6 @@ void main() {
7899
8016
  }
7900
8017
  };
7901
8018
  }
7902
- var SPHERE_DEFAULT_WIDTH = 8;
7903
- var SPHERE_DEFAULT_HEIGHT = 6;
7904
- function geometryOf(THREE, particle) {
7905
- switch (particle.shape) {
7906
- case "cone":
7907
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7908
- case "cone_open":
7909
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7910
- case "sphere":
7911
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7912
- case "icosahedron":
7913
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7914
- }
7915
- }
7916
- function materialOf(THREE, particle) {
7917
- if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7918
- return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
7919
- }
7920
- function createMeshLayer(THREE, pool) {
7921
- const group = new THREE.Group();
7922
- group.name = "workshop-meshes";
7923
- const drawn = /* @__PURE__ */ new Map();
7924
- const frame = new THREE.Quaternion();
7925
- const euler = new THREE.Euler();
7926
- const turn = new THREE.Quaternion();
7927
- const offset = new THREE.Vector3();
7928
- function release(mesh2) {
7929
- mesh2.removeFromParent();
7930
- mesh2.geometry.dispose();
7931
- mesh2.material.dispose();
7932
- }
7933
- return {
7934
- group,
7935
- sync() {
7936
- const live = /* @__PURE__ */ new Set();
7937
- for (const particle of pool.particles) {
7938
- live.add(particle.id);
7939
- let mesh2 = drawn.get(particle.id);
7940
- if (!mesh2) {
7941
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7942
- mesh2.frustumCulled = false;
7943
- mesh2.renderOrder = particle.additive ? 5 : 3;
7944
- drawn.set(particle.id, mesh2);
7945
- group.add(mesh2);
7946
- }
7947
- frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7948
- offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7949
- mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7950
- turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7951
- mesh2.quaternion.copy(frame).multiply(turn);
7952
- mesh2.scale.setScalar(particle.scale);
7953
- mesh2.material.opacity = particle.opacity;
7954
- }
7955
- for (const [id, mesh2] of drawn) {
7956
- if (live.has(id)) continue;
7957
- release(mesh2);
7958
- drawn.delete(id);
7959
- }
7960
- return pool.count;
7961
- },
7962
- dispose() {
7963
- for (const mesh2 of drawn.values()) release(mesh2);
7964
- drawn.clear();
7965
- group.removeFromParent();
7966
- }
7967
- };
7968
- }
7969
8019
  var WORKSHOP_SPRITE_VERTEX = `attribute float size;attribute float alpha;attribute vec3 pcol;varying vec3 vC;varying float vA;uniform float uScale;
7970
8020
  void main(){vC=pcol;vA=alpha;vec4 mv=modelViewMatrix*vec4(position,1.);gl_PointSize=clamp(size*uScale/-mv.z,1.,512.);gl_Position=projectionMatrix*mv;}`;
7971
8021
  var WORKSHOP_SPRITE_FRAGMENT = `uniform float uSoft;varying vec3 vC;varying float vA;
@@ -8330,6 +8380,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8330
8380
  const requested = options.vfx;
8331
8381
  const world = isWorld(requested) ? requested : createVfx({ profile: options.quality ?? "medium", reducedMotion, ...requested });
8332
8382
  const pools = world.pools();
8383
+ let anchors;
8384
+ const releaseAnchors = () => {
8385
+ if (!anchors) return;
8386
+ anchors.meshes[0]?.geometry.dispose();
8387
+ for (const mesh2 of anchors.meshes) mesh2.material.dispose();
8388
+ anchors = void 0;
8389
+ };
8333
8390
  const root = new THREE.Group();
8334
8391
  options.scene.add(root);
8335
8392
  const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
@@ -8446,6 +8503,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8446
8503
  syncAll();
8447
8504
  overlay?.update(screenNow());
8448
8505
  },
8506
+ warmUpProbes() {
8507
+ releaseAnchors();
8508
+ const meshes = createMeshLayerProbes(THREE);
8509
+ for (const mesh2 of meshes) mesh2.frustumCulled = false;
8510
+ const group = new THREE.Group();
8511
+ group.add(...meshes);
8512
+ anchors = { meshes, group };
8513
+ return { group, dispose: () => {
8514
+ } };
8515
+ },
8449
8516
  applyCamera() {
8450
8517
  const shake = screenNow().shake;
8451
8518
  if (reducedMotion || !(shake > SHAKE_FLOOR)) return () => {
@@ -8464,6 +8531,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8464
8531
  },
8465
8532
  screen: screenNow,
8466
8533
  dispose() {
8534
+ releaseAnchors();
8467
8535
  stopMarks();
8468
8536
  overlay?.dispose();
8469
8537
  workshopLayers?.dispose();
@@ -8676,6 +8744,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8676
8744
  this.vertices = 0;
8677
8745
  return;
8678
8746
  }
8747
+ if (state.closed && this.isShellOpaque()) {
8748
+ if (this.mesh) this.mesh.visible = false;
8749
+ this.vertices = 0;
8750
+ return;
8751
+ }
8679
8752
  this.build(state, quality);
8680
8753
  if (!this.mesh || !this.solid) return;
8681
8754
  this.mesh.material = materials.liquidBody(def);
@@ -8701,6 +8774,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8701
8774
  }
8702
8775
  } else this.steamAcc = 0;
8703
8776
  }
8777
+ /** Every mesh of the bound object (the liquid itself aside) is drawn with an opaque material. */
8778
+ isShellOpaque() {
8779
+ let opaque = true;
8780
+ this.object.traverse((child) => {
8781
+ const mesh2 = child;
8782
+ if (!opaque || !mesh2.isMesh || child === this.mesh) return;
8783
+ const materials = Array.isArray(mesh2.material) ? mesh2.material : [mesh2.material];
8784
+ if (materials.some((material) => material.transparent || material.opacity < 1)) opaque = false;
8785
+ });
8786
+ return opaque;
8787
+ }
8704
8788
  get triangles() {
8705
8789
  return this.visible ? Math.floor(this.vertices / 3) : 0;
8706
8790
  }
@@ -8826,6 +8910,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8826
8910
  }
8827
8911
  return m;
8828
8912
  }
8913
+ /** One material per role, in a clear and in an opaque liquid, which are the shader programs a leak will need (the colour is not part of a program). */
8914
+ probes() {
8915
+ const base = { id: "warm_up", color: 8421504, viscosity: 1, density: 1e3, dryTime: 60, gloss: 0.5 };
8916
+ const defs = [{ ...base, opacity: 0.5 }, { ...base, opacity: 1 }];
8917
+ return {
8918
+ plain: defs.flatMap((def) => [this.liquidBody(def), this.liquidStream(def)]),
8919
+ instanced: defs.map((def) => this.liquidStream(def)),
8920
+ coloured: defs.flatMap((def) => [this.stainFilm(def), this.stainWet(def)])
8921
+ };
8922
+ }
8829
8923
  dispose() {
8830
8924
  for (const map of [this.body, this.stream, this.film, this.wet]) {
8831
8925
  for (const m of map.values()) m.dispose();
@@ -8901,9 +8995,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8901
8995
  var dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8902
8996
  var cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
8903
8997
  var length = (a) => Math.hypot(a[0], a[1], a[2]);
8904
- var normalize2 = (a, fallback = [0, 1, 0]) => {
8998
+ var normalize2 = (a, fallback2 = [0, 1, 0]) => {
8905
8999
  const l = length(a);
8906
- return l < 1e-9 ? [fallback[0], fallback[1], fallback[2]] : [a[0] / l, a[1] / l, a[2] / l];
9000
+ return l < 1e-9 ? [fallback2[0], fallback2[1], fallback2[2]] : [a[0] / l, a[1] / l, a[2] / l];
8907
9001
  };
8908
9002
  var ballistic = (origin, velocity, gravity, t) => [
8909
9003
  origin[0] + velocity[0] * t,
@@ -10339,6 +10433,29 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10339
10433
  body.dispose();
10340
10434
  bodies.delete(containerId);
10341
10435
  },
10436
+ warmUpProbes() {
10437
+ const plainGeometry = new THREE.BufferGeometry();
10438
+ plainGeometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(9), 3));
10439
+ plainGeometry.setAttribute("normal", new THREE.BufferAttribute(new Float32Array(9), 3));
10440
+ const stainGeometry = plainGeometry.clone();
10441
+ stainGeometry.setAttribute("color", new THREE.BufferAttribute(new Float32Array(12), 4));
10442
+ const { plain, instanced, coloured } = materials.probes();
10443
+ const meshes = [
10444
+ ...plain.map((material) => new THREE.Mesh(plainGeometry, material)),
10445
+ ...instanced.map((material) => new THREE.InstancedMesh(dropGeometry, material, 1)),
10446
+ ...coloured.map((material) => new THREE.Mesh(stainGeometry, material))
10447
+ ];
10448
+ const group = new THREE.Group();
10449
+ for (const mesh2 of meshes) mesh2.frustumCulled = false;
10450
+ group.add(...meshes);
10451
+ return {
10452
+ group,
10453
+ dispose() {
10454
+ plainGeometry.dispose();
10455
+ stainGeometry.dispose();
10456
+ }
10457
+ };
10458
+ },
10342
10459
  update(dt) {
10343
10460
  if (disposed) return;
10344
10461
  time += dt;