@woosh/meep-engine 3.42.0 → 3.43.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.
Files changed (62) hide show
  1. package/package.json +91 -96
  2. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  3. package/src/shade/playground/vgeo_runtime/README.md +5 -4
  4. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  5. package/src/shade/renderer/Renderer.js +6 -5
  6. package/src/shade/renderer/atmosphere/GPUSky.d.ts +12 -0
  7. package/src/shade/renderer/atmosphere/GPUSky.d.ts.map +1 -1
  8. package/src/shade/renderer/atmosphere/GPUSky.js +162 -147
  9. package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.d.ts.map +1 -1
  10. package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.js +10 -3
  11. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  12. package/src/shade/renderer/particles/DESIGN.md +13 -9
  13. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +1 -1
  14. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +1 -1
  15. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  16. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +3 -2
  17. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  18. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +3 -2
  19. package/src/shade/renderer/scene/GPUSceneContext.d.ts +9 -1
  20. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  21. package/src/shade/renderer/scene/GPUSceneContext.js +23 -1
  22. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +4 -1
  23. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  24. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +74 -3
  25. package/src/shade/renderer/volumetrics/NOTES.md +1023 -957
  26. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +5 -4
  27. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -1
  28. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +5 -4
  29. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +1 -1
  30. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +1 -1
  31. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +1 -1
  32. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +1 -1
  33. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts +41 -0
  34. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts.map +1 -0
  35. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.js +112 -0
  36. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +9 -23
  37. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  38. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +18 -126
  39. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts +22 -0
  40. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts.map +1 -0
  41. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.js +228 -0
  42. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  43. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +0 -53
  44. package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts +4 -3
  45. package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts.map +1 -1
  46. package/src/shade/renderer/water/surface/chunk_water_surface_sample.js +17 -7
  47. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts.map +1 -1
  48. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.js +69 -23
  49. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts.map +1 -1
  50. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.js +13 -5
  51. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts +29 -8
  52. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts.map +1 -1
  53. package/src/shade/renderer/water/surface/particles/chunk_water_particle.js +155 -30
  54. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts +6 -1
  55. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts.map +1 -1
  56. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.js +157 -39
  57. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts +91 -21
  58. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts.map +1 -1
  59. package/src/shade/renderer/water/surface/particles/shader_water_particles.js +442 -70
  60. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +11 -3
  61. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  62. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +3 -0
@@ -10,7 +10,7 @@ import { WebGPUStruct } from "../../../shader/type/WebGPUStruct.js";
10
10
  * without integration error and every particle of one front shares the origin and the age.
11
11
  */
12
12
  export const WATER_PARTICLE_STRUCT = WebGPUStruct.from({
13
- /** Centre of the front, or its mirror image after a reflection; metric local xz. */
13
+ /** Centre of the front, or the mirror image of it for a reflection; metric local xz. */
14
14
  origin: vec2f,
15
15
  /** Propagation direction, radians from local +x towards +z. */
16
16
  angle: f32,
@@ -24,6 +24,11 @@ export const WATER_PARTICLE_STRUCT = WebGPUStruct.from({
24
24
  dispersion: f32,
25
25
  /** The water_waves row of the body it lives on. */
26
26
  body: u32,
27
+ /**
28
+ * 1 once the first of its support has left its body's outline, or that of the particle it split from: its mirror
29
+ * image was emitted then, and it dies once the last of it has left too. 0 from emission.
30
+ */
31
+ reflected: u32,
27
32
  }, 'WaterParticle');
28
33
 
29
34
  /**
@@ -37,13 +42,16 @@ export const WATER_PARTICLE_ENTRY_STRUCT = WebGPUStruct.from({
37
42
  angle: f32,
38
43
  amplitude: f32,
39
44
  radius: f32,
40
- /** Tangential window: the distance to the front's neighbouring particle. */
41
- width: f32,
45
+ /** Half-angle of the window around the front's centre: the angle to the front's neighbouring particle. */
46
+ dispersion: f32,
42
47
  /** Carrier phase lag of the crests behind the envelope, radians. */
43
48
  phase: f32,
44
- /** water_waves row << 4 | level */
49
+ /** water_waves row and level, as water_particle_key packs them */
45
50
  key: u32,
46
51
  /** Index of the particle, which orders a bucket. */
47
52
  source: u32,
48
- reserved: u32,
53
+ /** How far the particle stands from its front's centre, metres. */
54
+ distance: f32,
55
+ /** Half the length of its envelope across its front, metres. */
56
+ half_length: f32,
49
57
  }, 'WaterParticleEntry');
@@ -1,19 +1,40 @@
1
- /** Levels of the particle hash: cell sizes 4 mm · 2^level, so supports up to 64 m. */
2
- export const WATER_PARTICLE_LEVELS: 16;
1
+ /**
2
+ * Levels of the particle hash: cell sizes 4 mm · 2^level, so supports up to 256 m, which a 2 m splash's stretched
3
+ * packets reach before they die.
4
+ */
5
+ export const WATER_PARTICLE_LEVELS: 18;
3
6
  /** Cell size of level 0, metres. A particle sits at the smallest level whose cell is at least its support's diameter. */
4
7
  export const WATER_PARTICLE_LEVEL0_CELL: number;
