all_is_cubes_content/
landscape.rs

1#![allow(
2    clippy::module_name_repetitions,
3    reason = "false positive; TODO: remove after Rust 1.84 is released"
4)]
5
6use alloc::boxed::Box;
7use core::array;
8use core::fmt;
9
10use exhaust::Exhaust;
11use rand::{Rng as _, SeedableRng as _};
12
13use all_is_cubes::arcstr;
14use all_is_cubes::block::{
15    Block, BlockAttributes, BlockCollision, Primitive,
16    Resolution::{self, R16},
17    AIR,
18};
19use all_is_cubes::linking::{BlockModule, BlockProvider, DefaultProvision, GenError, InGenError};
20use all_is_cubes::math::{zo32, Cube, FreeCoordinate, GridAab, GridCoordinate, GridVector, Rgb};
21use all_is_cubes::space::Sky;
22use all_is_cubes::space::{SetCubeError, Space};
23use all_is_cubes::universe::UniverseTransaction;
24use all_is_cubes::util::YieldProgress;
25
26use crate::alg::{array_of_noise, scale_color, voronoi_pattern, NoiseFnExt};
27use crate::{palette, tree};
28
29/// Names for blocks assigned specific roles in generating outdoor landscapes.
30///
31/// TODO: This is probably too specific to be useful in the long term; call it a
32/// placeholder.
33#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Exhaust)]
34#[non_exhaustive]
35#[allow(missing_docs)]
36pub enum LandscapeBlocks {
37    Grass,
38    GrassBlades {
39        height: GrassHeight,
40    },
41    Dirt,
42    Stone,
43    /// Half a tree part; composite it with another one to make a log/branch.
44    Log(tree::TreeGrowth),
45    Leaves(tree::TreeGrowth),
46}
47
48#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Exhaust, strum::IntoStaticStr)]
49#[non_exhaustive]
50#[allow(missing_docs)]
51#[repr(u8)]
52pub enum GrassHeight {
53    H1 = 1,
54    H2 = 2,
55    H3 = 3,
56    H4 = 4,
57    H5 = 5,
58    H6 = 6,
59    H7 = 7,
60    H8 = 8,
61}
62impl GrassHeight {
63    fn from_int(i: u8) -> Option<GrassHeight> {
64        match i {
65            0 => None,
66            1 => Some(Self::H1),
67            2 => Some(Self::H2),
68            3 => Some(Self::H3),
69            4 => Some(Self::H4),
70            5 => Some(Self::H5),
71            6 => Some(Self::H6),
72            7 => Some(Self::H7),
73            _ => Some(Self::H8), // clamped high
74        }
75    }
76}
77
78impl fmt::Display for LandscapeBlocks {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            LandscapeBlocks::Grass => write!(f, "grass"),
82            &LandscapeBlocks::GrassBlades { height } => {
83                write!(f, "grass-blades/{}", height as u8)
84            }
85            LandscapeBlocks::Dirt => write!(f, "dirt"),
86            LandscapeBlocks::Stone => write!(f, "stone"),
87            LandscapeBlocks::Log(growth) => write!(f, "log/{growth}"),
88            LandscapeBlocks::Leaves(growth) => write!(f, "leaves/{growth}"),
89        }
90    }
91}
92
93impl BlockModule for LandscapeBlocks {
94    fn namespace() -> &'static str {
95        "all-is-cubes/landscape"
96    }
97}
98
99/// Provides a bland instance of [`LandscapeBlocks`] with single color blocks.
100impl DefaultProvision<Block> for LandscapeBlocks {
101    fn module_default(self) -> Block {
102        fn color_and_name(color: Rgb, name: &'static str) -> Block {
103            Block::builder()
104                .display_name(name)
105                .color(color.with_alpha_one())
106                .build()
107        }
108
109        fn blades() -> Block {
110            Block::builder()
111                .display_name("Grass Blades")
112                .color(palette::GRASS.with_alpha(zo32(0.1)))
113                .collision(BlockCollision::None)
114                .build()
115        }
116
117        use LandscapeBlocks::*;
118        match self {
119            Grass => color_and_name(palette::GRASS, "Grass"),
120            GrassBlades { height: _ } => blades(),
121            Dirt => color_and_name(palette::DIRT, "Dirt"),
122            Stone => color_and_name(palette::STONE, "Stone"),
123            Log(g) => color_and_name(
124                palette::TREE_BARK * (0.8 + (8. - g.radius() as f32) * 0.1),
125                "Wood",
126            ),
127
128            Leaves(_g) => color_and_name(palette::TREE_LEAVES, "Leaves"),
129        }
130    }
131}
132
133/// Construct blocks for [`LandscapeBlocks`] with some detail and add block definitions to the universe.
134///
135/// This is an async function for the sake of cancellation and optional cooperative
136/// multitasking. It may be blocked on from a synchronous context.
137pub async fn install_landscape_blocks(
138    txn: &mut UniverseTransaction,
139    resolution: Resolution,
140    progress: YieldProgress,
141) -> Result<(), GenError> {
142    use LandscapeBlocks::*;
143    let colors = BlockProvider::<LandscapeBlocks>::default();
144    let rng = &mut rand_xoshiro::Xoshiro256Plus::seed_from_u64(123890483921741);
145
146    let mut grass_blade_atom = colors[GrassBlades {
147        height: GrassHeight::H4,
148    }]
149    .clone();
150    // TODO: easier way to do this?
151    if let Primitive::Atom(atom) = grass_blade_atom.primitive_mut() {
152        atom.color = atom.color.to_rgb().with_alpha_one();
153        atom.collision = BlockCollision::None;
154    }
155
156    let blade_color_noise = {
157        let blade_color_noise_v = noise::Value::new(0x2e240365);
158        move |cube: Cube| blade_color_noise_v.at_grid(cube.lower_bounds()) * 0.12 + 1.0
159    };
160    let overhang_noise = array_of_noise(resolution, &noise::Value::new(0), |value| {
161        value * 2.5 + f64::from(resolution) * 0.75
162    });
163    let blade_noise = array_of_noise(
164        // TODO: not very well sized for the job — we just want a different chunk for each height
165        resolution.double().unwrap(),
166        &noise::ScalePoint::new(noise::OpenSimplex::new(0x7af8c181)).set_y_scale(0.1),
167        |value| value * (f64::from(resolution) * 1.7) + (f64::from(resolution) * -0.4),
168    );
169
170    // boxed to avoid the async fn future being huge
171    let stone_points: Box<[_; 240]> = Box::new(array::from_fn(|_| {
172        (
173            Cube::ORIGIN.aab().random_point(rng),
174            scale_color(colors[Stone].clone(), rng.gen_range(0.9..1.1), 0.02),
175        )
176    }));
177    let stone_pattern = voronoi_pattern(resolution, true, &*stone_points);
178
179    // TODO: give dirt a palette of varying hue and saturation
180    let dirt_points: Box<[_; 1024]> = Box::new(array::from_fn(|_| {
181        (
182            Cube::ORIGIN.aab().random_point(rng),
183            scale_color(colors[Dirt].clone(), rng.gen_range(0.9..1.1), 0.02),
184        )
185    }));
186    let dirt_pattern = voronoi_pattern(resolution, true, &*dirt_points);
187
188    // TODO: needs a tiling and abruptly-changing pattern -- perhaps a coordinate-streched voronoi noise instead
189    let bark_noise = {
190        let noise = noise::ScalePoint::new(noise::Value::new(0x28711937)).set_y_scale(1. / 4.);
191        move |cube: Cube| noise.at_grid(cube.lower_bounds()) * 0.4 + 0.7
192    };
193
194    let attributes_from = |block: &Block| -> Result<BlockAttributes, InGenError> {
195        Ok(block
196            .evaluate()
197            .map_err(InGenError::other)?
198            .attributes()
199            .clone())
200    };
201
202    BlockProvider::<LandscapeBlocks>::new(progress, |key| {
203        let grass_blades = |txn, height: GrassHeight| -> Result<Block, InGenError> {
204            let height_index = height as GridCoordinate - 1;
205            // give each grass variant a different portion of the noise array
206            let noise_section = GridVector::new(
207                height_index.rem_euclid(2),
208                height_index.div_euclid(2).rem_euclid(2),
209                height_index.div_euclid(4).rem_euclid(2),
210            ) * GridCoordinate::from(resolution);
211
212            // Increase the brightness of the blade color to compensate for the way the
213            // voxel blade shape is darkened by its own opacity.
214            // This is essentially “baked ambient occlusion” but backwards:
215            // faking the highlights that the algorithm does not comprehend.
216            // TODO: Ideally this would be handled by some kind of lighting hints instead.
217            let ao_fudge = 1.0 + f64::from(height as u8) * 0.15;
218
219            Ok(Block::builder()
220                .attributes(attributes_from(&grass_blade_atom)?)
221                .display_name(arcstr::format!("Grass Blades {}", height as u8))
222                .voxels_fn(resolution, |cube| {
223                    let mut cube_for_lookup = cube;
224                    cube_for_lookup.y = 0;
225                    cube_for_lookup += noise_section;
226                    if f64::from(cube.y - height_index) < blade_noise[cube_for_lookup] {
227                        scale_color(
228                            grass_blade_atom.clone(),
229                            blade_color_noise(cube) * ao_fudge,
230                            0.02,
231                        )
232                    } else {
233                        AIR
234                    }
235                })?
236                .build_txn(txn))
237        };
238
239        Ok(match key {
240            Stone => Block::builder()
241                .attributes(attributes_from(&colors[Stone])?)
242                .voxels_fn(resolution, &stone_pattern)?
243                .build_txn(txn),
244
245            Grass => Block::builder()
246                .attributes(attributes_from(&colors[Grass])?)
247                .voxels_fn(resolution, |cube| {
248                    if f64::from(cube.y) >= overhang_noise[cube] {
249                        scale_color(colors[Grass].clone(), blade_color_noise(cube), 0.02)
250                    } else {
251                        dirt_pattern(cube).clone()
252                    }
253                })?
254                .build_txn(txn),
255
256            GrassBlades { height } => grass_blades(txn, height)?,
257
258            Dirt => Block::builder()
259                .attributes(attributes_from(&colors[Dirt])?)
260                .voxels_fn(resolution, &dirt_pattern)?
261                .build_txn(txn),
262
263            key @ Log(growth) => {
264                let resolution = R16;
265                let mid = GridCoordinate::from(resolution) / 2;
266                let radius = growth as GridCoordinate;
267                let trunk_box = GridAab::from_lower_upper(
268                    [mid - radius, 0, mid - radius],
269                    [mid + radius, mid + radius, mid + radius],
270                );
271                let color_block = &colors[key];
272                Block::builder()
273                    .attributes(attributes_from(color_block)?)
274                    .voxels_fn(resolution, |cube| {
275                        if trunk_box.contains_cube(cube) {
276                            // TODO: separate bark from inner wood
277                            scale_color(color_block.clone(), bark_noise(cube), 0.05)
278                        } else {
279                            AIR
280                        }
281                    })?
282                    .build_txn(txn)
283            }
284
285            key @ Leaves(growth) => Block::builder()
286                .attributes(attributes_from(&colors[key])?)
287                .voxels_fn(resolution, |cube| {
288                    // Distance this cube is from the center.
289                    // TODO: This is the same computation as done by square_radius() but
290                    // not with the same output. Can we share some logic there?
291                    // Or add a helpful method on GridAab?
292                    let radius_vec =
293                        cube.map(|c| (c * 2 + 1 - GridCoordinate::from(resolution)).abs() / 2 + 1);
294                    let radius = radius_vec
295                        .x
296                        .abs()
297                        .max(radius_vec.y.abs())
298                        .max(radius_vec.z.abs());
299
300                    let signed_distance_from_edge = radius - growth.radius();
301                    let unit_scale_distance =
302                        f64::from(signed_distance_from_edge) / f64::from(growth.radius());
303
304                    if unit_scale_distance <= 1.0
305                        && !rng.gen_bool(
306                            ((unit_scale_distance * 4.0).powi(2) / 2.0 + 0.5).clamp(0., 1.),
307                        )
308                    {
309                        &colors[key]
310                    } else {
311                        &AIR
312                    }
313                })?
314                .build_txn(txn),
315        })
316    })
317    .await?
318    .install(txn)?;
319    Ok(())
320}
321
322/// Generate a landscape of grass-on-top-of-rock with some bumps to it.
323/// Replaces all blocks in the specified region except for those intended to be “air”.
324///
325/// ```
326/// use all_is_cubes::space::Space;
327/// use all_is_cubes::linking::BlockProvider;
328/// use all_is_cubes_content::{LandscapeBlocks, wavy_landscape};
329///
330/// let mut space = Space::empty_positive(10, 10, 10);
331/// wavy_landscape(
332///     space.bounds(),
333///     &mut space,
334///     &BlockProvider::<LandscapeBlocks>::default(),
335///     1.0,
336/// ).unwrap();
337/// # // TODO: It didn't panic, but how about some assertions?
338/// ```
339pub fn wavy_landscape(
340    region: GridAab,
341    space: &mut Space,
342    blocks: &BlockProvider<LandscapeBlocks>,
343    max_slope: FreeCoordinate,
344) -> Result<(), SetCubeError> {
345    // TODO: justify this constant (came from cubes v1 code).
346    let slope_scaled = max_slope / 0.904087;
347    let middle_y = (region.lower_bounds().y + region.upper_bounds().y) / 2;
348
349    let grass_at = grass_placement_function(0x21b5cc6b);
350
351    for x in region.x_range() {
352        for z in region.z_range() {
353            let fx = FreeCoordinate::from(x);
354            let fz = FreeCoordinate::from(z);
355            let terrain_variation = slope_scaled
356                * (((fx / 8.0).sin() + (fz / 8.0).sin()) * 1.0
357                    + ((fx / 14.0).sin() + (fz / 14.0).sin()) * 3.0
358                    + ((fx / 2.0).sin() + (fz / 2.0).sin()) * 0.6);
359            let surface_y = middle_y + (terrain_variation as GridCoordinate);
360            for y in region.y_range() {
361                let altitude = y - surface_y;
362                use LandscapeBlocks::*;
363                let cube = Cube::new(x, y, z);
364                let block: &Block = if altitude > 1 {
365                    continue;
366                } else if altitude == 1 {
367                    if let Some(height) = grass_at(cube) {
368                        // TODO: add randomized rotation like the city grass has
369                        &blocks[GrassBlades { height }]
370                    } else {
371                        &AIR
372                    }
373                } else if altitude == 0 {
374                    &blocks[Grass]
375                } else if altitude == -1 {
376                    &blocks[Dirt]
377                } else {
378                    &blocks[Stone]
379                };
380                space.set(cube, block)?;
381                // TODO: Add various decorations on the ground. And trees.
382            }
383        }
384    }
385    Ok(())
386}
387
388pub(crate) fn grass_placement_function(seed: u32) -> impl Fn(Cube) -> Option<GrassHeight> {
389    let grass_noise = noise::ScalePoint::new(
390        noise::ScaleBias::new(noise::OpenSimplex::new(seed))
391            .set_bias(1.0) // grass rather than nongrass
392            .set_scale(15.0), // height variation
393    )
394    .set_scale(0.25);
395
396    move |cube| GrassHeight::from_int(grass_noise.at_cube(cube) as u8)
397}
398
399/// Sky whose lower half pretends to be a grassy plane.
400pub(crate) fn sky_with_grass(sky_color: Rgb) -> Sky {
401    let ground = palette::GRASS.with_alpha_one().reflect(sky_color);
402    Sky::Octants([
403        ground, ground, sky_color, sky_color, //
404        ground, ground, sky_color, sky_color, //
405    ])
406}