Skip to main content

celestialsim/
descriptors.rs

1//! CPU → GPU packing of the shared terrain-noise parameters.
2//!
3//! Both the geometry realize and the surface-detail bake read the same 56-byte
4//! `TerrainGpu` block. The CPU splits ownership into two layer structs
5//! (`HeightGpu` geometry, `TextureGpu` albedo) so a colour-only edit can route
6//! through the graph without re-staging geometry; [`assemble`] packs them back
7//! into the single block the shaders read.
8
9/// Terrain noise parameters as the shaders read them (14 floats = 56 bytes,
10/// matching `struct TerrainGpu` in the realize/bake shaders — the CONTAINING
11/// params structs pad back up to a 16-byte multiple). Octave counts ride as
12/// floats and are cast in-shader.
13#[repr(C)]
14#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
15pub struct TerrainGpu {
16    pub frequency: f32,
17    pub height_octaves: f32,
18    pub height_amp: f32,
19    pub height_gain: f32,
20    pub height_lacunarity: f32,
21    pub ridge_tiles: f32,
22    pub ridge_octaves: f32,
23    pub ridge_gain: f32,
24    pub ridge_lacunarity: f32,
25    pub ridge_strength: f32,
26    pub water_height: f32,
27    pub height_scale: f32,
28    pub fd_eps: f32,
29    pub enabled: f32,
30}
31
32/// The height (geometry) layer's params: everything that displaces the surface
33/// and is the input to the derived albedo. Owned by the height-layer node; a
34/// change re-stages geometry. `enabled` globally toggles terrain displacement.
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct HeightGpu {
37    pub frequency: f32,
38    pub height_octaves: f32,
39    pub height_amp: f32,
40    pub height_gain: f32,
41    pub height_lacunarity: f32,
42    pub ridge_tiles: f32,
43    pub ridge_octaves: f32,
44    pub ridge_gain: f32,
45    pub ridge_lacunarity: f32,
46    pub ridge_strength: f32,
47    pub height_scale: f32,
48    pub fd_eps: f32,
49    pub enabled: f32,
50}
51
52impl Default for HeightGpu {
53    /// Defaults matching `terrain_noise_3d.slang`'s built-in constants (the
54    /// Lague-style continent/ridge composition). Used when no height-layer
55    /// resource is assigned.
56    fn default() -> Self {
57        Self {
58            frequency: 1.4,
59            height_octaves: 8.0,
60            height_amp: 0.35,
61            height_gain: 0.396,
62            height_lacunarity: 2.0,
63            ridge_tiles: 2.4,
64            ridge_octaves: 5.0,
65            ridge_gain: 0.5,
66            ridge_lacunarity: 2.0,
67            ridge_strength: 0.0552,
68            height_scale: 0.0585,
69            fd_eps: 0.0008,
70            enabled: 1.0,
71        }
72    }
73}
74
75/// The texture (albedo) layer's params. Most albedo is derived from the height
76/// field, so this is currently just the water-colouring threshold — but owning
77/// it as a separate layer lets a colour-only change route through the graph
78/// without re-staging geometry, and is where future albedo tunables land.
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub struct TextureGpu {
81    pub water_height: f32,
82}
83
84impl Default for TextureGpu {
85    /// Default sea level, matching the committed `celestial_v5.tscn` look.
86    fn default() -> Self {
87        Self { water_height: 0.549 }
88    }
89}
90
91/// Pack the two layer structs into the single 56-byte `TerrainGpu` the realize
92/// and bake shaders read. Field order/positions are unchanged — the split is
93/// CPU-side ownership only, so no shader or SPIR-V change is needed.
94pub fn assemble(h: &HeightGpu, t: &TextureGpu) -> TerrainGpu {
95    TerrainGpu {
96        frequency: h.frequency,
97        height_octaves: h.height_octaves,
98        height_amp: h.height_amp,
99        height_gain: h.height_gain,
100        height_lacunarity: h.height_lacunarity,
101        ridge_tiles: h.ridge_tiles,
102        ridge_octaves: h.ridge_octaves,
103        ridge_gain: h.ridge_gain,
104        ridge_lacunarity: h.ridge_lacunarity,
105        ridge_strength: h.ridge_strength,
106        water_height: t.water_height,
107        height_scale: h.height_scale,
108        fd_eps: h.fd_eps,
109        enabled: h.enabled,
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn assemble_maps_layers_into_terrain_gpu() {
119        // Distinct values per field so a mis-mapping is caught.
120        let h = HeightGpu {
121            frequency: 1.0,
122            height_octaves: 2.0,
123            height_amp: 3.0,
124            height_gain: 4.0,
125            height_lacunarity: 5.0,
126            ridge_tiles: 6.0,
127            ridge_octaves: 7.0,
128            ridge_gain: 8.0,
129            ridge_lacunarity: 9.0,
130            ridge_strength: 12.0,
131            height_scale: 13.0,
132            fd_eps: 14.0,
133            enabled: 1.0,
134        };
135        let t = TextureGpu { water_height: 99.0 };
136        let g = assemble(&h, &t);
137        // Geometry fields come from the height layer …
138        assert_eq!(g.frequency, 1.0);
139        assert_eq!(g.ridge_strength, 12.0);
140        assert_eq!(g.height_scale, 13.0);
141        assert_eq!(g.fd_eps, 14.0);
142        assert_eq!(g.enabled, 1.0);
143        // … the albedo field from the texture layer.
144        assert_eq!(g.water_height, 99.0);
145        // The terrain block the shaders read is 14 floats = 56 bytes.
146        assert_eq!(std::mem::size_of::<TerrainGpu>(), 56);
147    }
148
149    #[test]
150    fn layer_defaults_assemble_to_builtin_terrain() {
151        // The no-resource fallback must match the shader's built-in constants
152        // so a planet with no layer resources still looks right.
153        let g = assemble(&HeightGpu::default(), &TextureGpu::default());
154        assert_eq!(g.frequency, 1.4);
155        assert_eq!(g.height_octaves, 8.0);
156        assert_eq!(g.ridge_tiles, 2.4);
157        assert_eq!(g.ridge_octaves, 5.0);
158        assert_eq!(g.height_scale, 0.0585);
159        assert_eq!(g.water_height, 0.549);
160        assert_eq!(g.enabled, 1.0);
161    }
162}