1use bevy::{
15 input::mouse::{MouseMotion, MouseWheel},
16 prelude::*,
17 window::PrimaryWindow,
18};
19use bevy_sculpter::prelude::*;
20use chunky_bevy::prelude::*;
21
22fn main() {
23 App::new()
24 .add_plugins(DefaultPlugins)
25 .add_plugins(SurfaceNetsPlugin)
26 .insert_resource(DensityFieldMeshSize(vec3(10., 10., 10.)))
27 .init_resource::<SculptBrush>()
28 .add_systems(Startup, (setup, show_chunks))
29 .add_systems(
30 Update,
31 (fly_camera, sculpt_terrain, update_brush_preview, ui_text),
32 )
33 .run();
34}
35
36fn show_chunks(mut show_chunks: ResMut<NextState<ChunkBoundryVisualizer>>) {
37 show_chunks.set(ChunkBoundryVisualizer::On);
38}
39
40#[derive(Clone, Copy, PartialEq, Eq, Default)]
41enum BrushMode {
42 #[default]
43 Smooth,
44 _Hard,
45 Blur,
46}
47
48#[derive(Resource)]
49struct SculptBrush {
50 radius: f32,
51 min_radius: f32,
52 max_radius: f32,
53 strength: f32,
54 min_strength: f32,
55 max_strength: f32,
56 falloff: f32,
57 mode: BrushMode,
58}
59
60impl Default for SculptBrush {
61 fn default() -> Self {
62 Self {
63 radius: 2.0,
64 min_radius: 0.5,
65 max_radius: 8.0,
66 strength: 5.0, min_strength: 0.5,
68 max_strength: 20.0,
69 falloff: 2.0, mode: BrushMode::Smooth,
71 }
72 }
73}
74
75#[derive(Component)]
76struct BrushPreview;
77
78#[derive(Component)]
79struct UiText;
80
81#[derive(Component)]
82struct FlyCam {
83 speed: f32,
84 sensitivity: f32,
85 pitch: f32,
86 yaw: f32,
87}
88
89impl Default for FlyCam {
90 fn default() -> Self {
91 Self {
92 speed: 20.0,
93 sensitivity: 0.003,
94 pitch: 0.0,
95 yaw: 0.0,
96 }
97 }
98}
99
100fn setup(
101 mut commands: Commands,
102 mut meshes: ResMut<Assets<Mesh>>,
103 mut materials: ResMut<Assets<StandardMaterial>>,
104) {
105 for x in -1..=1 {
106 for y in -1..=1 {
107 for z in -1..=1 {
108 let mut field = DensityField::new();
109 let local_center = vec3(16.0, 16.0, 16.0);
110 let global_offset = vec3(x as f32, y as f32, z as f32) * 32.0;
111 let sphere_center = vec3(0.0, 0.0, 0.0);
112 let local_sphere_center = sphere_center - global_offset + local_center;
113 bevy_sculpter::helpers::fill_sphere(&mut field, local_sphere_center, 20.0);
114 commands.spawn((Chunk, ChunkPos(ivec3(x, y, z)), field, DensityFieldDirty));
115 }
116 }
117 }
118
119 commands.spawn((
121 Mesh3d(meshes.add(Sphere::new(1.0).mesh().ico(2).unwrap())),
122 MeshMaterial3d(materials.add(StandardMaterial {
123 base_color: Color::srgba(0.2, 0.8, 0.2, 0.3),
124 alpha_mode: AlphaMode::Blend,
125 unlit: true,
126 ..default()
127 })),
128 Transform::from_scale(Vec3::ZERO),
129 BrushPreview,
130 ));
131
132 commands.spawn((
133 Camera3d::default(),
134 Transform::from_xyz(30.0, 30.0, 30.0).looking_at(Vec3::ZERO, Vec3::Y),
135 FlyCam::default(),
136 ));
137
138 commands.spawn((
139 DirectionalLight {
140 illuminance: 10000.0,
141 shadows_enabled: true,
142 ..default()
143 },
144 Transform::from_xyz(10.0, 20.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
145 ));
146
147 commands.spawn((
148 Text::new(""),
149 Node {
150 position_type: PositionType::Absolute,
151 top: Val::Px(10.0),
152 left: Val::Px(10.0),
153 ..default()
154 },
155 UiText,
156 ));
157}
158
159fn fly_camera(
160 time: Res<Time>,
161 keyboard: Res<ButtonInput<KeyCode>>,
162 mouse_buttons: Res<ButtonInput<MouseButton>>,
163 mut mouse_motion: MessageReader<MouseMotion>,
164 mut scroll: MessageReader<MouseWheel>,
165 mut query: Query<(&mut Transform, &mut FlyCam)>,
166 mut brush: ResMut<SculptBrush>,
167) {
168 let Ok((mut transform, mut fly_cam)) = query.single_mut() else {
169 return;
170 };
171
172 if keyboard.just_pressed(KeyCode::KeyB) {
174 brush.mode = match brush.mode {
175 BrushMode::Smooth => BrushMode::Blur,
176 BrushMode::Blur => BrushMode::Smooth,
177 BrushMode::_Hard => BrushMode::Smooth,
178 };
179 }
180
181 if keyboard.just_pressed(KeyCode::BracketLeft) {
183 brush.strength = (brush.strength - 1.0).max(brush.min_strength);
184 }
185 if keyboard.just_pressed(KeyCode::BracketRight) {
186 brush.strength = (brush.strength + 1.0).min(brush.max_strength);
187 }
188
189 if mouse_buttons.pressed(MouseButton::Middle) {
190 for motion in mouse_motion.read() {
191 fly_cam.yaw -= motion.delta.x * fly_cam.sensitivity;
192 fly_cam.pitch -= motion.delta.y * fly_cam.sensitivity;
193 fly_cam.pitch = fly_cam.pitch.clamp(-1.5, 1.5);
194 }
195 transform.rotation = Quat::from_euler(EulerRot::YXZ, fly_cam.yaw, fly_cam.pitch, 0.0);
196 } else {
197 mouse_motion.clear();
198 }
199
200 for ev in scroll.read() {
201 brush.radius = (brush.radius + ev.y * 0.2).clamp(brush.min_radius, brush.max_radius);
202 }
203
204 let mut velocity = Vec3::ZERO;
205 let forward = transform.forward();
206 let right = transform.right();
207
208 if keyboard.pressed(KeyCode::KeyW) {
209 velocity += *forward;
210 }
211 if keyboard.pressed(KeyCode::KeyS) {
212 velocity -= *forward;
213 }
214 if keyboard.pressed(KeyCode::KeyA) {
215 velocity -= *right;
216 }
217 if keyboard.pressed(KeyCode::KeyD) {
218 velocity += *right;
219 }
220 if keyboard.pressed(KeyCode::Space) {
221 velocity += Vec3::Y;
222 }
223 if keyboard.pressed(KeyCode::ShiftLeft) {
224 velocity -= Vec3::Y;
225 }
226
227 if velocity.length_squared() > 0.0 {
228 velocity = velocity.normalize() * fly_cam.speed * time.delta_secs();
229 transform.translation += velocity;
230 }
231}
232
233fn sculpt_terrain(
234 time: Res<Time>,
235 keyboard: Res<ButtonInput<KeyCode>>,
236 mouse_buttons: Res<ButtonInput<MouseButton>>,
237 window_q: Query<&Window, With<PrimaryWindow>>,
238 camera_q: Query<(&Camera, &GlobalTransform), With<FlyCam>>,
239 mut chunks: Query<(&ChunkPos, &mut DensityField)>,
240 mesh_size: Res<DensityFieldMeshSize>,
241 brush: Res<SculptBrush>,
242 mut commands: Commands,
243 chunk_entities: Query<Entity, With<ChunkPos>>,
244) {
245 let adding = mouse_buttons.pressed(MouseButton::Right);
246 let removing = mouse_buttons.pressed(MouseButton::Left);
247
248 if !adding && !removing {
249 return;
250 }
251
252 let Ok(window) = window_q.single() else {
253 return;
254 };
255 let Some(cursor_pos) = window.cursor_position() else {
256 return;
257 };
258 let Ok((camera, cam_transform)) = camera_q.single() else {
259 return;
260 };
261 let Ok(ray) = camera.viewport_to_world(cam_transform, cursor_pos) else {
262 return;
263 };
264
265 let Some(hit_point) = raycast_terrain(&chunks, &mesh_size, ray) else {
266 return;
267 };
268
269 let world_brush_radius = brush.radius;
270 let chunk_world_size = mesh_size.0;
271 let use_hard_brush =
272 keyboard.pressed(KeyCode::ControlLeft) || keyboard.pressed(KeyCode::ControlRight);
273
274 for (chunk_pos, mut field) in chunks.iter_mut() {
275 let chunk_world_origin = chunk_pos.0.as_vec3() * chunk_world_size;
276 let local_hit = hit_point - chunk_world_origin;
277
278 let scale = Vec3::new(32.0, 32.0, 32.0) / chunk_world_size;
279 let grid_center = local_hit * scale;
280 let grid_radius = world_brush_radius * scale.x;
281
282 let chunk_min = Vec3::ZERO;
284 let chunk_max = Vec3::splat(32.0);
285 let brush_min = grid_center - Vec3::splat(grid_radius);
286 let brush_max = grid_center + Vec3::splat(grid_radius);
287
288 if brush_max.x < chunk_min.x
289 || brush_min.x > chunk_max.x
290 || brush_max.y < chunk_min.y
291 || brush_min.y > chunk_max.y
292 || brush_max.z < chunk_min.z
293 || brush_min.z > chunk_max.z
294 {
295 continue;
296 }
297
298 if use_hard_brush {
299 bevy_sculpter::helpers::brush_sphere(&mut field, grid_center, grid_radius, adding);
301 } else {
302 match brush.mode {
303 BrushMode::Smooth => {
304 let rate = if adding {
308 -brush.strength
309 } else {
310 brush.strength
311 };
312 bevy_sculpter::helpers::brush_smooth_timed(
313 &mut field,
314 grid_center,
315 grid_radius,
316 rate,
317 time.delta_secs(),
318 brush.falloff,
319 );
320 }
321 BrushMode::Blur => {
322 bevy_sculpter::helpers::brush_blur(
324 &mut field,
325 grid_center,
326 grid_radius,
327 brush.strength * 0.1 * time.delta_secs(),
328 brush.falloff,
329 );
330 }
331 BrushMode::_Hard => {
332 bevy_sculpter::helpers::brush_sphere(
333 &mut field,
334 grid_center,
335 grid_radius,
336 adding,
337 );
338 }
339 }
340 }
341 }
342
343 for entity in chunk_entities.iter() {
344 commands.entity(entity).insert(DensityFieldDirty);
345 }
346}
347
348fn raycast_terrain(
349 chunks: &Query<(&ChunkPos, &mut DensityField)>,
350 mesh_size: &DensityFieldMeshSize,
351 ray: Ray3d,
352) -> Option<Vec3> {
353 let chunk_world_size = mesh_size.0;
354 let max_dist = 200.0;
355 let step = 0.1;
356 let mut t = 0.0;
357
358 while t < max_dist {
359 let point = ray.origin + ray.direction * t;
360 let chunk_coord = (point / chunk_world_size).floor().as_ivec3();
361
362 for (chunk_pos, field) in chunks.iter() {
363 if chunk_pos.0 != chunk_coord {
364 continue;
365 }
366
367 let chunk_origin = chunk_pos.0.as_vec3() * chunk_world_size;
368 let local_pos = point - chunk_origin;
369 let scale = Vec3::new(32.0, 32.0, 32.0) / chunk_world_size;
370 let grid_pos = local_pos * scale;
371
372 if grid_pos.x >= 0.0
373 && grid_pos.x < 32.0
374 && grid_pos.y >= 0.0
375 && grid_pos.y < 32.0
376 && grid_pos.z >= 0.0
377 && grid_pos.z < 32.0
378 {
379 let density = field.get(grid_pos.x as u32, grid_pos.y as u32, grid_pos.z as u32);
380 if density < 0.0 {
381 return Some(point);
382 }
383 }
384 }
385 t += step;
386 }
387 None
388}
389
390fn update_brush_preview(
391 window_q: Query<&Window, With<PrimaryWindow>>,
392 camera_q: Query<(&Camera, &GlobalTransform), With<FlyCam>>,
393 chunks: Query<(&ChunkPos, &mut DensityField)>,
394 mesh_size: Res<DensityFieldMeshSize>,
395 brush: Res<SculptBrush>,
396 mut preview_q: Query<(&mut Transform, &MeshMaterial3d<StandardMaterial>), With<BrushPreview>>,
397 mut materials: ResMut<Assets<StandardMaterial>>,
398) {
399 let Ok((mut preview_transform, mat_handle)) = preview_q.single_mut() else {
400 return;
401 };
402 let Ok(window) = window_q.single() else {
403 return;
404 };
405 let Some(cursor_pos) = window.cursor_position() else {
406 preview_transform.scale = Vec3::ZERO;
407 return;
408 };
409 let Ok((camera, cam_transform)) = camera_q.single() else {
410 return;
411 };
412 let Ok(ray) = camera.viewport_to_world(cam_transform, cursor_pos) else {
413 return;
414 };
415
416 if let Some(hit) = raycast_terrain(&chunks, &mesh_size, ray) {
417 preview_transform.translation = hit;
418 preview_transform.scale = Vec3::splat(brush.radius);
419
420 if let Some(mat) = materials.get_mut(&mat_handle.0) {
422 mat.base_color = match brush.mode {
423 BrushMode::Smooth => Color::srgba(0.2, 0.8, 0.2, 0.3),
424 BrushMode::Blur => Color::srgba(0.2, 0.2, 0.8, 0.3),
425 BrushMode::_Hard => Color::srgba(0.8, 0.2, 0.2, 0.3),
426 };
427 }
428 } else {
429 preview_transform.scale = Vec3::ZERO;
430 }
431}
432
433fn ui_text(brush: Res<SculptBrush>, mut text_q: Query<&mut Text, With<UiText>>) {
434 let Ok(mut text) = text_q.single_mut() else {
435 return;
436 };
437
438 let mode_str = match brush.mode {
439 BrushMode::Smooth => "Smooth (continuous)",
440 BrushMode::Blur => "Blur/Smooth surface",
441 BrushMode::_Hard => "Hard (CSG)",
442 };
443
444 *text = Text::new(format!(
445 "Sculpt Controls:\n\
446 Middle Click + Drag: Rotate camera\n\
447 Right Click (hold): Add material\n\
448 Left Click (hold): Remove material\n\
449 Ctrl + Click: Hard brush (instant CSG)\n\
450 \n\
451 B: Toggle brush mode\n\
452 Scroll: Brush size ({:.1})\n\
453 [ ]: Brush strength ({:.1})\n\
454 \n\
455 Mode: {}",
456 brush.radius, brush.strength, mode_str
457 ));
458}