8
+ /**
9
+ * How fast a packet's envelope stretches as it disperses, as a fraction κ of its group speed, by its wavenumber k against
10
+ * k_m = √(g/(σ/ρ)), the wavenumber of the slowest crests (λ = 1.71 cm): from x = log₂(k/k_m) = -7 in steps of a quarter,
11
+ * held at both ends.
12
+ *
13
+ * Its half-length h, 2r when emitted, grows as h² = (2r)² + (κD/2)² over the distance D its front has travelled since:
14
+ * the spread of an unchirped packet's energy grows as the spread of its group speeds times time, in quadrature with
15
+ * where it began (Jeschke & Wojtan 2017 grow it linearly, eq. 14, its far limit). Each κ is the one whose peak
16
+ * √(2r/h) best matches exact linear theory, ω² = gk + (σ/ρ)k³, for the emitted packet at 5 to 225 wavelengths (FFT,
17
+ * worst ratio minimised): within 2% for capillary waves up to r = 8.5 mm and gravity waves from r = 6 cm, where κ
18
+ * tends to 1 and 0.9, and 5% from 4 cm. Between, around the slowest group speed (λ = 4.4 cm), a broad band's energy
19
+ * gathers at that speed and the peak falls as t^(−1/3), which no κ draws: up to 16% below r = 12 mm, 27% at 3 cm.
20
+ */
21
+ export const WATER_PARTICLE_STRETCH: number[];
5
22
  /**
6
23
  * The physics of one wave particle as pure functions (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §5).
7
24
  *
8
25
  * A particle of radius r carries a packet of wavelength 2r, k = π/r. Under the gravity–capillary
9
26
  * dispersion relation the packet's envelope travels at the group speed and its crests at the phase speed
10
- * (Jeschke & Wojtan 2017; Xue et al. 2025 give wave particles their own wavelength's dispersion). It loses
11
- * amplitude at the clean-surface viscous rate 2νk² plus the surface-film rate k√(νω)/(2√2) of natural
12
- * water — so a raindrop's ripple is gone in a fraction of a second while a big splash crosses a bay.
27
+ * (Jeschke & Wojtan 2017; Xue et al. 2025 give wave particles their own wavelength's dispersion). The packet holds a
28
+ * band of wavelengths around 2r, which the same relation moves apart: its envelope, two radii either side when emitted,
29
+ * stretches as it travels and its height falls to keep its energy (WATER_PARTICLE_STRETCH), as a splash's single ring
30
+ * becomes a widening train of ripples. It loses amplitude at the clean-surface viscous rate 2νk² plus the surface-film
31
+ * rate k√(νω)/(2√2) of natural water — so a raindrop's ripple is gone in a fraction of a second while a big splash
32
+ * crosses a bay.
13
33
  *
14
- * The particles of a front are W apart, W the front's arc between them; a cosine window of half-width W
15
- * along the front is a partition of unity over them, so the front's height is the packet's amplitude
16
- * whatever the spacing, and one front adds at most two particles at any point.
34
+ * The particles of a front are β apart in angle around its centre, and a packet's crests are arcs around that
35
+ * centre; a cosine window of half-angle β is a partition of unity over them at every radius, so the front's
36
+ * height is the packet's amplitude whatever the spacing, splitting a front changes none of it, and one front
37
+ * adds at most two particles at any point.
17
38
  *
18
39
  * @type {CodeChunk}
19
40
  */
@@ -1 +1 @@
1
- {"version":3,"file":"chunk_water_particle.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/water/surface/particles/chunk_water_particle.js"],"names":[],"mappings":"AAKA,sFAAsF;AACtF,uCAAwC;AAExC,yHAAyH;AACzH,gDAAkD;AAElD;;;;;;;;;;;;;;GAcG;AACH,mCAFU,SAAS,CAsH6G;0BA9ItG,uCAAuC"}
1
+ {"version":3,"file":"chunk_water_particle.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/water/surface/particles/chunk_water_particle.js"],"names":[],"mappings":"AAKA;;;GAGG;AACH,uCAAwC;AAKxC,yHAAyH;AACzH,gDAAkD;AAElD;;;;;;;;;;;;GAYG;AACH,8CAIE;AAKF;;;;;;;;;;;;;;;;;;GAkBG;AACH,mCAFU,SAAS,CAmN6G;0BA3QtG,uCAAuC"}
@@ -3,30 +3,65 @@ import { chunk_pcg3d } from "../../../shader/chunk/random/pcg/chunk_pcg3d.js";
3
3
  import { WATER_PARTICLE_ENTRY_STRUCT, WATER_PARTICLE_STRUCT } from "./WATER_PARTICLE_STRUCT.js";
4
4
  import { chunk_water_spectrum } from "../spectrum/chunk_water_spectrum.js";
5
5
 
6
- /** Levels of the particle hash: cell sizes 4 mm · 2^level, so supports up to 64 m. */
7
- export const WATER_PARTICLE_LEVELS = 16;
6
+ /**
7
+ * Levels of the particle hash: cell sizes 4 mm · 2^level, so supports up to 256 m, which a 2 m splash's stretched
8
+ * packets reach before they die.
9
+ */
10
+ export const WATER_PARTICLE_LEVELS = 18;
11
+
12
+ /** Bits of a hash key that hold the level, below the body's row. */
13
+ const WATER_PARTICLE_LEVEL_BITS = Math.ceil(Math.log2(WATER_PARTICLE_LEVELS));
8
14
 
9
15
  /** Cell size of level 0, metres. A particle sits at the smallest level whose cell is at least its support's diameter. */
10
16
  export const WATER_PARTICLE_LEVEL0_CELL = 1 / 256;
11
17
 
