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