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)
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//!         DensityFieldDirty,
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//!
45//! ## Stability
46//!
47//! This crate is under active development. Breaking changes may occur between minor versions
48//! until 1.0 is released.
49
50// Surface Nets implementation inspired by fast-surface-nets-rs
51// https://github.com/bonsairobo/fast-surface-nets-rs
52// Original work Copyright 2021 bonsairobo, dual-licensed MIT/Apache-2.0
53
54#![warn(missing_docs)]
55
56use bevy::prelude::*;
57pub use chunky_bevy::prelude::{ChunkManager, ChunkPos};
58
59use crate::{
60    mesher::DensityFieldMeshSize,
61    neighbor::{NeighborFace, NeighborSlice},
62    prelude::{DensityField, DensityFieldDirty, NeighborDensityFields},
63};
64
65/// Density field storage and SDF operations.
66pub mod density_field;
67/// Sculpting brush functions for modifying density fields.
68pub mod helpers;
69/// Surface Nets mesh generation.
70pub mod mesher;
71/// Neighbor chunk data for seamless boundaries.
72pub mod neighbor;
73
74/// Common imports for working with bevy-sculpter.
75pub mod prelude {
76    pub use crate::{
77        DENSITY_FIELD_SIZE, SurfaceNetsPlugin,
78        density_field::{DensityField, DensityFieldDirty},
79        mesher::DensityFieldMeshSize,
80        neighbor::NeighborDensityFields,
81    };
82}
83
84/// Size of the density field grid per chunk (32×32×32 voxels).
85pub const DENSITY_FIELD_SIZE: UVec3 = uvec3(32, 32, 32);
86
87/// Total number of voxels per chunk.
88pub const FIELD_VOLUME: usize =
89    (DENSITY_FIELD_SIZE.x * DENSITY_FIELD_SIZE.y * DENSITY_FIELD_SIZE.z) as usize;
90
91/// Sentinel value indicating no vertex exists at a position.
92pub const NULL_VERTEX: u32 = u32::MAX;
93
94/// Plugin that enables automatic Surface Nets mesh generation for chunks with density fields.
95///
96/// When added to your app, this plugin will automatically generate and update meshes for any
97/// entity that has both a [`DensityField`] and [`DensityFieldDirty`] components.
98///
99/// # Example
100///
101/// ```no_run
102/// use bevy::prelude::*;
103/// use bevy_sculpter::prelude::*;
104/// use chunky_bevy::prelude::*;
105///
106/// App::new()
107///     .add_plugins(DefaultPlugins)
108///     .add_plugins(ChunkyPlugin::default())
109///     .add_plugins(SurfaceNetsPlugin)
110///     .insert_resource(DensityFieldMeshSize(vec3(10., 10., 10.)));
111/// ```
112pub struct SurfaceNetsPlugin;
113
114impl Plugin for SurfaceNetsPlugin {
115    fn build(&self, app: &mut App) {
116        app.init_resource::<DensityFieldMeshSize>().add_systems(
117            Update,
118            (
119                auto_mark_dirty,
120                gather_neighbor_fields,
121                process_dirty_chunks,
122            )
123                .chain(),
124        );
125    }
126}
127
128/// Auto-mark chunks dirty when their density field changes
129fn auto_mark_dirty(
130    mut commands: Commands,
131    changed: Query<Entity, (Changed<DensityField>, Without<DensityFieldDirty>)>,
132) {
133    for entity in changed.iter() {
134        commands.entity(entity).insert(DensityFieldDirty);
135    }
136}
137
138/// Gather neighbor density slices for dirty chunks
139fn gather_neighbor_fields(
140    mut commands: Commands,
141    dirty_chunks: Query<(Entity, &ChunkPos), (With<DensityFieldDirty>, With<DensityField>)>,
142    all_fields: Query<&DensityField>,
143    chunk_manager: Res<ChunkManager>,
144) {
145    for (entity, chunk_pos) in dirty_chunks.iter() {
146        let mut neighbors = NeighborDensityFields::default();
147
148        for face in NeighborFace::ALL {
149            let neighbor_pos = chunk_pos.0 + face.offset();
150
151            if let Some(neighbor_entity) = chunk_manager.get_chunk(&neighbor_pos)
152                && let Ok(neighbor_field) = all_fields.get(neighbor_entity)
153            {
154                neighbors.neighbors[face as usize] =
155                    Some(NeighborSlice::from_field(neighbor_field, face));
156            }
157        }
158
159        commands.entity(entity).insert(neighbors);
160    }
161}
162
163/// Process dirty chunks and generate meshes
164fn process_dirty_chunks(
165    mut commands: Commands,
166    mut meshes: ResMut<Assets<Mesh>>,
167    mut materials: ResMut<Assets<StandardMaterial>>,
168    dirty_chunks: Query<
169        (Entity, &DensityField, Option<&NeighborDensityFields>),
170        With<DensityFieldDirty>,
171    >,
172    mesh_size: Res<DensityFieldMeshSize>,
173    existing_meshes: Query<&Mesh3d>,
174) {
175    for (entity, field, neighbors) in dirty_chunks.iter() {
176        let neighbors = neighbors.cloned().unwrap_or_default();
177
178        if let Some(mesh) = mesher::generate_mesh_cpu(field, &neighbors, mesh_size.0) {
179            let mesh_handle = meshes.add(mesh);
180
181            if existing_meshes.get(entity).is_ok() {
182                commands.entity(entity).insert(Mesh3d(mesh_handle));
183            } else {
184                commands.entity(entity).insert((
185                    Mesh3d(mesh_handle),
186                    MeshMaterial3d(materials.add(StandardMaterial {
187                        base_color: Color::srgb(0.5, 0.7, 0.5),
188                        perceptual_roughness: 0.8,
189                        ..default()
190                    })),
191                ));
192            }
193        }
194
195        commands.entity(entity).remove::<DensityFieldDirty>();
196    }
197}