Skip to main content

roxlap_scene/
water.rs

1//! WT.0 — water volumes (`PORTING-WATER.md`): the *physics*
2//! representation of water.
3//!
4//! Why not the CC.4 passable-veto: the veto classifies by voxel
5//! colour, and `.vxl` slab interiors are colourless by format
6//! (`Cube::UnexposedSolid`) — a filled pool has a core the veto never
7//! sees, so colour-keyed water works only ~2 voxels deep. A
8//! [`WaterVolume`] is instead declared *beside* the voxels: an
9//! axis-aligned grid-local box that answers "is this point in water,
10//! and how deep?" independently of what the slabs store. The visuals
11//! stay voxels (fill the same region with a
12//! [`BlendMode::Volumetric`](roxlap_formats::material::BlendMode)
13//! material); the veto stays useful for thin pass-through curtains.
14//!
15//! Conventions (chapter 2: **+z is DOWN**):
16//! - `lo`/`hi` are **inclusive** voxel corners; voxel `(x, y, z)`
17//!   occupies the continuous cube `[x, x+1)³`, so the volume spans
18//!   `[lo, hi + 1)` in continuous grid-local coordinates.
19//! - The **surface is the volume's top face** — the plane
20//!   `z = lo.z` (smaller z is higher).
21//! - Volumes are grid-local: a scaled grid's world waterline moves
22//!   with `voxel_world_size`, and a rotated grid's surface tilts with
23//!   it. Water is *usually* authored on identity/yaw-only grids so
24//!   the surface stays world-horizontal; the queries are exact under
25//!   any transform regardless.
26//!
27//! Authoring state only, like [`crate::BakeLight`]s: editing the list
28//! re-renders nothing by itself (the *visual* water is voxels you
29//! edit separately). Persisted in snapshots since wire **v3**.
30
31use glam::{DVec3, IVec3};
32use serde::{Deserialize, Serialize};
33
34use crate::{GridId, Scene};
35
36/// An axis-aligned box of water in grid-local voxel coordinates,
37/// inclusive on both corners. See the module doc for the coordinate
38/// conventions (surface = `lo.z`, z-down).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub struct WaterVolume {
41    /// Minimum corner (inclusive). `lo.z` is the **surface** layer.
42    pub lo: IVec3,
43    /// Maximum corner (inclusive). `hi.z` is the bottom layer.
44    pub hi: IVec3,
45}
46
47impl WaterVolume {
48    /// A volume spanning the two corners in any order (canonicalised
49    /// so `lo <= hi` per component — `PartialEq` compares fields, so
50    /// prefer this over a raw struct literal).
51    #[must_use]
52    pub fn new(a: IVec3, b: IVec3) -> Self {
53        Self {
54            lo: a.min(b),
55            hi: a.max(b),
56        }
57    }
58
59    /// Depth of a continuous grid-local point below this volume's
60    /// surface, in **voxel units**: `Some(distance below the min-z
61    /// face)` when the point is inside the water, `None` outside.
62    /// Zero exactly at the surface plane; positive going down
63    /// (z-down).
64    ///
65    /// Corner order is normalised HERE, not assumed: the fields are
66    /// pub (and easy to swap under z-down), so a hand-built
67    /// `WaterVolume { lo: big, hi: small }` queries identically to
68    /// its [`Self::new`]-canonicalised form — and identically before
69    /// and after a snapshot round-trip.
70    #[must_use]
71    pub fn depth_local(&self, p: DVec3) -> Option<f64> {
72        let lo = self.lo.min(self.hi).as_dvec3();
73        let hi = self.lo.max(self.hi).as_dvec3() + DVec3::ONE; // inclusive → half-open
74        let inside =
75            p.x >= lo.x && p.x < hi.x && p.y >= lo.y && p.y < hi.y && p.z >= lo.z && p.z < hi.z;
76        inside.then_some(p.z - lo.z)
77    }
78}
79
80impl crate::Grid {
81    /// Register a water volume spanning the two grid-local voxel
82    /// corners (any order). Convenience over pushing onto
83    /// [`water_volumes`](crate::Grid::water_volumes) directly — this
84    /// normalises the corners.
85    pub fn add_water_volume(&mut self, a: IVec3, b: IVec3) {
86        self.water_volumes.push(WaterVolume::new(a, b));
87    }
88
89    /// Depth of a continuous **grid-local** point below the water
90    /// surface, in voxel units — the deepest submersion across this
91    /// grid's volumes (overlapping volumes: the highest surface
92    /// wins). `None` when the point is in no volume.
93    #[must_use]
94    pub fn water_depth_local(&self, p: DVec3) -> Option<f64> {
95        self.water_volumes
96            .iter()
97            .filter_map(|v| v.depth_local(p))
98            .max_by(f64::total_cmp)
99    }
100}
101
102impl Scene {
103    /// Depth of a **world**-space point below a water surface, in
104    /// **world units**, with the grid that owns the deepest water at
105    /// that point. `None` when no grid's water contains the point.
106    ///
107    /// The world↔grid boundary rule (SC) applies: the point is
108    /// converted into each grid's local voxel frame (un-rotate,
109    /// un-translate, `/ voxel_world_size`), the depth is measured
110    /// along the grid's **local** z (world-vertical for the usual
111    /// identity/yaw-only water grids), and converted back to world
112    /// units (`× voxel_world_size`).
113    ///
114    /// Deterministic: on an exact depth tie between grids (two
115    /// identity grids sharing a waterline at their seam), the
116    /// smallest [`GridId`] wins — [`Scene::grids`] iterates a
117    /// `HashMap`, so without the explicit tie-break the winner would
118    /// flip between runs (and WT.2/WT.3 hang per-grid state off this
119    /// id).
120    #[must_use]
121    pub fn water_depth_at(&self, world: DVec3) -> Option<(GridId, f64)> {
122        self.grids()
123            .filter(|(_, grid)| !grid.water_volumes.is_empty())
124            .filter_map(|(id, grid)| {
125                let local = crate::streaming::world_to_grid_local_pos(world, &grid.transform);
126                let d_local = grid.water_depth_local(local)?;
127                Some((id, d_local * grid.transform.voxel_world_size))
128            })
129            .max_by(|a, b| a.1.total_cmp(&b.1).then_with(|| b.0.cmp(&a.0)))
130    }
131
132    /// `true` when the world-space point is inside any water volume.
133    /// Short-circuits on the first hit — the query the swim state
134    /// (WT.1) runs per actor per frame; use
135    /// [`Self::water_depth_at`] only when the depth is needed.
136    #[must_use]
137    pub fn in_water(&self, world: DVec3) -> bool {
138        self.grids()
139            .filter(|(_, grid)| !grid.water_volumes.is_empty())
140            .any(|(_, grid)| {
141                let local = crate::streaming::world_to_grid_local_pos(world, &grid.transform);
142                grid.water_depth_local(local).is_some()
143            })
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::GridTransform;
151    use glam::DQuat;
152
153    // ---- WaterVolume geometry ----
154
155    #[test]
156    fn corners_normalise_and_span_is_inclusive() {
157        let v = WaterVolume::new(IVec3::new(9, 9, 9), IVec3::new(2, 3, 4));
158        assert_eq!(v.lo, IVec3::new(2, 3, 4));
159        assert_eq!(v.hi, IVec3::new(9, 9, 9));
160        // Inclusive hi: the far corner voxel's interior is inside...
161        assert!(v.depth_local(DVec3::new(9.5, 9.5, 9.5)).is_some());
162        // ...and the half-open continuous edge just past it is not.
163        assert!(v.depth_local(DVec3::new(10.0, 9.5, 9.5)).is_none());
164    }
165
166    #[test]
167    fn depth_is_zero_at_surface_and_grows_downward() {
168        // z-down: the surface is the MIN-z face.
169        let v = WaterVolume::new(IVec3::new(0, 0, 10), IVec3::new(7, 7, 19));
170        assert_eq!(v.depth_local(DVec3::new(4.0, 4.0, 10.0)), Some(0.0));
171        assert_eq!(v.depth_local(DVec3::new(4.0, 4.0, 15.5)), Some(5.5));
172        // Above the surface (smaller z) is dry.
173        assert!(v.depth_local(DVec3::new(4.0, 4.0, 9.9)).is_none());
174        // Below the bottom is outside the volume, not "very deep".
175        assert!(v.depth_local(DVec3::new(4.0, 4.0, 20.0)).is_none());
176    }
177
178    // ---- Grid / Scene queries ----
179
180    #[test]
181    fn grid_takes_deepest_of_overlapping_volumes() {
182        let mut g = crate::Grid::new(GridTransform::identity());
183        // Two overlapping pools; the one with the HIGHER surface
184        // (smaller lo.z) reads deeper at a shared point.
185        g.add_water_volume(IVec3::new(0, 0, 10), IVec3::new(9, 9, 19));
186        g.add_water_volume(IVec3::new(0, 0, 14), IVec3::new(9, 9, 19));
187        assert_eq!(g.water_depth_local(DVec3::new(5.0, 5.0, 16.0)), Some(6.0));
188    }
189
190    #[test]
191    fn scene_world_query_identity_grid() {
192        let mut scene = Scene::new();
193        let id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 0.0, 0.0)));
194        scene
195            .grid_mut(id)
196            .unwrap()
197            .add_water_volume(IVec3::new(0, 0, 50), IVec3::new(31, 31, 63));
198        // World x=110 → local x=10; world z=55 → 5 under the surface.
199        assert_eq!(
200            scene.water_depth_at(DVec3::new(110.0, 5.0, 55.0)),
201            Some((id, 5.0))
202        );
203        assert!(scene.water_depth_at(DVec3::new(110.0, 5.0, 49.0)).is_none());
204        assert!(!scene.in_water(DVec3::new(0.0, 0.0, 55.0))); // outside grid
205    }
206
207    #[test]
208    fn scene_world_query_scales_with_vws() {
209        // SC discipline: a vws=0.5 grid's water is half as deep in
210        // world units, and the world waterline sits at origin.z +
211        // lo.z * vws.
212        let mut scene = Scene::new();
213        let id = scene.add_grid(GridTransform::at_scale(DVec3::ZERO, 0.5));
214        scene
215            .grid_mut(id)
216            .unwrap()
217            .add_water_volume(IVec3::new(0, 0, 20), IVec3::new(15, 15, 39));
218        // World z=10 is the surface (20 voxels × 0.5); 4 world units
219        // down = 8 voxels down.
220        assert_eq!(
221            scene.water_depth_at(DVec3::new(3.0, 3.0, 14.0)),
222            Some((id, 4.0))
223        );
224        assert!(scene.water_depth_at(DVec3::new(3.0, 3.0, 9.9)).is_none());
225    }
226
227    #[test]
228    fn scene_world_query_rotated_grid() {
229        // 90° yaw about z: world (x, y) → local (y, -x); z unchanged,
230        // so the surface stays world-horizontal under yaw.
231        let mut scene = Scene::new();
232        let rot = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2);
233        let id = scene.add_grid(GridTransform {
234            origin: DVec3::ZERO,
235            rotation: rot,
236            ..GridTransform::identity()
237        });
238        scene
239            .grid_mut(id)
240            .unwrap()
241            .add_water_volume(IVec3::new(0, 0, 8), IVec3::new(9, 19, 15));
242        // Local target (5, 10, 12) → world = rot * local.
243        let world = rot * DVec3::new(5.0, 10.0, 12.0);
244        assert_eq!(scene.water_depth_at(world), Some((id, 4.0)));
245        // A point that would be inside pre-rotation but isn't post.
246        assert!(scene.water_depth_at(DVec3::new(5.0, 10.0, 12.0)).is_none());
247    }
248
249    #[test]
250    fn swapped_corners_query_like_canonical_ones() {
251        // The fields are pub and easy to swap under z-down; a
252        // hand-built inverted volume must behave exactly like its
253        // canonical form — live AND across a snapshot round-trip
254        // (depth_local normalises, nothing else has to).
255        let swapped = WaterVolume {
256            lo: IVec3::new(7, 7, 19),
257            hi: IVec3::new(0, 0, 10),
258        };
259        let canonical = WaterVolume::new(IVec3::new(0, 0, 10), IVec3::new(7, 7, 19));
260        for p in [
261            DVec3::new(4.0, 4.0, 10.0),
262            DVec3::new(4.0, 4.0, 15.5),
263            DVec3::new(4.0, 4.0, 9.9),
264            DVec3::new(8.5, 4.0, 15.0),
265        ] {
266            assert_eq!(swapped.depth_local(p), canonical.depth_local(p));
267        }
268    }
269
270    #[test]
271    fn equal_depth_tie_breaks_to_smallest_grid_id() {
272        // Two identity grids sharing a waterline: the returned owner
273        // must be deterministic (grids() iterates a HashMap).
274        let mut scene = Scene::new();
275        let a = scene.add_grid(GridTransform::identity());
276        let b = scene.add_grid(GridTransform::identity());
277        for id in [a, b] {
278            scene
279                .grid_mut(id)
280                .unwrap()
281                .add_water_volume(IVec3::new(0, 0, 50), IVec3::new(31, 31, 63));
282        }
283        let winner = scene.water_depth_at(DVec3::new(5.0, 5.0, 55.0));
284        assert_eq!(winner, Some((a.min(b), 5.0)), "smallest GridId wins ties");
285    }
286
287    #[test]
288    fn no_volumes_means_dry_everywhere() {
289        let mut scene = Scene::new();
290        scene.add_grid(GridTransform::identity());
291        assert!(scene.water_depth_at(DVec3::new(1.0, 2.0, 3.0)).is_none());
292        assert!(!scene.in_water(DVec3::splat(50.0)));
293    }
294}