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}