use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use bevy_app::{App, Plugin, Update};
use bevy_asset::{Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_mesh::Mesh;
use bevy_platform::time::Instant;
use bevy_tasks::{AsyncComputeTaskPool, Task, block_on, futures_lite::future};
use tracing::error;
use isomesh::chunk::dirty::{DirtySet, EditReport, mark_edit, mark_edit_subgrid};
use isomesh::chunk::{ChunkId, ChunkLayout};
use isomesh::{RuntimeShape3, Sdf};
use crate::MeshBuilder;
pub trait VolumeField: Sdf<Scalar = f32> + Send + Sync + 'static {}
impl<T> VolumeField for T where T: Sdf<Scalar = f32> + Send + Sync + 'static {}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Extractor {
#[default]
MarchingCubes,
SurfaceNets,
DualContouring,
Subgrid {
samples: u32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChunkSeams {
Closed,
Gapped,
Unverified,
}
impl Extractor {
#[must_use]
pub fn chunk_seams(self) -> ChunkSeams {
match self {
Self::MarchingCubes => ChunkSeams::Closed,
Self::SurfaceNets | Self::DualContouring => ChunkSeams::Gapped,
Self::Subgrid { .. } => ChunkSeams::Closed,
}
}
}
#[derive(Component, Clone)]
pub struct VoxelVolume {
pub layout: ChunkLayout<f32>,
pub field: Arc<dyn VolumeField>,
pub extractor: Extractor,
}
impl VoxelVolume {
pub fn new(layout: ChunkLayout<f32>, field: impl VolumeField) -> Self {
Self {
layout,
field: Arc::new(field),
extractor: Extractor::default(),
}
}
#[must_use]
pub fn with_extractor(mut self, extractor: Extractor) -> Self {
self.extractor = extractor;
self
}
pub fn replace_field(&mut self, field: impl VolumeField) -> Arc<dyn VolumeField> {
std::mem::replace(&mut self.field, Arc::new(field))
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct VoxelChunk {
pub id: ChunkId,
pub volume: Entity,
}
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct NeedsRemesh;
#[derive(Component, Clone, Debug)]
pub struct ChunkMesh(pub Handle<Mesh>);
#[derive(Component)]
struct MeshingTask(Task<isomesh::Result<MeshBuilder>>);
#[derive(Resource, Clone, Copy, Debug)]
pub struct MeshBudget {
pub per_frame: Duration,
pub max_in_flight: usize,
}
impl Default for MeshBudget {
fn default() -> Self {
Self {
per_frame: Duration::from_micros(4_000),
max_in_flight: 2 * bevy_tasks::available_parallelism(),
}
}
}
#[derive(Resource, Clone, Copy, Debug, Default)]
pub struct MeshStats {
pub spawned: usize,
pub applied: usize,
pub in_flight: usize,
pub waiting: usize,
}
type WaitingChunks<'w, 's> =
Query<'w, 's, (Entity, &'static VoxelChunk), (With<NeedsRemesh>, Without<MeshingTask>)>;
#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IsomeshSystems;
#[derive(Clone, Copy, Debug, Default)]
pub struct IsomeshPlugin;
impl Plugin for IsomeshPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<bevy_mesh::MeshPlugin>() {
assert!(
app.is_plugin_added::<bevy_asset::AssetPlugin>(),
"IsomeshPlugin stores meshes in `Assets<Mesh>`, which needs \
`AssetPlugin`: add `AssetPlugin` (or `DefaultPlugins`) before \
`IsomeshPlugin`"
);
app.add_plugins(bevy_mesh::MeshPlugin);
}
app.init_resource::<MeshBudget>()
.init_resource::<MeshStats>()
.add_systems(
Update,
(spawn_meshing_tasks, apply_finished_meshes)
.chain()
.in_set(IsomeshSystems),
);
}
}
pub fn mark_changed_chunks(
commands: &mut Commands,
volume_entity: Entity,
volume: &VoxelVolume,
chunks: &Query<(Entity, &VoxelChunk)>,
before: &dyn VolumeField,
after: &dyn VolumeField,
) -> isomesh::Result<EditReport> {
let (by_id, bounds) = volume_chunks(volume_entity, volume, chunks);
let Some(region) = bounds else {
return Ok(EditReport::default());
};
mark_region(commands, volume, &by_id, before, after, region)
}
pub fn mark_changed_chunks_in(
commands: &mut Commands,
volume_entity: Entity,
volume: &VoxelVolume,
chunks: &Query<(Entity, &VoxelChunk)>,
before: &dyn VolumeField,
after: &dyn VolumeField,
region: ([i64; 3], [i64; 3]),
) -> isomesh::Result<EditReport> {
let (by_id, _) = volume_chunks(volume_entity, volume, chunks);
mark_region(commands, volume, &by_id, before, after, region)
}
type CellBox = ([i64; 3], [i64; 3]);
type ChunksById = HashMap<ChunkId, Entity>;
fn volume_chunks(
volume_entity: Entity,
volume: &VoxelVolume,
chunks: &Query<(Entity, &VoxelChunk)>,
) -> (ChunksById, Option<CellBox>) {
let n = i64::from(volume.layout.cells());
let mut by_id = ChunksById::new();
let mut bounds: Option<CellBox> = None;
for (entity, chunk) in chunks {
if chunk.volume != volume_entity {
continue;
}
by_id.insert(chunk.id, entity);
let lo = volume.layout.base_sample(chunk.id);
let hi = [lo[0] + n - 1, lo[1] + n - 1, lo[2] + n - 1];
bounds = Some(match bounds {
None => (lo, hi),
Some((min, max)) => (
[lo[0].min(min[0]), lo[1].min(min[1]), lo[2].min(min[2])],
[hi[0].max(max[0]), hi[1].max(max[1]), hi[2].max(max[2])],
),
});
}
(by_id, bounds)
}
fn mark_region(
commands: &mut Commands,
volume: &VoxelVolume,
by_id: &ChunksById,
before: &dyn VolumeField,
after: &dyn VolumeField,
(min_cell, max_cell): CellBox,
) -> isomesh::Result<EditReport> {
let mut dirty = DirtySet::new();
let report = match volume.extractor {
Extractor::Subgrid { samples } => mark_edit_subgrid(
&volume.layout,
&before,
&after,
min_cell,
max_cell,
samples,
&mut dirty,
)?,
Extractor::MarchingCubes | Extractor::SurfaceNets | Extractor::DualContouring => mark_edit(
&volume.layout,
&before,
&after,
min_cell,
max_cell,
&mut dirty,
)?,
};
for id in dirty.iter() {
if let Some(&entity) = by_id.get(&id) {
commands.entity(entity).try_insert(NeedsRemesh);
}
}
Ok(report)
}
fn spawn_meshing_tasks(
mut commands: Commands,
budget: Res<MeshBudget>,
mut stats: ResMut<MeshStats>,
volumes: Query<&VoxelVolume>,
waiting: WaitingChunks,
in_flight: Query<(), With<MeshingTask>>,
) {
let pool = AsyncComputeTaskPool::get();
let mut running = in_flight.iter().count();
stats.spawned = 0;
let mut pending = 0usize;
for (entity, chunk) in &waiting {
if running >= budget.max_in_flight {
pending += 1;
continue;
}
let Ok(volume) = volumes.get(chunk.volume) else {
commands.entity(entity).remove::<NeedsRemesh>();
continue;
};
let field = Arc::clone(&volume.field);
let layout = volume.layout;
let extractor = volume.extractor;
let id = chunk.id;
let task = pool.spawn(async move { extract_chunk(&*field, &layout, id, extractor) });
commands
.entity(entity)
.remove::<NeedsRemesh>()
.try_insert(MeshingTask(task));
running += 1;
stats.spawned += 1;
}
stats.in_flight = running;
stats.waiting = pending;
}
fn apply_finished_meshes(
mut commands: Commands,
budget: Res<MeshBudget>,
mut stats: ResMut<MeshStats>,
mut meshes: ResMut<Assets<Mesh>>,
mut tasks: Query<(Entity, &mut MeshingTask)>,
) {
let started = Instant::now();
stats.applied = 0;
for (entity, mut task) in &mut tasks {
let Some(result) = block_on(future::poll_once(&mut task.0)) else {
continue;
};
match result {
Ok(builder) => {
commands
.entity(entity)
.remove::<MeshingTask>()
.try_insert(ChunkMesh(meshes.add(builder.into_mesh())));
stats.applied += 1;
}
Err(e) => {
error!("meshing chunk {entity} failed: {e}");
commands.entity(entity).remove::<MeshingTask>();
}
}
if started.elapsed() >= budget.per_frame {
break;
}
}
stats.in_flight = stats.in_flight.saturating_sub(stats.applied);
}
fn extract_chunk(
field: &dyn VolumeField,
layout: &ChunkLayout<f32>,
id: ChunkId,
extractor: Extractor,
) -> isomesh::Result<MeshBuilder> {
let mut out = MeshBuilder::new();
let shape = layout.sample_shape()?;
let origin = layout.sample_origin(id);
let cell = layout.cell_size();
extract_with(extractor, field, &shape, origin, cell, &mut out)?;
Ok(out)
}
fn extract_with(
extractor: Extractor,
field: &dyn VolumeField,
shape: &RuntimeShape3,
origin: [f32; 3],
cell: f32,
out: &mut MeshBuilder,
) -> isomesh::Result<()> {
match extractor {
Extractor::MarchingCubes => isomesh::marching_cubes::MarchingCubes::<f32>::new()
.extract(&field, shape, origin, cell, out),
Extractor::SurfaceNets => isomesh::surface_nets::SurfaceNets::<f32>::new()
.extract(&field, shape, origin, cell, out),
Extractor::DualContouring => isomesh::dual_contouring::DualContouring::<f32>::new()
.extract(&field, shape, origin, cell, out),
Extractor::Subgrid { samples } => {
isomesh::subgrid::extract::SubgridMarchingTetrahedra::<f32>::new(samples)?
.extract(&field, shape, origin, cell, out)
}
}
}
#[cfg(test)]
mod tests;