Skip to main content

concinnity_asset/
water_surface.rs

1// Animated water-surface schema.
2
3use crate::AssetId;
4use alloc::vec;
5use alloc::vec::Vec;
6
7/// Maximum number of waves per water surface. Shared by the render backends'
8/// wave uniforms and the build-side water validator.
9pub const MAX_WATER_WAVES: usize = 4;
10
11/// One wave in a water surface's motion. A surface sums up to four of these
12/// to displace its flat grid. Each wave travels
13/// horizontally along `direction`, rising and falling with `amplitude` peak
14/// height, `wavelength` distance between crests, and `speed` metres per second.
15/// `steepness` in [0, 1] pinches the crests and broadens the troughs (choppier
16/// water).
17#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
18#[serde(default)]
19pub struct WaterWave {
20    /// Peak height of the wave, in world units.
21    pub amplitude: f32,
22    /// Distance between successive crests, in world units.
23    pub wavelength: f32,
24    /// Horizontal travel speed, in metres per second.
25    pub speed: f32,
26    /// Horizontal travel direction `[x, z]`.
27    pub direction: [f32; 2],
28    /// Crest sharpness in [0, 1]. 0 is a smooth sine; higher pinches crests and
29    /// broadens troughs.
30    pub steepness: f32,
31}
32
33impl Default for WaterWave {
34    fn default() -> Self {
35        Self {
36            amplitude: 0.15,
37            wavelength: 4.0,
38            speed: 1.0,
39            direction: [1.0, 0.0],
40            steepness: 0.4,
41        }
42    }
43}
44
45/// A translucent animated water surface.
46///
47/// A flat, subdivided horizontal surface whose vertices ripple with summed
48/// waves. It refracts and reflects the scene, blends from a shallow to a deep
49/// colour with depth, and adds shoreline foam.
50///
51/// The surface is positioned by `centre` and sized by `extent` (XZ
52/// half-widths). The mesh itself is flat; all height variation comes from the
53/// animated waves.
54///
55/// ```rust
56/// # use concinnity_asset::WaterSurface;
57/// WaterSurface {
58///     centre: [0.0, 0.4, 0.0],
59///     extent: [12.0, 8.0],
60///     subdivisions: 96,
61///     ..Default::default()
62/// };
63/// ```
64#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65#[serde(default)]
66pub struct WaterSurface {
67    /// Asset identity; injected via `inject_name`. Not part of `args`.
68    #[serde(skip)]
69    pub asset_id: AssetId,
70    /// World-space position of the surface's centre.
71    pub centre: [f32; 3],
72    /// Half-width and half-depth of the surface `[x, z]`, in world units.
73    pub extent: [f32; 2],
74    /// Grid subdivisions across the surface. Higher gives smoother waves.
75    /// Clamped to [8, 255].
76    pub subdivisions: u32,
77    /// The waves summed to animate the surface (up to 4). Defaults to a single
78    /// gentle wave.
79    pub waves: Vec<WaterWave>,
80    /// Linear-space RGB colour of deep water.
81    pub deep_colour: [f32; 3],
82    /// Linear-space RGB colour of shallow water near the shore.
83    pub shallow_colour: [f32; 3],
84    /// Depth over which the colour blends from shallow to deep, in metres.
85    pub depth_falloff_metres: f32,
86    /// Width of the shoreline foam band, in metres.
87    pub foam_width_metres: f32,
88    /// Strength of the shoreline foam, in [0, 1].
89    pub foam_intensity: f32,
90    /// Sharpness of the grazing-angle reflection. Higher confines reflections to
91    /// steeper viewing angles.
92    pub fresnel_power: f32,
93    /// Surface roughness in [0, 1]. Higher gives blurrier reflections.
94    pub roughness: f32,
95    /// How strongly the surface bends the view of what's beneath it.
96    pub refraction_strength: f32,
97    /// When false the surface is skipped each frame.
98    pub visible: bool,
99}
100
101impl Default for WaterSurface {
102    fn default() -> Self {
103        Self {
104            asset_id: AssetId::default(),
105            centre: [0.0, 0.0, 0.0],
106            extent: [10.0, 10.0],
107            subdivisions: 64,
108            waves: vec![WaterWave::default()],
109            deep_colour: [0.02, 0.05, 0.15],
110            shallow_colour: [0.20, 0.50, 0.55],
111            depth_falloff_metres: 4.0,
112            foam_width_metres: 0.30,
113            foam_intensity: 0.8,
114            fresnel_power: 5.0,
115            roughness: 0.05,
116            refraction_strength: 0.15,
117            visible: true,
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn a_blank_wave_travels_along_positive_x() {
128        let w = WaterWave::default();
129        assert_eq!(w.amplitude, 0.15);
130        assert_eq!(w.wavelength, 4.0);
131        assert_eq!(w.speed, 1.0);
132        assert_eq!(w.direction, [1.0, 0.0]);
133        assert_eq!(w.steepness, 0.4);
134    }
135
136    #[test]
137    fn a_blank_surface_already_has_one_wave_so_it_is_not_a_flat_plane() {
138        let s = WaterSurface::default();
139        assert_eq!(s.waves.len(), 1);
140        assert_eq!(s.waves[0].amplitude, WaterWave::default().amplitude);
141        assert_eq!(s.extent, [10.0, 10.0]);
142        assert_eq!(s.subdivisions, 64);
143        // Deep water is darker and bluer than shallow: the depth gradient is
144        // what reads as water rather than a tinted mirror.
145        assert!(s.deep_colour[2] > s.deep_colour[0]);
146        assert!(s.shallow_colour[1] > s.deep_colour[1]);
147        assert_eq!(s.depth_falloff_metres, 4.0);
148        assert_eq!(s.foam_width_metres, 0.3);
149        assert_eq!(s.foam_intensity, 0.8);
150        assert_eq!(s.fresnel_power, 5.0);
151        assert_eq!(s.roughness, 0.05);
152        assert_eq!(s.refraction_strength, 0.15);
153        assert!(s.visible);
154    }
155
156    #[test]
157    fn a_multi_wave_surface_parses_and_round_trips_through_postcard() {
158        let s: WaterSurface = serde_json::from_str(
159            r#"{"centre":[0,0.2,-5],"extent":[40,25],"subdivisions":128,
160                "waves":[{"amplitude":0.4,"wavelength":12,"direction":[0.7,0.7]},
161                         {"amplitude":0.05,"wavelength":1.5,"speed":2.5,"steepness":0.1}],
162                "deep_colour":[0,0.02,0.1],"shallow_colour":[0.1,0.4,0.45],
163                "depth_falloff_metres":8,"foam_width_metres":0.6,"foam_intensity":1.2,
164                "fresnel_power":4,"roughness":0.02,"refraction_strength":0.3,
165                "visible":false}"#,
166        )
167        .unwrap();
168        assert_eq!(s.waves.len(), 2);
169        // A wave that mentions only some fields keeps the wave defaults.
170        assert_eq!(s.waves[0].speed, 1.0);
171        assert_eq!(s.waves[0].steepness, 0.4);
172        assert_eq!(s.waves[1].direction, [1.0, 0.0]);
173        assert!(!s.visible);
174
175        let bytes = postcard::to_allocvec(&s).unwrap();
176        let back: WaterSurface = postcard::from_bytes(&bytes).unwrap();
177        assert_eq!(back.centre, [0.0, 0.2, -5.0]);
178        assert_eq!(back.extent, [40.0, 25.0]);
179        assert_eq!(back.subdivisions, 128);
180        assert_eq!(back.waves[1].speed, 2.5);
181        assert_eq!(back.depth_falloff_metres, 8.0);
182        assert_eq!(back.foam_intensity, 1.2);
183        assert_eq!(back.refraction_strength, 0.3);
184        assert_eq!(back.asset_id, AssetId::default());
185    }
186}