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