@adatechnology/game-fluids-three 0.3.1 → 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
  ]
@@ -3581,8 +3614,10 @@ var GameFluidsThree = (() => {
3581
3614
  aliasOf(ENGINE_SMOKE, "vehicle_fire", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 3 }, loop: 12 }),
3582
3615
  /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */
3583
3616
  aliasOf(ENGINE_SMOKE, "vehicle_exhaust", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 0 }, loop: 0.5 }),
3584
- /** A hole in the tank: the page's own petrol leak (drips, pool, sparks, fire running to the tank). */
3585
- aliasOf(FUEL_LEAK, "vehicle_leak", {})
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", {})
3586
3621
  ];
3587
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];
3588
3623
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
@@ -7739,6 +7774,79 @@ void main(){
7739
7774
  }
7740
7775
  };
7741
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
+ }
7742
7850
  var clamp5 = (value, min2, max2) => value < min2 ? min2 : value > max2 ? max2 : value;
7743
7851
  function layer(container, name, blend) {
7744
7852
  const element = container.ownerDocument.createElement("div");
@@ -7908,73 +8016,6 @@ void main() {
7908
8016
  }
7909
8017
  };
7910
8018
  }
7911
- var SPHERE_DEFAULT_WIDTH = 8;
7912
- var SPHERE_DEFAULT_HEIGHT = 6;
7913
- function geometryOf(THREE, particle) {
7914
- switch (particle.shape) {
7915
- case "cone":
7916
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7917
- case "cone_open":
7918
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7919
- case "sphere":
7920
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7921
- case "icosahedron":
7922
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7923
- }
7924
- }
7925
- function materialOf(THREE, particle) {
7926
- if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7927
- return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
7928
- }
7929
- function createMeshLayer(THREE, pool) {
7930
- const group = new THREE.Group();
7931
- group.name = "workshop-meshes";
7932
- const drawn = /* @__PURE__ */ new Map();
7933
- const frame = new THREE.Quaternion();
7934
- const euler = new THREE.Euler();
7935
- const turn = new THREE.Quaternion();
7936
- const offset = new THREE.Vector3();
7937
- function release(mesh2) {
7938
- mesh2.removeFromParent();
7939
- mesh2.geometry.dispose();
7940
- mesh2.material.dispose();
7941
- }
7942
- return {
7943
- group,
7944
- sync() {
7945
- const live = /* @__PURE__ */ new Set();
7946
- for (const particle of pool.particles) {
7947
- live.add(particle.id);
7948
- let mesh2 = drawn.get(particle.id);
7949
- if (!mesh2) {
7950
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7951
- mesh2.frustumCulled = false;
7952
- mesh2.renderOrder = particle.additive ? 5 : 3;
7953
- drawn.set(particle.id, mesh2);
7954
- group.add(mesh2);
7955
- }
7956
- frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7957
- offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7958
- mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7959
- turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7960
- mesh2.quaternion.copy(frame).multiply(turn);
7961
- mesh2.scale.setScalar(particle.scale);
7962
- mesh2.material.opacity = particle.opacity;
7963
- }
7964
- for (const [id, mesh2] of drawn) {
7965
- if (live.has(id)) continue;
7966
- release(mesh2);
7967
- drawn.delete(id);
7968
- }
7969
- return pool.count;
7970
- },
7971
- dispose() {
7972
- for (const mesh2 of drawn.values()) release(mesh2);
7973
- drawn.clear();
7974
- group.removeFromParent();
7975
- }
7976
- };
7977
- }
7978
8019
  var WORKSHOP_SPRITE_VERTEX = `attribute float size;attribute float alpha;attribute vec3 pcol;varying vec3 vC;varying float vA;uniform float uScale;
7979
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;}`;
7980
8021
  var WORKSHOP_SPRITE_FRAGMENT = `uniform float uSoft;varying vec3 vC;varying float vA;
@@ -8339,6 +8380,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8339
8380
  const requested = options.vfx;
8340
8381
  const world = isWorld(requested) ? requested : createVfx({ profile: options.quality ?? "medium", reducedMotion, ...requested });
8341
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
+ };
8342
8390
  const root = new THREE.Group();
8343
8391
  options.scene.add(root);
8344
8392
  const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
@@ -8455,6 +8503,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8455
8503
  syncAll();
8456
8504
  overlay?.update(screenNow());
8457
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
+ },
8458
8516
  applyCamera() {
8459
8517
  const shake = screenNow().shake;
8460
8518
  if (reducedMotion || !(shake > SHAKE_FLOOR)) return () => {
@@ -8473,6 +8531,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8473
8531
  },
8474
8532
  screen: screenNow,
8475
8533
  dispose() {
8534
+ releaseAnchors();
8476
8535
  stopMarks();
8477
8536
  overlay?.dispose();
8478
8537
  workshopLayers?.dispose();
@@ -8685,6 +8744,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8685
8744
  this.vertices = 0;
8686
8745
  return;
8687
8746
  }
8747
+ if (state.closed && this.isShellOpaque()) {
8748
+ if (this.mesh) this.mesh.visible = false;
8749
+ this.vertices = 0;
8750
+ return;
8751
+ }
8688
8752
  this.build(state, quality);
8689
8753
  if (!this.mesh || !this.solid) return;
8690
8754
  this.mesh.material = materials.liquidBody(def);
@@ -8710,6 +8774,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8710
8774
  }
8711
8775
  } else this.steamAcc = 0;
8712
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
+ }
8713
8788
  get triangles() {
8714
8789
  return this.visible ? Math.floor(this.vertices / 3) : 0;
8715
8790
  }
@@ -8835,6 +8910,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8835
8910
  }
8836
8911
  return m;
8837
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
+ }
8838
8923
  dispose() {
8839
8924
  for (const map of [this.body, this.stream, this.film, this.wet]) {
8840
8925
  for (const m of map.values()) m.dispose();
@@ -8910,9 +8995,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8910
8995
  var dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8911
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]];
8912
8997
  var length = (a) => Math.hypot(a[0], a[1], a[2]);
8913
- var normalize2 = (a, fallback = [0, 1, 0]) => {
8998
+ var normalize2 = (a, fallback2 = [0, 1, 0]) => {
8914
8999
  const l = length(a);
8915
- 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];
8916
9001
  };
8917
9002
  var ballistic = (origin, velocity, gravity, t) => [
8918
9003
  origin[0] + velocity[0] * t,
@@ -10348,6 +10433,29 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10348
10433
  body.dispose();
10349
10434
  bodies.delete(containerId);
10350
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
+ },
10351
10459
  update(dt) {
10352
10460
  if (disposed) return;
10353
10461
  time += dt;