bevy_cube_marcher 0.5.2

A shader-based marching cubes implementation for Bevy
Documentation
//! A shader-based marching cubes implementation for Bevy.
//!
//! To get started, add the [`MarchingCubesPlugin`] to your app.
//!
//! The plugin takes a type argument similar to a Bevy material, [`ChunkComputeShader`].
//! The [`ShaderRef`] provided by this type is a wgsl compute shader that takes a coordinate and returns the density of the chunk's mass at that point,
//! generally with positive values representing being inside the mass and negative values being in air,
//! although the threshold can be configured with [`ChunkGeneratorSettings`].
//! See [this example asset](https://git.dragonfox.dev/DragonFoxCollective/bevy_cube_marcher/src/branch/main/assets/sample.wgsl) for more details.
//!
//! The plugin also takes a type argument implementing [`GpuExtraBufferCache`], defining additional buffers to be used in the density sampler.
//! [This example](https://git.dragonfox.dev/DragonFoxCollective/bevy_cube_marcher/src/branch/main/examples/noise_with_extra_buffers.rs)
//! uses this to determine the final position of a Point Of Interest based on terrain generation.
//!
//! [`ChunkGeneratorSettings`] must be inserted.
//!
//! [`ChunkMaterial`] must be inserted.
//!
//! An entity must also be given [`ChunkLoader`] to start generating any chunks.

use std::marker::PhantomData;
use std::num::NonZero;

use bevy::platform::collections::HashMap;
use bevy::prelude::*;

#[cfg(feature = "cpu")]
pub mod cpu;
#[cfg(feature = "gpu")]
pub mod gpu;

/// [`SystemSet`] for chunk generation systems.
#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ChunkGenSystems;

#[derive(Default, Debug, Clone, Copy, Reflect)]
#[cfg_attr(feature = "gpu", derive(encase::ShaderType))]
struct Vertex {
    position: Vec3,
    _padding1: f32,
    normal: Vec3,
    _padding2: f32,
}

#[derive(Default, Debug, Clone, Copy, Reflect)]
#[cfg_attr(feature = "gpu", derive(encase::ShaderType))]
struct Triangle {
    vertex_a: u32,
    vertex_b: u32,
    vertex_c: u32,
}

/// Marker component for each chunk.
///
/// The entity this is on should also have the appropriate mesh, material, and transform.
#[derive(Component, Debug)]
pub struct Chunk<Sampler> {
    pub position: IVec3,
    _marker: std::marker::PhantomData<Sampler>,
}

/// Loads nearby chunks.
#[derive(Component, Default, Debug, Clone)]
pub struct ChunkLoader<T> {
    pub position: IVec3,
    pub loading_radius: u32,
    _marker: std::marker::PhantomData<T>,
}

impl<T> ChunkLoader<T> {
    pub fn new(loading_radius: u32) -> Self {
        Self {
            position: IVec3::ZERO,
            loading_radius,
            _marker: std::marker::PhantomData,
        }
    }
}

/// Holds the actual material chunks should be spawned with.
#[derive(Resource, Debug)]
pub struct ChunkMaterial<Sampler, Material: Asset> {
    pub material: Handle<Material>,
    _marker: std::marker::PhantomData<Sampler>,
}

impl<Sampler, Material: Asset> ChunkMaterial<Sampler, Material> {
    pub fn new(material: Handle<Material>) -> Self {
        Self {
            material,
            _marker: std::marker::PhantomData,
        }
    }
}

/// Controls for whether the generator should be running.
#[derive(Debug, PartialEq, Eq)]
pub enum ChunkGeneratorRunning {
    /// Generator will run as usual.
    Run,
    /// Generator will pause.
    Pause,
    /// Generator will stop and caches will be deleted. Loaded chunks will not be deleted!
    Stop,
    /// Generator will reset, refreshing caches. Loaded chunks will not be deleted!
    Reset,
}

/// Settings for a chunk generator. Also controls whether said generator is running.
#[derive(Resource, Debug)]
#[cfg_attr(
    feature = "gpu",
    derive(bevy::render::extract_resource::ExtractResource)
)]
pub struct ChunkGeneratorSettings<Sampler: Send + Sync + 'static> {
    pub running: ChunkGeneratorRunning,
    surface_threshold: f32, // not pub so you can't change it later
    num_voxels_per_axis: u32,
    chunk_size: f32,
    max_chunks_per_frame: usize,
    num_buffers: usize,
    bounds: Option<GenBounds>,
    _marker: std::marker::PhantomData<Sampler>,
}

