1use std::f32::consts::{PI, SQRT_2};
5
6use bevy::{
7 asset::RenderAssetUsages,
8 camera::ScalingMode,
9 color::palettes::css::{RED, WHITE},
10 input::common_conditions::input_just_pressed,
11 math::{
12 bounding::{
13 Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume,
14 },
15 Isometry2d,
16 },
17 mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment},
18 prelude::*,
19};
20
21const HEART: Heart = Heart::new(0.5);
22const EXTRUSION: Extrusion<Heart> = Extrusion {
23 base_shape: Heart::new(0.5),
24 half_depth: 0.5,
25};
26
27const TRANSFORM_2D: Transform = Transform {
29 translation: Vec3::ZERO,
30 rotation: Quat::IDENTITY,
31 scale: Vec3::ONE,
32};
33const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection {
35 near: -1.0,
36 far: 10.0,
37 scale: 1.0,
38 viewport_origin: Vec2::new(0.5, 0.5),
39 scaling_mode: ScalingMode::AutoMax {
40 max_width: 8.0,
41 max_height: 20.0,
42 },
43 area: Rect {
44 min: Vec2::NEG_ONE,
45 max: Vec2::ONE,
46 },
47});
48
49const TRANSFORM_3D: Transform = Transform {
51 translation: Vec3::ZERO,
52 rotation: Quat::from_xyzw(-0.14521316, -0.0, -0.0, 0.98940045),
54 scale: Vec3::ONE,
55};
56const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection {
58 fov: PI / 4.0,
59 near: 0.1,
60 far: 1000.0,
61 aspect_ratio: 1.0,
62});
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
66enum CameraActive {
67 #[default]
68 Dim2,
70 Dim3,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
76enum BoundingShape {
77 #[default]
78 None,
80 BoundingSphere,
82 BoundingBox,
84}
85
86#[derive(Component)]
88struct Shape2d;
89
90#[derive(Component)]
92struct Shape3d;
93
94fn main() {
95 App::new()
96 .add_plugins(DefaultPlugins)
97 .init_state::<BoundingShape>()
98 .init_state::<CameraActive>()
99 .add_systems(Startup, setup)
100 .add_systems(
101 Update,
102 (
103 (rotate_2d_shapes, bounding_shapes_2d).run_if(in_state(CameraActive::Dim2)),
104 (rotate_3d_shapes, bounding_shapes_3d).run_if(in_state(CameraActive::Dim3)),
105 update_bounding_shape.run_if(input_just_pressed(KeyCode::KeyB)),
106 switch_cameras.run_if(input_just_pressed(KeyCode::Space)),
107 ),
108 )
109 .run();
110}
111
112fn setup(
113 mut commands: Commands,
114 mut meshes: ResMut<Assets<Mesh>>,
115 mut materials: ResMut<Assets<StandardMaterial>>,
116) {
117 commands.spawn((Camera3d::default(), TRANSFORM_2D, PROJECTION_2D));
119
120 commands.spawn((
122 Mesh3d(meshes.add(HEART.mesh().resolution(50))),
124 MeshMaterial3d(materials.add(StandardMaterial {
125 emissive: RED.into(),
126 base_color: RED.into(),
127 ..Default::default()
128 })),
129 Transform::from_xyz(0.0, 0.0, 0.0),
130 Shape2d,
131 ));
132
133 commands.spawn((
135 Mesh3d(meshes.add(EXTRUSION.mesh().resolution(50))),
137 MeshMaterial3d(materials.add(StandardMaterial {
138 base_color: RED.into(),
139 ..Default::default()
140 })),
141 Transform::from_xyz(0., -3., -10.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
142 Shape3d,
143 ));
144
145 commands.spawn((
147 PointLight {
148 shadows_enabled: true,
149 intensity: 10_000_000.,
150 range: 100.0,
151 shadow_depth_bias: 0.2,
152 ..default()
153 },
154 Transform::from_xyz(8.0, 12.0, 1.0),
155 ));
156
157 commands.spawn((
159 Text::new("Press 'B' to toggle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
160 Press 'Space' to switch between 3D and 2D"),
161 Node {
162 position_type: PositionType::Absolute,
163 top: px(12),
164 left: px(12),
165 ..default()
166 },
167 ));
168}
169
170fn rotate_2d_shapes(mut shapes: Query<&mut Transform, With<Shape2d>>, time: Res<Time>) {
172 let elapsed_seconds = time.elapsed_secs();
173
174 for mut transform in shapes.iter_mut() {
175 transform.rotation = Quat::from_rotation_z(elapsed_seconds);
176 }
177}
178
179fn bounding_shapes_2d(
181 shapes: Query<&Transform, With<Shape2d>>,
182 mut gizmos: Gizmos,
183 bounding_shape: Res<State<BoundingShape>>,
184) {
185 for transform in shapes.iter() {
186 let rotation = transform.rotation.to_scaled_axis().z;
188 let rotation = Rot2::radians(rotation);
189 let isometry = Isometry2d::new(transform.translation.xy(), rotation);
190
191 match bounding_shape.get() {
192 BoundingShape::None => (),
193 BoundingShape::BoundingBox => {
194 let aabb = HEART.aabb_2d(isometry);
196 gizmos.rect_2d(aabb.center(), aabb.half_size() * 2., WHITE);
197 }
198 BoundingShape::BoundingSphere => {
199 let bounding_circle = HEART.bounding_circle(isometry);
201 gizmos
202 .circle_2d(bounding_circle.center(), bounding_circle.radius(), WHITE)
203 .resolution(64);
204 }
205 }
206 }
207}
208
209fn rotate_3d_shapes(mut shapes: Query<&mut Transform, With<Shape3d>>, time: Res<Time>) {
211 let delta_seconds = time.delta_secs();
212
213 for mut transform in shapes.iter_mut() {
214 transform.rotate_y(delta_seconds);
215 }
216}
217
218fn bounding_shapes_3d(
220 shapes: Query<&Transform, With<Shape3d>>,
221 mut gizmos: Gizmos,
222 bounding_shape: Res<State<BoundingShape>>,
223) {
224 for transform in shapes.iter() {
225 match bounding_shape.get() {
226 BoundingShape::None => (),
227 BoundingShape::BoundingBox => {
228 let aabb = EXTRUSION.aabb_3d(transform.to_isometry());
230
231 gizmos.primitive_3d(
232 &Cuboid::from_size(Vec3::from(aabb.half_size()) * 2.),
233 aabb.center(),
234 WHITE,
235 );
236 }
237 BoundingShape::BoundingSphere => {
238 let bounding_sphere = EXTRUSION.bounding_sphere(transform.to_isometry());
240
241 gizmos.sphere(bounding_sphere.center(), bounding_sphere.radius(), WHITE);
242 }
243 }
244 }
245}
246
247fn update_bounding_shape(
249 current: Res<State<BoundingShape>>,
250 mut next: ResMut<NextState<BoundingShape>>,
251) {
252 next.set(match current.get() {
253 BoundingShape::None => BoundingShape::BoundingBox,
254 BoundingShape::BoundingBox => BoundingShape::BoundingSphere,
255 BoundingShape::BoundingSphere => BoundingShape::None,
256 });
257}
258
259fn switch_cameras(
261 current: Res<State<CameraActive>>,
262 mut next: ResMut<NextState<CameraActive>>,
263 camera: Single<(&mut Transform, &mut Projection)>,
264) {
265 let next_state = match current.get() {
266 CameraActive::Dim2 => CameraActive::Dim3,
267 CameraActive::Dim3 => CameraActive::Dim2,
268 };
269 next.set(next_state);
270
271 let (mut transform, mut projection) = camera.into_inner();
272 match next_state {
273 CameraActive::Dim2 => {
274 *transform = TRANSFORM_2D;
275 *projection = PROJECTION_2D;
276 }
277 CameraActive::Dim3 => {
278 *transform = TRANSFORM_3D;
279 *projection = PROJECTION_3D;
280 }
281 };
282}
283
284#[derive(Copy, Clone)]
288struct Heart {
289 radius: f32,
291}
292
293impl Primitive2d for Heart {}
296
297impl Heart {
298 const fn new(radius: f32) -> Self {
299 Self { radius }
300 }
301}
302
303impl Measured2d for Heart {
306 fn perimeter(&self) -> f32 {
307 self.radius * (2.5 * PI + ops::powf(2f32, 1.5) + 2.0)
308 }
309
310 fn area(&self) -> f32 {
311 let circle_area = PI * self.radius * self.radius;
312 let triangle_area = self.radius * self.radius * (1.0 + 2f32.sqrt()) / 2.0;
313 let cutout = triangle_area - circle_area * 3.0 / 16.0;
314
315 2.0 * circle_area + 4.0 * cutout
316 }
317}
318
319impl Bounded2d for Heart {
321 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
322 let isometry = isometry.into();
323
324 let circle_center = isometry.rotation * Vec2::new(self.radius, 0.0);
326 let max_circle = circle_center.abs() + Vec2::splat(self.radius);
328 let min_circle = -max_circle;
330
331 let tip_position = isometry.rotation * Vec2::new(0.0, -self.radius * (1. + SQRT_2));
333
334 Aabb2d {
335 min: isometry.translation + min_circle.min(tip_position),
336 max: isometry.translation + max_circle.max(tip_position),
337 }
338 }
339
340 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
341 let isometry = isometry.into();
342
343 let offset = self.radius / ops::powf(2f32, 1.5);
345 let center = isometry * Vec2::new(0.0, -offset);
347 let radius = self.radius * (1.0 + 2f32.sqrt()) - offset;
349
350 BoundingCircle::new(center, radius)
351 }
352}
353impl BoundedExtrusion for Heart {}
356
357impl Meshable for Heart {
359 type Output = HeartMeshBuilder;
361
362 fn mesh(&self) -> Self::Output {
363 Self::Output {
364 heart: *self,
365 resolution: 32,
366 }
367 }
368}
369
370struct HeartMeshBuilder {
372 heart: Heart,
373 resolution: usize,
375}
376
377trait HeartBuilder {
380 fn resolution(self, resolution: usize) -> Self;
382}
383
384impl HeartBuilder for HeartMeshBuilder {
385 fn resolution(mut self, resolution: usize) -> Self {
386 self.resolution = resolution;
387 self
388 }
389}
390
391impl HeartBuilder for ExtrusionBuilder<Heart> {
392 fn resolution(mut self, resolution: usize) -> Self {
393 self.base_builder.resolution = resolution;
394 self
395 }
396}
397
398impl MeshBuilder for HeartMeshBuilder {
399 fn build(&self) -> Mesh {
401 let radius = self.heart.radius;
402 let wing_angle = PI * 1.25;
404
405 let mut vertices = Vec::with_capacity(2 * self.resolution);
407 let mut uvs = Vec::with_capacity(2 * self.resolution);
408 let mut indices = Vec::with_capacity(6 * self.resolution - 9);
409 let normals = vec![[0f32, 0f32, 1f32]; 2 * self.resolution];
411
412 vertices.push([0.0; 3]);
414 uvs.push([0.5, 0.5]);
415
416 for i in 1..self.resolution {
418 let angle = (i as f32 / self.resolution as f32) * wing_angle;
419 let (sin, cos) = ops::sin_cos(angle);
420 vertices.push([radius * (cos - 1.0), radius * sin, 0.0]);
421 uvs.push([0.5 - (cos - 1.0) / 4., 0.5 - sin / 2.]);
422 }
423
424 vertices.push([0.0, radius * (-1. - SQRT_2), 0.0]);
426 uvs.push([0.5, 1.]);
427
428 for i in 0..self.resolution - 1 {
430 let angle = (i as f32 / self.resolution as f32) * wing_angle - PI / 4.;
431 let (sin, cos) = ops::sin_cos(angle);
432 vertices.push([radius * (cos + 1.0), radius * sin, 0.0]);
433 uvs.push([0.5 - (cos + 1.0) / 4., 0.5 - sin / 2.]);
434 }
435
436 for i in 2..2 * self.resolution as u32 {
439 indices.extend_from_slice(&[i - 1, i, 0]);
440 }
441
442 Mesh::new(
444 bevy::mesh::PrimitiveTopology::TriangleList,
445 RenderAssetUsages::default(),
446 )
447 .with_inserted_indices(bevy::mesh::Indices::U32(indices))
448 .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
449 .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
450 .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
451 }
452}
453
454impl Extrudable for HeartMeshBuilder {
456 fn perimeter(&self) -> Vec<PerimeterSegment> {
457 let resolution = self.resolution as u32;
458 vec![
459 PerimeterSegment::Smooth {
461 first_normal: Vec2::X,
463 last_normal: Vec2::new(-1.0, -1.0).normalize(),
464 indices: (0..resolution).collect(),
466 },
467 PerimeterSegment::Flat {
469 indices: vec![resolution - 1, resolution, resolution + 1],
470 },
471 PerimeterSegment::Smooth {
473 first_normal: Vec2::new(1.0, -1.0).normalize(),
474 last_normal: Vec2::NEG_X,
475 indices: (resolution + 1..2 * resolution).chain([0]).collect(),
476 },
477 ]
478 }
479}