bevy_sculpter/
lib.rs

1//! # bevy-sculpter
2//!
3//! SDF-based voxel sculpting and Surface Nets meshing for Bevy.
4//!
5//! This crate provides tools for creating and manipulating volumetric data using
6//! signed distance fields (SDFs), with automatic mesh generation via the Surface Nets
7//! algorithm.
8//!
9//! ## Quick Start
10//!
11//! ```no_run
12//! use bevy::prelude::*;
13//! use bevy_sculpter::prelude::*;
14//! use chunky_bevy::prelude::*;
15//!
16//! fn main() {
17//!     App::new()
18//!         .add_plugins(DefaultPlugins)
19//!         .add_plugins(ChunkyPlugin::default())
20//!         .add_plugins(SurfaceNetsPlugin::default())
21//!         .insert_resource(DensityFieldMeshSize(vec3(10., 10., 10.)))
22//!         .add_systems(Startup, setup)
23//!         .run();
24//! }
25//!
26//! fn setup(mut commands: Commands) {
27//!     let mut field = DensityField::new();
28//!     bevy_sculpter::helpers::fill_centered_sphere(&mut field, 12.0);
29//!     
30//!     commands.spawn((
31//!         Chunk,
32//!         ChunkPos(ivec3(0, 0, 0)),
33//!         field,
34//!         GenerateMesh,
35//!     ));
36//! }
37//! ```
38//!
39//! ## Features
40//!
41//! - **[`DensityField`]**: SDF-based volumetric storage with raycasting and nearest-point queries
42//! - **[`SurfaceNetsPlugin`]**: Automatic mesh generation with seamless chunk boundaries
43//! - **[`helpers`]**: Sculpting brushes (smooth, hard, blur, flatten)
44//! - **[`neighbor`]**: Generic neighbor data structures for seamless chunk boundaries
45//!
46//! ## Stability
47//!
48//! This crate is under active development. Breaking changes may occur between minor versions
49//! until 1.0 is released.
50
51// Surface Nets implementation inspired by fast-surface-nets-rs
52// https://github.com/bonsairobo/fast-surface-nets-rs
53// Original work Copyright 2021 bonsairobo, dual-licensed MIT/Apache-2.0
54
55#![warn(missing_docs)]
56
57use bevy::prelude::*;
58use chunky_bevy::ChunkyPlugin;
59pub use chunky_bevy::prelude::{ChunkManager, ChunkPos};
60
61pub use crate::{
62    mesher::DensityFieldMeshSize,
63    neighbor::{NeighborFace, NeighborSlice},
64    prelude::{DensityField, GenerateMesh, NeighborDensityFields},
65};
66
67/// Density field storage and SDF operations.
68pub mod density_field;
69pub mod field;
70/// Sculpting brush functions for modifying density fields.
71pub mod helpers;
72/// Surface Nets mesh generation.
73pub mod mesher;
74/// Neighbor chunk data for seamless boundaries.
75pub mod neighbor;
76
77/// Common imports for working with bevy-sculpter.
78pub mod prelude {
79    pub use crate::{
80        DENSITY_FIELD_SIZE,
81        SurfaceNetsPlugin,
82        density_field::{DensityField, GenerateMesh},
83        field::Field,
84        mesher::DensityFieldMeshSize,
85        // Export generic neighbor types for reuse
86        neighbor::{
87            DensitySlice, NEIGHBOR_DEPTH, NeighborDensityFields, NeighborFace, NeighborFields,
88            NeighborSlice,
89        },
90    };
91}
92
93/// Size of the density field grid per chunk (32×32×32 voxels).
94pub const DENSITY_FIELD_SIZE: UVec3 = uvec3(32, 32, 32);
95
96/// Total number of voxels per chunk.
97pub const FIELD_VOLUME: usize =
98    (DENSITY_FIELD_SIZE.x * DENSITY_FIELD_SIZE.y * DENSITY_FIELD_SIZE.z) as usize;
99
100/// Sentinel value indicating no vertex exists at a position.
101pub const NULL_VERTEX: u32 = u32::MAX;
102
103/// Plugin that enables automatic Surface Nets mesh generation for chunks with density fields.
104///
105/// When added to your app, this plugin will automatically generate and update meshes for any
106/// entity that has both a [`DensityField`] and [`GenerateMesh`] components.
107///
108/// # Auto-marking
109///
110/// By default (with the `auto-mesh` feature enabled), chunks are automatically marked for
111/// meshing when their [`DensityField`] changes. Disable this feature for manual control:
112///
113/// ```toml
114/// bevy-sculpter = { version = "...", default-features = false }
115/// ```
116///
117/// # Example
118///
119/// ```no_run
120/// use bevy::prelude::*;
121/// use bevy_sculpter::prelude::*;
122/// use chunky_bevy::prelude::*;
123///
124/// App::new()
125///     .add_plugins(DefaultPlugins)
126///     .add_plugins(ChunkyPlugin::default())
127///     .add_plugins(SurfaceNetsPlugin)
128///     .insert_resource(DensityFieldMeshSize(vec3(10., 10., 10.)));
129/// ```
130pub struct SurfaceNetsPlugin;
131
132impl Plugin for SurfaceNetsPlugin {
133    fn build(&self, app: &mut App) {
134        app.add_plugins(ChunkyPlugin::default())
135            .init_resource::<DensityFieldMeshSize>();
136
137        #[cfg(feature = "auto-mesh")]
138        app.add_systems(Update, auto_mark_generate);
139
140        app.add_systems(Update, (gather_neighbor_fields, process_chunks).chain());
141    }
142}
143
144/// Auto-mark chunks for mesh generation when their density field changes
145#[cfg(feature = "auto-mesh")]
146fn auto_mark_generate(
147    mut commands: Commands,
148    changed: Query<Entity, (Changed<DensityField>, Without<GenerateMesh>)>,
149) {
150    for entity in changed.iter() {
151        commands.entity(entity).insert(GenerateMesh);
152    }
153}
154
155/// Gather neighbor density slices for chunks pending mesh generation
156fn gather_neighbor_fields(
157    mut commands: Commands,
158    pending_chunks: Query<(Entity, &ChunkPos), (With<GenerateMesh>, With<DensityField>)>,
159    all_fields: Query<&DensityField>,
160    chunk_manager: Res<ChunkManager>,
161) {
162    for (entity, chunk_pos) in pending_chunks.iter() {
163        // Use the new gather method - much cleaner!
164        let neighbors = NeighborDensityFields::gather(|face| {
165            let neighbor_pos = chunk_pos.0 + face.offset();
166            chunk_manager
167                .get_chunk(&neighbor_pos)
168                .and_then(|e| all_fields.get(e).ok())
169        });
170
171        commands.entity(entity).insert(neighbors);
172    }
173}
174
175/// Process chunks pending mesh generation
176fn process_chunks(
177    mut commands: Commands,
178    mut meshes: ResMut<Assets<Mesh>>,
179    mut materials: ResMut<Assets<StandardMaterial>>,
180    pending_chunks: Query<
181        (Entity, &DensityField, Option<&NeighborDensityFields>),
182        With<GenerateMesh>,
183    >,
184    mesh_size: Res<DensityFieldMeshSize>,
185    existing_meshes: Query<&Mesh3d>,
186) {
187    for (entity, field, neighbors) in pending_chunks.iter() {
188        let neighbors = neighbors.cloned().unwrap_or_default();
189
190        if let Some(mesh) = mesher::generate_mesh_cpu(field, &neighbors, mesh_size.0) {
191            let mesh_handle = meshes.add(mesh);
192
193            if existing_meshes.get(entity).is_ok() {
194                commands.entity(entity).insert(Mesh3d(mesh_handle));
195            } else {
196                commands.entity(entity).insert((
197                    Mesh3d(mesh_handle),
198                    MeshMaterial3d(materials.add(StandardMaterial {
199                        base_color: Color::srgb(0.5, 0.7, 0.5),
200                        perceptual_roughness: 0.8,
201                        ..default()
202                    })),
203                ));
204            }
205        }
206
207        commands.entity(entity).remove::<GenerateMesh>();
208    }
209}