18
+ /**
19
+ * How fast a packet's envelope stretches as it disperses, as a fraction κ of its group speed, by its wavenumber k against
20
+ * k_m = √(g/(σ/ρ)), the wavenumber of the slowest crests (λ = 1.71 cm): from x = log₂(k/k_m) = -7 in steps of a quarter,
21
+ * held at both ends.
22
+ *
23
+ * Its half-length h, 2r when emitted, grows as h² = (2r)² + (κD/2)² over the distance D its front has travelled since:
24
+ * the spread of an unchirped packet's energy grows as the spread of its group speeds times time, in quadrature with
25
+ * where it began (Jeschke & Wojtan 2017 grow it linearly, eq. 14, its far limit). Each κ is the one whose peak
26
+ * √(2r/h) best matches exact linear theory, ω² = gk + (σ/ρ)k³, for the emitted packet at 5 to 225 wavelengths (FFT,
27
+ * worst ratio minimised): within 2% for capillary waves up to r = 8.5 mm and gravity waves from r = 6 cm, where κ
28
+ * tends to 1 and 0.9, and 5% from 4 cm. Between, around the slowest group speed (λ = 4.4 cm), a broad band's energy
29
+ * gathers at that speed and the peak falls as t^(−1/3), which no κ draws: up to 16% below r = 12 mm, 27% at 3 cm.
30
+ */
31
+ export const WATER_PARTICLE_STRETCH = [
32
+ 0.9002, 0.9002, 0.8998, 0.8993, 0.8984, 0.8971, 0.8957, 0.8935, 0.8904, 0.8859, 0.8798, 0.8706, 0.8585, 0.8411, 0.8166,
33
+ 0.7819, 0.7323, 0.6669, 0.5795, 0.461, 0.216, 0.0922, 0.064, 0.0723, 0.1089, 0.1877, 0.3427, 0.6145, 0.9295, 1.0417,
34
+ 1.0734, 1.0739, 1.0622, 1.0469, 1.0313, 1.0175, 1.0059, 0.9969, 0.9899,
35
+ ];
36
+
37
+ /** log₂(k/k_m) of the stretch table's first entry; entries are a quarter apart. */
38
+ const WATER_PARTICLE_STRETCH_FIRST = -7;
39
+
12
40
  /**
13
41
  * The physics of one wave particle as pure functions (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §5).
14
42
  *
15
43
  * A particle of radius r carries a packet of wavelength 2r, k = π/r. Under the gravity–capillary
16
44
  * dispersion relation the packet's envelope travels at the group speed and its crests at the phase speed
17
- * (Jeschke & Wojtan 2017; Xue et al. 2025 give wave particles their own wavelength's dispersion). It loses
18
- * amplitude at the clean-surface viscous rate 2νk² plus the surface-film rate k√(νω)/(2√2) of natural
19
- * water — so a raindrop's ripple is gone in a fraction of a second while a big splash crosses a bay.
45
+ * (Jeschke & Wojtan 2017; Xue et al. 2025 give wave particles their own wavelength's dispersion). The packet holds a
46
+ * band of wavelengths around 2r, which the same relation moves apart: its envelope, two radii either side when emitted,
47
+ * stretches as it travels and its height falls to keep its energy (WATER_PARTICLE_STRETCH), as a splash's single ring
48
+ * becomes a widening train of ripples. It loses amplitude at the clean-surface viscous rate 2νk² plus the surface-film
49
+ * rate k√(νω)/(2√2) of natural water — so a raindrop's ripple is gone in a fraction of a second while a big splash
50
+ * crosses a bay.
20
51
  *
21
- * The particles of a front are W apart, W the front's arc between them; a cosine window of half-width W
22
- * along the front is a partition of unity over them, so the front's height is the packet's amplitude
23
- * whatever the spacing, and one front adds at most two particles at any point.
52
+ * The particles of a front are β apart in angle around its centre, and a packet's crests are arcs around that
53
+ * centre; a cosine window of half-angle β is a partition of unity over them at every radius, so the front's
54
+ * height is the packet's amplitude whatever the spacing, splitting a front changes none of it, and one front
55
+ * adds at most two particles at any point.
24
56
  *
25
57
  * @type {CodeChunk}
26
58
  */