impl<Sampler: Send + Sync + 'static> Clone for ChunkGeneratorSettings<Sampler> {
    fn clone(&self) -> Self {
        Self {
            running: ChunkGeneratorRunning::Run,
            surface_threshold: self.surface_threshold,
            num_voxels_per_axis: self.num_voxels_per_axis,
            chunk_size: self.chunk_size,
            max_chunks_per_frame: self.max_chunks_per_frame,
            num_buffers: self.num_buffers,
            bounds: self.bounds.clone(),
            _marker: self._marker,
        }
    }
}

#[derive(Debug, Clone)]
struct GenBounds {
    min: Vec3,
    max: Vec3,
}

impl<Sampler: Send + Sync + 'static> ChunkGeneratorSettings<Sampler> {
    pub fn new(num_voxels_per_axis: u32, chunk_size: f32) -> Self {
        Self {
            running: ChunkGeneratorRunning::Run,
            surface_threshold: 0.0,
            num_voxels_per_axis,
            chunk_size,
            max_chunks_per_frame: 1,
            num_buffers: 3,
            bounds: None,
            _marker: PhantomData,
        }
    }

    pub fn with_surface_threshold(mut self, surface_threshold: f32) -> Self {
        self.surface_threshold = surface_threshold;
        self
    }

    pub fn with_bounds(mut self, min: Vec3, max: Vec3) -> Self {
        self.bounds = Some(GenBounds { min, max });
        self
    }

    pub fn with_max_chunks_per_frame(mut self, max_chunks_per_frame: usize) -> Self {
        self.max_chunks_per_frame = max_chunks_per_frame;
        self
    }

    pub fn with_num_buffers(mut self, num_buffers: usize) -> Self {
        self.num_buffers = num_buffers;
        self
    }

    pub fn stopped(mut self) -> Self {
        self.running = ChunkGeneratorRunning::Stop;
        self
    }

    pub fn num_voxels_per_axis(&self) -> u32 {
        self.num_voxels_per_axis
    }

    pub fn num_samples_per_axis(&self) -> u32 {
        self.num_voxels_per_axis + 3 // We sample the next chunk over too for normals
    }

    pub fn max_num_vertices(&self) -> u64 {
        self.max_num_triangles() * 3
    }

    pub fn chunk_size(&self) -> f32 {
        self.chunk_size
    }

    pub fn vertices_buffer_size(&self) -> NonZero<u64> {
        (size_of::<Vertex>() as u64 * self.max_num_vertices())
            .try_into()
            .expect("zero vertices")
    }

    pub fn max_num_triangles(&self) -> u64 {
        (self.num_voxels_per_axis as u64).pow(3) * 5
    }

    pub fn triangles_buffer_size(&self) -> NonZero<u64> {
        (size_of::<Triangle>() as u64 * self.max_num_triangles())
            .try_into()
            .expect("zero triangles")
    }

    pub fn voxel_size(&self) -> f32 {
        self.chunk_size / self.num_voxels_per_axis as f32
    }

    pub fn position_to_chunk(&self, position: Vec3) -> IVec3 {
        (position / self.chunk_size).floor().as_ivec3()
    }

    pub fn chunk_to_position(&self, chunk: IVec3) -> Vec3 {
        chunk.as_vec3() * self.chunk_size
    }

    pub fn sample_to_local_position(&self, sample: UVec3) -> Vec3 {
        (sample.as_vec3() - Vec3::ONE) / self.num_voxels_per_axis as f32 * self.chunk_size
    }

    pub fn sample_to_position(&self, sample: UVec3, chunk: IVec3) -> Vec3 {
        self.chunk_to_position(chunk) + self.sample_to_local_position(sample)
    }

    pub fn voxel_to_local_position(&self, voxel: UVec3) -> Vec3 {
        voxel.as_vec3() / self.num_voxels_per_axis as f32 * self.chunk_size
    }

    pub fn voxel_to_position(&self, voxel: UVec3, chunk: IVec3) -> Vec3 {
        self.chunk_to_position(chunk) + self.voxel_to_local_position(voxel)
    }

    fn is_chunk_in_bounds(&self, chunk_position: IVec3) -> bool {
        if let Some(bounds) = &self.bounds {
            let position = self.chunk_to_position(chunk_position);
            position.x >= bounds.min.x
                && position.x <= bounds.max.x
                && position.y >= bounds.min.y
                && position.y <= bounds.max.y
                && position.z >= bounds.min.z
                && position.z <= bounds.max.z
        } else {
            true
        }
    }
}

