bevy_generative 0.4.0

Procedural generation in Bevy
Documentation
//! Generate terrain
//! # Example
//! For configuration, see [`Terrain`](struct.Terrain.html)
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_generative::terrain::{TerrainBundle, TerrainPlugin};
//!
//! fn main() {
//!     App::new()
//!         .add_plugins(DefaultPlugins)
//!         .add_plugins(TerrainPlugin)
//!         .add_systems(Startup, setup)
//!         .run();
//! }
//!
//! fn setup(mut commands: Commands) {
//!     let light_bundle = (
//!        PointLight::default(),
//!        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
//!    );
//!
//!
//!    commands.spawn(light_bundle);
//!
//!    let camera_bundle = (
//!        Camera3d::default(),
//!        Projection::Perspective(PerspectiveProjection::default()),
//!        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
//!
//!    );
//!    commands.spawn(camera_bundle);
//!    commands.spawn(TerrainBundle {
//!        terrain: bevy_generative::terrain::Terrain {
//!            resolution: 4,
//!            ..default()
//!        },
//!        ..default()
//!    });
//! }
//! ```
use bevy::{
    prelude::*,
    render::{
        render_asset::RenderAssetUsages,
        render_resource::{PrimitiveTopology, TextureFormat},
    },
};
use colorgrad::LinearGradient;
use image::Pixel;
use serde::{Deserialize, Serialize};

use crate::{noise::generate_noise_map, noise::Noise, util::export_model};

/// Component for terrain configuration
#[derive(Component, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Terrain {
    /// Noise configuration for terrain
    pub noise: Noise,
    /// Size of the terrain
    pub size: [u32; 2],
    /// Resolution of terrain
    pub resolution: u32,
    /// If true, renders terrain mesh as wireframe
    pub wireframe: bool,
    /// Height values are raised to this value.
    /// Lower values result in plains, higher values result in mountains
    pub height_exponent: f32,
    /// Percentage of terrain that should appear under sea
    /// The mesh below this value will be flat
    pub sea_percent: f32,
    /// If true, exports model in glb format
    #[serde(skip)]
    pub export: bool,
}

impl Default for Terrain {
    fn default() -> Self {
        Self {
            noise: Noise::default(),
            size: [2; 2],
            resolution: 15,
            wireframe: false,
            height_exponent: 1.0,
            sea_percent: 10.0,
            export: false,
        }
    }
}

/// Render `Terrain` as a `Mesh3d`
#[derive(Bundle, Default)]
pub struct TerrainBundle {
    /// Terrain configuration
    pub terrain: Terrain,
    /// Generated mesh data
    pub mesh: (Mesh3d, MeshMaterial3d<StandardMaterial>),
}

/// Plugin to generate terrain
pub struct TerrainPlugin;

impl Plugin for TerrainPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, generate_terrain);
    }
}