27
59
  export const chunk_water_particle = CodeChunk.from(/* language=WGSL */ `
28
60
  const WATER_PARTICLE_LEVELS: u32 = ${WATER_PARTICLE_LEVELS}u;
61
+ const WATER_PARTICLE_LEVEL_BITS: u32 = ${WATER_PARTICLE_LEVEL_BITS}u;
29
62
  const WATER_PARTICLE_LEVEL0_CELL: f32 = ${WATER_PARTICLE_LEVEL0_CELL};
63
+ const WATER_PARTICLE_STRETCH = array<f32, ${WATER_PARTICLE_STRETCH.length}>(${WATER_PARTICLE_STRETCH.join(', ')});
64
+ const WATER_PARTICLE_STRETCH_FIRST: f32 = ${WATER_PARTICLE_STRETCH_FIRST}.0;
30
65
  // kinematic viscosity of water, m²/s
31
66
  const WATER_VISCOSITY: f32 = 1e-6;
32
67
  const WATER_PI: f32 = 3.14159265358979;
@@ -66,7 +101,7 @@ fn water_particle_position(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> vec2<
66
101
  return particle.origin + water_particle_direction(particle.angle) * water_particle_distance(particle);
67
102
  }
68
103
 
69
- // The front's arc between neighbouring particles: the half-width of the tangential window.
104
+ // The front's arc between neighbouring particles, at the particle's own radius: what the window spans there.
70
105
  fn water_particle_width(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
71
106
  return max(particle.dispersion * water_particle_distance(particle), 1e-3 * particle.radius);
72
107
  }
@@ -77,9 +112,79 @@ fn water_particle_phase(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
77
112
  return water_particle_wavenumber(particle.radius) * lag;
78
113
  }
79
114
 
80
- // Radius of a circle around the particle holding its whole kernel: two radii across the front, the window along it.
81
- fn water_particle_support(radius: f32, width: f32) -> f32 {
82
- return length(vec2<f32>(2.0 * radius, width));
115
+ // How far from a splash's centre its ring starts: two radii, the packet's half-length, so it starts clear of the centre.
116
+ fn water_particle_launch_distance(radius: f32) -> f32 {
117
+ return 2.0 * radius;
118
+ }
119
+
120
+ // How fast the packet's envelope stretches, as a fraction of its group speed (WATER_PARTICLE_STRETCH): interpolated in
121
+ // log κ between quarter octaves of k against the wavenumber of the slowest crests.
122
+ fn water_particle_stretch(radius: f32) -> f32 {
123
+ let octave = log2(water_particle_wavenumber(radius) * sqrt(WATER_SURFACE_TENSION / WATER_GRAVITY));
124
+ let last = f32(${WATER_PARTICLE_STRETCH.length}u - 1u);
125
+ let t = clamp(4.0 * (octave - WATER_PARTICLE_STRETCH_FIRST), 0.0, last);
126
+ let i = u32(min(floor(t), last - 1.0));
127
+ return exp2(mix(log2(WATER_PARTICLE_STRETCH[i]), log2(WATER_PARTICLE_STRETCH[i + 1u]), t - f32(i)));
128
+ }
129
+
130
+ // Half the length of the packet's envelope across its front, for a front that stands distance from its centre: 2r when
131
+ // emitted, growing in quadrature with half the stretch over the distance travelled since. Held to half the largest support
132
+ // the hash holds, so a window no wider keeps the packet within the coarsest cells.
133
+ fn water_particle_half_length_at(radius: f32, distance: f32) -> f32 {
134
+ let travelled = max(distance - water_particle_launch_distance(radius), 0.0);
135
+ let stretched = length(vec2<f32>(2.0 * radius, 0.5 * water_particle_stretch(radius) * travelled));
136
+ return min(stretched, 0.5 * water_particle_support_limit());
137
+ }
138
+
139
+ fn water_particle_half_length(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
140
+ return water_particle_half_length_at(particle.radius, water_particle_distance(particle));
141
+ }
142
+
143
+ // Radius of a circle around the particle holding its whole kernel: the envelope's half-length h across the front, and
144
+ // along it the front's arc of half-angle β plus its outer edge's overhang. A point at radius ρ = D + u and angle ψ from
145
+ // a particle at distance D lies |d|² = u² + 4ρD·sin²(ψ/2) ≤ h² + (D + h)Dβ² ≤ h² + ((D + h/2)β)² from it, and
146
+ // width ≥ Dβ.
147
+ fn water_particle_support(half_length: f32, width: f32, dispersion: f32) -> f32 {
148
+ return length(vec2<f32>(half_length, width + 0.5 * half_length * dispersion));
149
+ }
150
+
151
+ // The largest support the hash holds: half its coarsest cell.
152
+ fn water_particle_support_limit() -> f32 {
153
+ return 0.5 * water_particle_cell_size(WATER_PARTICLE_LEVELS - 1u);
154
+ }
155
+
156
+ // Whether two particles are of one front: one splash's, neither reflected or faded apart, alike in all but angle.
157
+ fn water_particle_same_front(a: ${WATER_PARTICLE_STRUCT.wgsl_ref}, b: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> bool {
158
+ return a.body == b.body && all(a.origin == b.origin) && a.age == b.age && a.amplitude == b.amplitude
159
+ && a.radius == b.radius && a.dispersion == b.dispersion;
160
+ }
161
+
162
+ // How a particle is drawn, given its front's run of particles in the array and its place in it: 1 as itself; 0 not at
163
+ // all, or a group of 3^g as one packet with 3^g times its window, when its front is whole. Whole is every particle
164
+ // the front split into still there, in order and alike: as many as fill its circle. The step puts a particle's three
165
+ // in its place, the middle at its own angle, so a group of 3^g is 3^g in a row and its middle stands at the angle
166
+ // the group was emitted at; with curved crests the groups draw what the particles do. The largest group whose packet
167
+ // the hash holds: the seven a front was emitted as, until their windows outgrow the coarsest cell.
168
+ fn water_particle_group(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, members: u32, offset: u32) -> u32 {
169
+ if (members != u32(round(WATER_TAU / particle.dispersion))) { return 1u; }
170
+ var group = 1u;
171
+ for (var g = 0u; g < 20u && members % (3u * group) == 0u; g++) { group *= 3u; }
172
+ let distance = water_particle_distance(particle);
173
+ let half_length = water_particle_half_length_at(particle.radius, distance);
174
+ for (var g = 0u; g < 20u && group > 1u; g++) {
175
+ let dispersion = particle.dispersion * f32(group);
176
+ let width = max(dispersion * distance, 1e-3 * particle.radius);
177
+ if (water_particle_support(half_length, width, dispersion) <= water_particle_support_limit()) { break; }
178
+ group /= 3u;
179
+ }
180
+ return select(0u, group, offset % group == group / 2u);
181
+ }
182
+
183
+ // The particle as its group draws it: one packet with the group's window.
184
+ fn water_particle_drawn(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, group: u32) -> ${WATER_PARTICLE_STRUCT.wgsl_ref} {
185
+ var drawn = particle;
186
+ drawn.dispersion *= f32(group);
187
+ return drawn;
83
188
  }
84
189
 
85
190
  // The smallest hash level whose cell is at least the support's diameter, so the support overlaps at most
@@ -93,8 +198,13 @@ fn water_particle_cell_size(level: u32) -> f32 {
93
198
  return WATER_PARTICLE_LEVEL0_CELL * exp2(f32(level));
94
199
  }
95
200
 
201
+ // A hash entry's body and level in one word.
202
+ fn water_particle_key(body: u32, level: u32) -> u32 {
203
+ return (body << WATER_PARTICLE_LEVEL_BITS) | level;
204
+ }
205
+
96
206
  fn water_particle_bucket(body: u32, level: u32, cell: vec2<i32>, table_size: u32) -> u32 {
97
- let bits = pcg3d(vec3<u32>((body << 4u) | level, bitcast<u32>(cell.x), bitcast<u32>(cell.y)));
207
+ let bits = pcg3d(vec3<u32>(water_particle_key(body, level), bitcast<u32>(cell.x), bitcast<u32>(cell.y)));
98
208
  return bits.x & (table_size - 1u);
99
209
  }
100
210
 
@@ -105,39 +215,54 @@ struct WaterParticleField {
105
215
  displacement: vec2<f32>,
106
216
  // columns ∂D/∂x and ∂D/∂z: (∂Dx/∂x, ∂Dz/∂x), (∂Dx/∂z, ∂Dz/∂z)
107
217
  jacobian: mat2x2<f32>,
218
+ // the gradient of the displacement's signed length along the radius: the slope a quarter carrier cycle on, so that
219
+ // ½(|slope|² + |this|²) is the slope's mean square over a cycle
220
+ displacement_gradient: vec2<f32>,
108
221
  }
109
222
 
110
- // The packet at offset d from its particle. u across the front, v along it: envelope E(u) over two radii,
111
- // window T(v) over the width, carrier phase φ = πu/r − phase. Height a·E·T·cos φ; horizontal displacement
112
- // −a·E·T·sin φ along the direction (Gerstner-like orbits, Yuksel's longitudinal sign); with derivatives.
113
- fn water_particle_field(d: vec2<f32>, angle: f32, amplitude: f32, radius: f32, width: f32, phase: f32) -> WaterParticleField {
223
+ // The packet at offset d from its particle, which stands distance from its front's centre. ρ the radius from the
224
+ // centre and u = ρ − distance across the front, ψ the angle around the centre from the particle's direction:
225
+ // envelope E(u) over half_length either side, window T(ψ) over the dispersion β either side, carrier phase πu/r − phase.
226
+ // Height a·E·T·cos; horizontal displacement −a·E·T·sin along the radius (Gerstner-like orbits, Yuksel's longitudinal
227
+ // sign); with derivatives, ∇ψ = e_ψ/ρ.
228
+ fn water_particle_field(d: vec2<f32>, angle: f32, amplitude: f32, radius: f32, half_length: f32, dispersion: f32, distance: f32,
229
+ phase: f32) -> WaterParticleField {
114
230
  var field: WaterParticleField;
115
231
  let direction = water_particle_direction(angle);
116
232
  let tangent = vec2<f32>(-direction.y, direction.x);
117
- let u = dot(d, direction);
118
- let v = dot(d, tangent);
119
- if (abs(u) >= 2.0 * radius || abs(v) >= width) { return field; }
120
- let eu = 0.5 * WATER_PI * u / radius;
233
+ let q = d + direction * distance;
234
+ let rho = length(q);
235
+ // ρ − distance, without the cancellation
236
+ let u = (dot(d, d) + 2.0 * distance * dot(d, direction)) / max(rho + distance, 1e-30);
237
+ if (abs(u) >= half_length || !(rho > 0.0)) { return field; }
238
+ let psi = atan2(dot(d, tangent), distance + dot(d, direction));
239
+ if (abs(psi) >= dispersion) { return field; }
240
+ let radial = q / rho;
241
+ let around = vec2<f32>(-radial.y, radial.x);
242
+ let eu = WATER_PI * u / half_length;
121
243
  let envelope = 0.5 * (1.0 + cos(eu));
122
- let envelope_du = -0.25 * WATER_PI / radius * sin(eu);
123
- let tv = WATER_PI * v / width;
244
+ let envelope_du = -0.5 * WATER_PI / half_length * sin(eu);
245
+ let tv = WATER_PI * psi / dispersion;
124
246
  let window = 0.5 * (1.0 + cos(tv));
125
- let window_dv = -0.5 * WATER_PI / width * sin(tv);
247
+ let window_dpsi = -0.5 * WATER_PI / dispersion * sin(tv);
126
248
  let carrier = WATER_PI * u / radius - phase;
127
249
  let c = cos(carrier);
128
250
  let s = sin(carrier);
129
251
  let k = WATER_PI / radius;
130
252
  let z = amplitude * envelope * window * c;
131
253
  let z_u = amplitude * window * (envelope_du * c - envelope * s * k);
132
- let z_v = amplitude * envelope * window_dv * c;
254
+ let z_psi = amplitude * envelope * window_dpsi * c;
133
255
  let h = -amplitude * envelope * window * s;
134
256
  let h_u = -amplitude * window * (envelope_du * s + envelope * c * k);
135
- let h_v = -amplitude * envelope * window_dv * s;
136
- let gradient_h = h_u * direction + h_v * tangent;
257
+ let h_psi = -amplitude * envelope * window_dpsi * s;
258
+ let gradient_h = h_u * radial + (h_psi / rho) * around;
259
+ // the radius turns as the point moves around the centre: ∂e_ρ/∂x_j = e_ψ (e_ψ)_j / ρ
260
+ let turn = (h / rho) * around;
137
261
  field.height = z;
138
- field.slope = z_u * direction + z_v * tangent;
139
- field.displacement = h * direction;
140
- field.jacobian = mat2x2<f32>(direction * gradient_h.x, direction * gradient_h.y);
262
+ field.slope = z_u * radial + (z_psi / rho) * around;
263
+ field.displacement = h * radial;
264
+ field.jacobian = mat2x2<f32>(radial * gradient_h.x + turn * around.x, radial * gradient_h.y + turn * around.y);
265
+ field.displacement_gradient = gradient_h;
141
266
  return field;
142
267
  }
143
268
  `, [chunk_water_spectrum, chunk_pcg3d, WATER_PARTICLE_STRUCT.declaration_chunk, WATER_PARTICLE_ENTRY_STRUCT.declaration_chunk]);
