Skip to main content

concinnity_core/components/
water_surface.rs

1// Animated water-surface schema.
2
3use crate::ecs::asset_id::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_core::components::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, and
94    /// pushes a mirrored reflection further off its line with each wave: a
95    /// near-mirror surface keeps its reflection almost still.
96    pub roughness: f32,
97    /// How strongly the surface bends the view of what's beneath it.
98    pub refraction_strength: f32,
99    /// When false the surface is skipped each frame.
100    pub visible: bool,
101}
102
103impl Default for WaterSurface {
104    fn default() -> Self {
105        Self {
106            asset_id: AssetId::default(),
107            centre: [0.0, 0.0, 0.0],
108            extent: [10.0, 10.0],
109            subdivisions: 64,
110            waves: vec![WaterWave::default()],
111            deep_colour: [0.02, 0.05, 0.15],
112            shallow_colour: [0.20, 0.50, 0.55],
113            depth_falloff_metres: 4.0,
114            foam_width_metres: 0.30,
115            foam_intensity: 0.8,
116            fresnel_power: 5.0,
117            roughness: 0.05,
118            refraction_strength: 0.15,
119            visible: true,
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn a_blank_wave_travels_along_positive_x() {
130        let w = WaterWave::default();
131        assert_eq!(w.amplitude, 0.15);
132        assert_eq!(w.wavelength, 4.0);
133        assert_eq!(w.speed, 1.0);
134        assert_eq!(w.direction, [1.0, 0.0]);
135        assert_eq!(w.steepness, 0.4);
136    }
137
138    #[test]
139    fn a_blank_surface_already_has_one_wave_so_it_is_not_a_flat_plane() {
140        let s = WaterSurface::default();
141        assert_eq!(s.waves.len(), 1);
142        assert_eq!(s.waves[0].amplitude, WaterWave::default().amplitude);
143        assert_eq!(s.extent, [10.0, 10.0]);
144        assert_eq!(s.subdivisions, 64);
145        // Deep water is darker and bluer than shallow: the depth gradient is
146        // what reads as water rather than a tinted mirror.
147        assert!(s.deep_colour[2] > s.deep_colour[0]);
148        assert!(s.shallow_colour[1] > s.deep_colour[1]);
149        assert_eq!(s.depth_falloff_metres, 4.0);
150        assert_eq!(s.foam_width_metres, 0.3);
151        assert_eq!(s.foam_intensity, 0.8);
152        assert_eq!(s.fresnel_power, 5.0);
153        assert_eq!(s.roughness, 0.05);
154        assert_eq!(s.refraction_strength, 0.15);
155        assert!(s.visible);
156    }
157
158    #[test]
159    fn a_multi_wave_surface_parses_and_round_trips_through_postcard() {
160        let s: WaterSurface = serde_json::from_str(
161            r#"{"centre":[0,0.2,-5],"extent":[40,25],"subdivisions":128,
162                "waves":[{"amplitude":0.4,"wavelength":12,"direction":[0.7,0.7]},
163                         {"amplitude":0.05,"wavelength":1.5,"speed":2.5,"steepness":0.1}],
164                "deep_colour":[0,0.02,0.1],"shallow_colour":[0.1,0.4,0.45],
165                "depth_falloff_metres":8,"foam_width_metres":0.6,"foam_intensity":1.2,
166                "fresnel_power":4,"roughness":0.02,"refraction_strength":0.3,
167                "visible":false}"#,
168        )
169        .unwrap();
170        assert_eq!(s.waves.len(), 2);
171        // A wave that mentions only some fields keeps the wave defaults.
172        assert_eq!(s.waves[0].speed, 1.0);
173        assert_eq!(s.waves[0].steepness, 0.4);
174        assert_eq!(s.waves[1].direction, [1.0, 0.0]);
175        assert!(!s.visible);
176
177        let bytes = postcard::to_allocvec(&s).unwrap();
178        let back: WaterSurface = postcard::from_bytes(&bytes).unwrap();
179        assert_eq!(back.centre, [0.0, 0.2, -5.0]);
180        assert_eq!(back.extent, [40.0, 25.0]);
181        assert_eq!(back.subdivisions, 128);
182        assert_eq!(back.waves[1].speed, 2.5);
183        assert_eq!(back.depth_falloff_metres, 8.0);
184        assert_eq!(back.foam_intensity, 1.2);
185        assert_eq!(back.refraction_strength, 0.3);
186        assert_eq!(back.asset_id, AssetId::default());
187    }
188}