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}