@@ -2,7 +2,12 @@
2
2
  export const WATER_PARTICLE_FADE_SECONDS: 0.15;
3
3
  /**
4
4
  * One frame of one wave particle (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §5.1): it ages, spreads over its
5
- * widening front, damps, reflects off its body's authored outline, and dies or splits.
5
+ * widening front, damps, meets its body's authored outline, and dies or splits.
6
+ *
7
+ * At the outline the water shows the packet plus the shore's share of its mirror image in the outline (the image
8
+ * method, as Jeschke & Wojtan 2017 §4.4 keep a "ghost" of a reflected packet): the image is emitted the step the first
9
+ * of the packet's support leaves the body, and the packet carries on until the last of it has left it too. The body's
10
+ * outline clips what of either lies beyond it, so the water changes only as the two move.
6
11
  *
7
12
  * Whether a point is over the body is a down-ray into the body's own BLAS, so the outline is exactly the
8
13
  * authored mesh's, holes for islands included. Bindings: `geometries`, `blas_lookup`, `blas_data`,
@@ -1 +1 @@
1
- {"version":3,"file":"chunk_water_particle_step.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/water/surface/particles/chunk_water_particle_step.js"],"names":[],"mappings":"AAOA,qHAAqH;AACrH,+CAAgD;AAEhD;;;;;;;;;GASG;AACH,wCAFU,SAAS,CAsGsC;0BAxH/B,uCAAuC"}
1
+ {"version":3,"file":"chunk_water_particle_step.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/water/surface/particles/chunk_water_particle_step.js"],"names":[],"mappings":"AAQA,qHAAqH;AACrH,+CAAgD;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wCAFU,SAAS,CAsN8E;0BA9OvE,uCAAuC"}
@@ -1,6 +1,7 @@
1
1
  import { CodeChunk } from "../../../shader/compiler/CodeChunk.js";
2
2
  import { BVH_TRAVERSAL_STACK_SIZE } from "../../../shader/chunk/bvh/BVH_TRAVERSAL_STACK_SIZE.js";
3
3
  import { chunk_ray_query_blas_nearest } from "../../../shader/chunk/geometry/ray/chunk_ray_query_blas_nearest.js";
4
+ import { chunk_geometry_read_triangle_positions } from "../../../shader/chunk/geometry/format/chunk_geometry_read_triangle_positions.js";
4
5
  import { RAY_HIT_INFO } from "../../../shader/chunk/geometry/ray/RAY_HIT_INFO.js";
5
6
  import { WATER_PARTICLE_STRUCT } from "./WATER_PARTICLE_STRUCT.js";
6
7
  import { chunk_water_particle } from "./chunk_water_particle.js";
@@ -10,7 +11,12 @@ export const WATER_PARTICLE_FADE_SECONDS = 0.15;
10
11
 
11
12
  /**
12
13
  * One frame of one wave particle (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §5.1): it ages, spreads over its
13
- * widening front, damps, reflects off its body's authored outline, and dies or splits.
14
+ * widening front, damps, meets its body's authored outline, and dies or splits.
15
+ *
16
+ * At the outline the water shows the packet plus the shore's share of its mirror image in the outline (the image
17
+ * method, as Jeschke & Wojtan 2017 §4.4 keep a "ghost" of a reflected packet): the image is emitted the step the first
18
+ * of the packet's support leaves the body, and the packet carries on until the last of it has left it too. The body's
19
+ * outline clips what of either lies beyond it, so the water changes only as the two move.
14
20
  *
15
21
  * Whether a point is over the body is a down-ray into the body's own BLAS, so the outline is exactly the
16
22
  * authored mesh's, holes for islands included. Bindings: `geometries`, `blas_lookup`, `blas_data`,
@@ -22,8 +28,11 @@ export const chunk_water_particle_step = CodeChunk.from(/* language=WGSL */ `
22
28
  const WATER_PARTICLE_FADE_SECONDS: f32 = ${WATER_PARTICLE_FADE_SECONDS};
23
29
 
24
30
  struct WaterParticleStep {
31
+ // the particle a step on
25
32
  particle: ${WATER_PARTICLE_STRUCT.wgsl_ref},
26
- // 0 dies, 1 lives on, 3 splits into three
33
+ // the mirror image it leaves, when it leaves one
34
+ image: ${WATER_PARTICLE_STRUCT.wgsl_ref},
35
+ // the particles it leaves, each split into three when they split: 0, 1, 2 (it, then its image), 3 or 6
27
36
  children: u32,
28
37
  }
29
38
 
@@ -37,6 +46,50 @@ fn water_particle_over_body(geometry_index: u32, local: vec2<f32>) -> ${RAY_HIT_
37
46
  top - box[1] + margin), geometry_index, &stack, 0u);
38
47
  }
39
48
 
49
+ // The points of a packet's support the step tests against the outline, on its arc ρ from its front's centre: the k-th
50
+ // of its axis and its window's ends, β either side of it. The support is the annular sector of two radii either side of
51
+ // the front and β either side of the axis, so a straight outline meets first the point of its leading arc whose
52
+ // direction is nearest the outline's normal, an end unless the normal is within β of the axis, and leaves last the end
53
+ // of its trailing arc farthest from it: at any incidence, these three find both, or within the arc's sagitta.
54
+ fn water_particle_support_point(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, k: u32, rho: f32) -> vec2<f32> {
55
+ let turn = select(0.0, select(particle.dispersion, -particle.dispersion, k == 1u), k > 0u);
56
+ return particle.origin + water_particle_direction(particle.angle + turn) * rho;
57
+ }
58
+
59
+ // Whether a triangle seen from above holds a point strictly inside it, in the body's local xz.
60
+ fn water_triangle_covers(a: vec2<f32>, b: vec2<f32>, c: vec2<f32>, point: vec2<f32>) -> bool {
61
+ let side = sign((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x));
62
+ let ab = (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x);
63
+ let bc = (c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x);
64
+ let ca = (a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x);
65
+ return side * ab > 0.0 && side * bc > 0.0 && side * ca > 0.0;
66
+ }
67
+
68
+ // The k-th tested point of a particle's leading arc once it has stepped dt on: what water_particle_advance tests.
69
+ fn water_particle_leading_point(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, dt: f32, k: u32) -> vec2<f32> {
70
+ var stepped = particle;
71
+ stepped.age += dt;
72
+ return water_particle_support_point(stepped, k, water_particle_distance(stepped) + water_particle_half_length(stepped));
73
+ }
74
+
75
+ // What is already known of a particle's leading arc a step on, bit k for its k-th tested point: whether that is known,
76
+ // and whether it is over the body; and the triangle under the axis's point when that is. Zero is nothing known. A
77
+ // particle's window's ends are its neighbours' axis points when they are of its front, so the update pass hands each
78
+ // what its neighbours in the subgroup found.
79
+ struct WaterParticleArcHint {
80
+ known: u32,
81
+ over: u32,
82
+ triangle: u32,
83
+ }
84
+
85
+ // Whether any of an arc's tested points is over the body.
86
+ fn water_particle_arc_over(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, geometry_index: u32, scale: vec2<f32>, rho: f32) -> bool {
87
+ for (var k = 0u; k < 3u; k++) {
88
+ if (water_particle_over_body(geometry_index, water_particle_support_point(particle, k, rho) / scale).t >= 0.0) { return true; }
89
+ }
90
+ return false;
91
+ }
92
+
40
93
  // The line of the edge of a triangle nearest a point inside it, in the metric xz frame: a point on it and a
41
94
  // unit normal. Which side the normal points to does not matter: a mirror in a line is the same for either.
42
95
  struct WaterOutlineLine {
@@ -62,54 +115,119 @@ fn water_outline_line(geometry_index: u32, triangle: u32, point: vec2<f32>, scal
62
115
  return line;
63
116
  }
64
117
 
118
+ // The particle's mirror image in a line of the outline: its front's centre and its direction mirrored. Not yet reflected.
119
+ fn water_particle_mirrored(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, line: WaterOutlineLine) -> ${WATER_PARTICLE_STRUCT.wgsl_ref} {
120
+ var image = particle;
121
+ let direction = water_particle_direction(particle.angle);
122
+ let mirrored = direction - 2.0 * dot(direction, line.normal) * line.normal;
123
+ image.origin -= 2.0 * dot(particle.origin - line.point, line.normal) * line.normal;
124
+ image.angle = atan2(mirrored.y, mirrored.x);
125
+ image.reflected = 0u;
126
+ return image;
127
+ }
128
+
65
129
  // Advance a particle by dt. scale turns the body's metric frame into its local one; reflection is the
66
- // outline's amplitude reflection coefficient; threshold is the steepness |a|/r below which it dies.
130
+ // outline's amplitude reflection coefficient; threshold is the steepness |a|/r below which it dies; hint is what is
131
+ // already known of its leading arc.
67
132
  fn water_particle_advance(input: ${WATER_PARTICLE_STRUCT.wgsl_ref}, dt: f32, geometry_index: u32, scale: vec2<f32>,
68
- reflection: f32, threshold: f32) -> WaterParticleStep {
133
+ reflection: f32, threshold: f32, hint: WaterParticleArcHint) -> WaterParticleStep {
69
134
  var particle = input;
70
- let before = water_particle_position(particle);
71
135
  let distance_before = water_particle_distance(particle);
136
+ let half_length_before = water_particle_half_length(particle);
72
137
  particle.age += dt;
73
138
  let distance_after = water_particle_distance(particle);
74
- // energy spread over the widening circumference, and the water's own damping
75
- particle.amplitude *= sqrt(max(distance_before, 1e-30) / max(distance_after, 1e-30))
76
- * exp(-water_particle_damping(particle.radius) * dt);
77
- let after = water_particle_position(particle);
78
- if (water_particle_over_body(geometry_index, after / scale).t < 0.0) {
79
- let start = water_particle_over_body(geometry_index, before / scale);
80
- if (!(reflection > 0.0) || start.t < 0.0) { return WaterParticleStep(particle, 0u); }
81
- // where along this frame's step it left the mesh
82
- var lo = 0.0;
83
- var hi = 1.0;
84
- var last = start;
85
- for (var halving = 0u; halving < 6u; halving++) {
86
- let mid = 0.5 * (lo + hi);
87
- let probe = water_particle_over_body(geometry_index, mix(before, after, mid) / scale);
88
- if (probe.t >= 0.0) { lo = mid; last = probe; } else { hi = mid; }
89
- }
90
- // the mirror image of its front in the outline's own line, wherever along the step it was crossed
91
- let line = water_outline_line(geometry_index, last.triangle, mix(before, after, lo), scale);
92
- let normal = line.normal;
93
- let direction = water_particle_direction(particle.angle);
94
- let reflected = direction - 2.0 * dot(direction, normal) * normal;
95
- particle.origin -= 2.0 * dot(particle.origin - line.point, normal) * normal;
96
- particle.angle = atan2(reflected.y, reflected.x);
97
- particle.amplitude *= reflection;
98
- // a corner can send the mirror image off the mesh too
99
- if (water_particle_over_body(geometry_index, water_particle_position(particle) / scale).t < 0.0) {
100
- return WaterParticleStep(particle, 0u);
101
- }
102
- }
139
+ // energy spread over the widening circumference and the lengthening envelope (Jeschke & Wojtan 2017 eq. 17), and the
140
+ // water's own damping
141
+ particle.amplitude *= sqrt(max(distance_before, 1e-30) / max(distance_after, 1e-30)
142
+ * half_length_before / water_particle_half_length(particle)) * exp(-water_particle_damping(particle.radius) * dt);
103
143
  // below the threshold a packet fades out rather than vanishing: its amplitude decays on top of the water's own
104
144
  // damping until it is a tenth of the threshold's height, where it dies
105
145
  let floor = threshold * particle.radius;
106
146
  if (!(abs(particle.amplitude) >= floor)) {
107
147
  particle.amplitude *= exp(-dt / WATER_PARTICLE_FADE_SECONDS);
108
- if (!(abs(particle.amplitude) >= 0.1 * floor)) { return WaterParticleStep(particle, 0u); }
148
+ if (!(abs(particle.amplitude) >= 0.1 * floor)) { return WaterParticleStep(particle, particle, 0u); }
149
+ }
150
+ // its support: its envelope's half-length either side of its front, its window's angle either side of its axis
151
+ let reach = water_particle_half_length(particle);
152
+ // whether any of it may still be over the body
153
+ var remains = true;
154
+ var image = particle;
155
+ var mirrored = false;
156
+ if (particle.reflected == 0u) {
157
+ // which of its leading arc's tested points are over the body: what the hint knows, then the axis's by a down-ray,
158
+ // the ends by the triangle under it, which covers them away from the outline, and the rest by a down-ray each
159
+ let ahead = distance_after + reach;
160
+ var known = hint.known;
161
+ var over = hint.over & known;
162
+ var triangle = hint.triangle;
163
+ if ((known & 1u) == 0u) {
164
+ let hit = water_particle_over_body(geometry_index, water_particle_support_point(particle, 0u, ahead) / scale);
165
+ known |= 1u;
166
+ over |= select(0u, 1u, hit.t >= 0.0);
167
+ triangle = hit.triangle;
168
+ }
169
+ if ((over & 1u) != 0u && (known & 6u) != 6u) {
170
+ var a: vec3<f32>;
171
+ var b: vec3<f32>;
172
+ var c: vec3<f32>;
173
+ geometry_read_triangle_positions(&a, &b, &c, geometries[geometry_index], triangle);
174
+ for (var k = 1u; k < 3u; k++) {
175
+ if (((known >> k) & 1u) == 0u && water_triangle_covers(a.xz, b.xz, c.xz, water_particle_support_point(particle, k, ahead) / scale)) {
176
+ known |= 1u << k;
177
+ over |= 1u << k;
178
+ }
179
+ }
180
+ }
181
+ for (var k = 1u; k < 3u; k++) {
182
+ if (((known >> k) & 1u) == 0u
183
+ && water_particle_over_body(geometry_index, water_particle_support_point(particle, k, ahead) / scale).t >= 0.0) { over |= 1u << k; }
184
+ }
185
+ // the first of them to leave the body: one off it whose radius was over it where its trailing arc was when the
186
+ // step began. With none over it there, the point is not leaving the body but entering it, as a mirror image's
187
+ // do: every radius of one starts beyond its outline.
188
+ var crossing = 3u;
189
+ var back = vec2<f32>(0.0);
190
+ var last: ${RAY_HIT_INFO.wgsl_ref};
191
+ for (var k = 0u; k < 3u && crossing == 3u; k++) {
192
+ if (((over >> k) & 1u) != 0u) { continue; }
193
+ back = water_particle_support_point(particle, k, distance_before - reach);
194
+ last = water_particle_over_body(geometry_index, back / scale);
195
+ if (last.t >= 0.0) { crossing = k; }
196
+ }
197
+ if (crossing < 3u) {
198
+ // it is kept this step: if all of it has left the body too, it dies at the next, unseen beyond the outline
199
+ particle.reflected = 1u;
200
+ // where from there that radius leaves the body
201
+ let leading = water_particle_support_point(particle, crossing, ahead);
202
+ var lo = 0.0;
203
+ var hi = 1.0;
204
+ for (var halving = 0u; halving < 10u; halving++) {
205
+ let mid = 0.5 * (lo + hi);
206
+ let probe = water_particle_over_body(geometry_index, mix(back, leading, mid) / scale);
207
+ if (probe.t >= 0.0) { lo = mid; last = probe; } else { hi = mid; }
208
+ }
209
+ // the mirror image of its front in the outline's own line, wherever that was crossed
210
+ image = water_particle_mirrored(particle, water_outline_line(geometry_index, last.triangle, mix(back, leading, lo), scale));
211
+ image.amplitude *= reflection;
212
+ mirrored = abs(image.amplitude) >= 0.1 * floor;
213
+ } else if (over == 0u) {
214
+ // with none of them over the body and none leaving it, all of it is beyond the outline: a splash off the
215
+ // mesh, or a mirror image landing off it at a corner (one entering the body has the point it was mirrored
216
+ // from over it)
217
+ remains = false;
218
+ }
219
+ } else {
220
+ remains = water_particle_arc_over(particle, geometry_index, scale, distance_after - reach);
109
221
  }
110
- // Yuksel's subdivision, at a coarser spacing: the window along the front keeps its height
111
- if (water_particle_width(particle) > 2.0 * particle.radius) { return WaterParticleStep(particle, 3u); }
112
- return WaterParticleStep(particle, 1u);
222
+ // Yuksel's subdivision, at a coarser spacing: the three windows of a third of the angle sum to the front's height
223
+ // as the parents' did, so the surface does not change. A window splits once its arc along the front is longer than
224
+ // its envelope is across it, so a packet's support stays about round, whatever its stretch. An image is as wide as
225
+ // its packet and splits with it.
226
+ let split = select(1u, 3u, water_particle_width(particle) > water_particle_half_length(particle));
227
+ // a reflected packet lives until the last of it has left the body
228
+ if (!remains) { return WaterParticleStep(particle, particle, 0u); }
229
+ if (mirrored) { return WaterParticleStep(particle, image, 2u * split); }
230
+ return WaterParticleStep(particle, particle, split);
113
231
  }
114
232
 
115
233
  // The death threshold: a packet flatter than the body's own wind waves is not drawn, and the fuller the pool,
@@ -118,4 +236,4 @@ fn water_particle_threshold(slope_variance: f32, live: u32, capacity: u32) -> f3
118
236
  let pressure = f32(live) / f32(max(capacity, 1u));
119
237
  return max(0.003, 0.1 * sqrt(max(slope_variance, 0.0))) / max(1.0 - pressure, 0.05);
120
238
  }
121
- `, [chunk_water_particle, chunk_ray_query_blas_nearest]);
239
+ `, [chunk_water_particle, chunk_ray_query_blas_nearest, chunk_geometry_read_triangle_positions]);