/// Run condition for whether a chunk generator is running and listening to [`ChunkLoader`]s, regardless of whether it's actually generating anything.
pub fn is_generator_running<Sampler: Send + Sync + 'static>(
    settings: Res<ChunkGeneratorSettings<Sampler>>,
) -> bool {
    matches!(settings.running, ChunkGeneratorRunning::Run)
}

/// Holds the loading states of all the chunks.
#[derive(Resource, Debug, Clone)]
pub struct ChunkGeneratorCache<Sampler> {
    loaded_chunks: HashMap<IVec3, LoadState>,
    chunks_to_load: Vec<IVec3>,
    _marker: std::marker::PhantomData<Sampler>,
}

impl<Sampler: Send + Sync + 'static> ChunkGeneratorCache<Sampler> {
    pub fn is_chunk_marked(
        &self,
        settings: &ChunkGeneratorSettings<Sampler>,
        chunk_position: IVec3,
    ) -> bool {
        !settings.is_chunk_in_bounds(chunk_position)
            || self.loaded_chunks.contains_key(&chunk_position)
    }

    pub fn is_chunk_generated(
        &self,
        settings: &ChunkGeneratorSettings<Sampler>,
        chunk_position: IVec3,
    ) -> bool {
        !settings.is_chunk_in_bounds(chunk_position)
            || matches!(
                self.loaded_chunks.get(&chunk_position),
                Some(LoadState::Finished)
            )
    }

    pub fn is_chunk_with_position_marked(
        &self,
        settings: &ChunkGeneratorSettings<Sampler>,
        position: Vec3,
    ) -> bool {
        self.is_chunk_marked(settings, settings.position_to_chunk(position))
    }

    pub fn is_chunk_with_position_generated(
        &self,
        settings: &ChunkGeneratorSettings<Sampler>,
        position: Vec3,
    ) -> bool {
        self.is_chunk_generated(settings, settings.position_to_chunk(position))
    }
}

impl<Sampler: Send + Sync + 'static> Default for ChunkGeneratorCache<Sampler> {
    fn default() -> Self {
        Self {
            loaded_chunks: default(),
            chunks_to_load: default(),
            _marker: default(),
        }
    }
}

/// Whether a chunk is loading or has finished loading.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum LoadState {
    Loading,
    Finished,
}

fn update_chunk_loaders<Sampler: Send + Sync + 'static>(
    settings: Res<ChunkGeneratorSettings<Sampler>>,
    mut chunk_loaders: Query<
        (&mut ChunkLoader<Sampler>, &GlobalTransform),
        Changed<GlobalTransform>,
    >,
) {
    for (mut chunk_loader, transform) in chunk_loaders.iter_mut() {
        let chunk_position = (transform.translation() / settings.chunk_size)
            .floor()
            .as_ivec3();

        // Properly update change detection
        if chunk_loader.position != chunk_position {
            chunk_loader.position = chunk_position;
        }
    }
}

fn queue_chunks<Sampler: Send + Sync + 'static>(
    settings: Res<ChunkGeneratorSettings<Sampler>>,
    mut cache: ResMut<ChunkGeneratorCache<Sampler>>,
    chunk_loaders: Query<&ChunkLoader<Sampler>, Changed<ChunkLoader<Sampler>>>,
) {
    for chunk_loader in chunk_loaders.iter() {
        let mut load_order = Vec::new();
        let loading_radius = chunk_loader.loading_radius as i32;
        for x in -loading_radius..=loading_radius {
            for y in -loading_radius..=loading_radius {
                for z in -loading_radius..=loading_radius {
                    load_order.push(Vec3::new(x as f32, y as f32, z as f32));
                }
            }
        }

        load_order.sort_by(|a, b| {
            // Sort ascending so that the closest chunks are loaded first (drained from front)
            a.length_squared()
                .partial_cmp(&b.length_squared())
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        for offset in load_order {
            let chunk_position = chunk_loader.position + offset.as_ivec3();
            if !cache.is_chunk_marked(&settings, chunk_position) {
                cache
                    .loaded_chunks
                    .insert(chunk_position, LoadState::Loading);
                cache.chunks_to_load.push(chunk_position);
                trace!("Queued chunk for loading: {chunk_position:?}");
            }
        }
    }
}

#[derive(EntityEvent)]
pub struct ChunkGenerated(Entity);