fn generate_terrain(
    mut images: ResMut<Assets<Image>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut query: Query<(&mut Terrain, &mut Mesh3d, &MeshMaterial3d<StandardMaterial>)>,
) {
    for (mut terrain, mut mesh_handle, material) in &mut query {
        if let Some(material) = materials.get_mut(material) {
            *material = StandardMaterial::default();
        }
        terrain.noise.size = [
            terrain.size[0] * terrain.resolution,
            terrain.size[1] * terrain.resolution,
        ];
        let noise_values = generate_noise_map(&terrain.noise);

        let mut colors: Vec<colorgrad::Color> = Vec::with_capacity(terrain.noise.regions.len());
        let mut domain: Vec<f32> = Vec::with_capacity(terrain.noise.regions.len());
        for region in &terrain.noise.regions {
            colors.push(colorgrad::Color {
                r: f32::from(region.color[0]) / 255.0,
                g: f32::from(region.color[1]) / 255.0,
                b: f32::from(region.color[2]) / 255.0,
                a: f32::from(region.color[3]) / 255.0,
            });
            domain.push(region.position);
        }
        let grad = colorgrad::GradientBuilder::new()
            .colors(&colors)
            .domain(&domain)
            .build::<LinearGradient>()
            .unwrap_or_else(|_| {
                colorgrad::GradientBuilder::new()
                    .colors(&colors)
                    .build::<LinearGradient>()
                    .expect("Gradient generation failed")
            });

        let mut gradient_buffer = image::ImageBuffer::from_pixel(
            terrain.noise.gradient.size[0],
            terrain.noise.gradient.size[1],
            image::Rgba(terrain.noise.base_color),
        );

        for (x, _, pixel) in gradient_buffer.enumerate_pixels_mut() {
            let rgba = colorgrad::Gradient::at(
                &grad,
                (f64::from(x) * 100.0 / f64::from(terrain.noise.gradient.size[0])) as f32,
            )
            .to_rgba8();
            pixel.blend(&image::Rgba(rgba));
        }

        terrain.noise.gradient.image = images.add(
            Image::from_dynamic(
                gradient_buffer.into(),
                true,
                RenderAssetUsages::RENDER_WORLD,
            )
            .convert(TextureFormat::Rgba8UnormSrgb)
            .expect("Could not convert to Rgba8UnormSrgb"),
        );

        let vertices_count: usize =
            ((terrain.noise.size[0] + 1) * (terrain.noise.size[1] + 1)) as usize;
        let triangle_count: usize =
            (terrain.noise.size[0] * terrain.noise.size[1] * 2 * 3) as usize;

        let mut positions: Vec<[f32; 3]> = Vec::with_capacity(vertices_count);
        let mut normals: Vec<[f32; 3]> = Vec::with_capacity(vertices_count);
        let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(vertices_count);
        let mut indices: Vec<u32> = Vec::with_capacity(triangle_count);
        let mut colors: Vec<[f32; 4]> = Vec::with_capacity(vertices_count);

        let rows = terrain.size[0] * terrain.resolution;
        let cols = terrain.size[1] * terrain.resolution;
        let width = terrain.size[0] as f32 + 1.0;
        let depth = terrain.size[1] as f32 + 1.0;
        for row in 0..rows {
            for col in 0..cols {
                let row = row as f32;
                let col = col as f32;
                let noise_value = noise_values[row as usize][col as usize] as f32;
                let height_value = (0_f32.max(noise_value - terrain.sea_percent)) / 100.0;
                let x = (row / terrain.resolution as f32 - width / 2.0) + 0.5;
                let y = ((height_value * 1.2).powf(terrain.height_exponent) - 0.5) * 2.0;
                let z = (col / terrain.resolution as f32 - depth / 2.0) + 0.5;

                let color = colorgrad::Gradient::at(
                    &grad,
                    (noise_values[row as usize][col as usize]) as f32,
                );
                let color = [
                    color.r as f32,
                    color.g as f32,
                    color.b as f32,
                    color.a as f32,
                ];

                positions.push([x, y, z]);
                normals.push([0.0, 1.0, 0.0]);
                uvs.push([row, col]);
                colors.push(color);
            }
        }

        for i in 0..(rows - 1) {
            for j in 0..(cols - 1) {
                let current = i * cols + j;
                let next_row = (i + 1) * cols + j;

                // Triangle 1
                indices.push(current);
                indices.push(current + 1);
                indices.push(next_row);

                // Triangle 2
                indices.push(next_row);
                indices.push(current + 1);
                indices.push(next_row + 1);
            }
        }

        if terrain.wireframe {
            let triangle_number = indices.len() / 3;
            let cloned_indices = indices.clone();
            indices = vec![];
            for i in 0..triangle_number {
                for j in &[0, 1, 1, 2, 2, 0] {
                    indices.push(cloned_indices[i * 3 + j]);
                }
            }
        }

        let mut mesh = if terrain.wireframe {
            Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::RENDER_WORLD)
        } else {
            Mesh::new(
                PrimitiveTopology::TriangleList,
                RenderAssetUsages::RENDER_WORLD,
            )
        };
        mesh.insert_indices(bevy::render::mesh::Indices::U32(indices.clone()));
        mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions.clone());
        mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
        mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors.clone());
        mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
        *mesh_handle = Mesh3d(meshes.add(mesh));

        if terrain.export {
            export_model(&positions, indices, &colors);
            terrain.export = false;
        }
    }
}