docs/bevy/examples/math/custom_primitives.rs 21.1 K · 598 lines · raw · history

1 //! This example demonstrates how you can add your own custom primitives to bevy highlighting
2 //! traits you may want to implement for your primitives to achieve different functionalities.
3
4 use std::f32::consts::{PI, SQRT_2};
5
6 #[cfg(not(target_family = "wasm"))]
7 use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin};
8
9 use bevy::{
10 asset::RenderAssetUsages,
11 camera::ScalingMode,
12 color::palettes::css::{RED, WHITE},
13 input::common_conditions::{input_just_pressed, input_toggle_active},
14 math::Isometry2d,
15 mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment},
16 prelude::*,
17 };
18
19 const HEART: Heart = Heart::new(0.5);
20 const HOLLOW: Heart = Heart::new(0.3);
21 // By implementing these traits we can construct the 2D ring version of this shape
22 const RING: Ring<Heart> = Ring::new(HEART, HOLLOW);
23 // By implementing these traits we can construct the 3D extrusion of this shape
24 const EXTRUSION: Extrusion<Heart> = Extrusion {
25 base_shape: HEART,
26 half_depth: 0.5,
27 };
28 const RING_EXTRUSION: Extrusion<Ring<Heart>> = Extrusion {
29 base_shape: RING,
30 half_depth: 0.5,
31 };
32
33 // The transform of the camera in 2D
34 const TRANSFORM_2D: Transform = Transform {
35 translation: Vec3::ZERO,
36 rotation: Quat::IDENTITY,
37 scale: Vec3::ONE,
38 };
39 // The projection used for the camera in 2D
40 const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection {
41 near: -1.0,
42 far: 10.0,
43 scale: 1.0,
44 viewport_origin: Vec2::new(0.5, 0.5),
45 scaling_mode: ScalingMode::AutoMax {
46 max_width: 8.0,
47 max_height: 20.0,
48 },
49 area: Rect {
50 min: Vec2::NEG_ONE,
51 max: Vec2::ONE,
52 },
53 });
54
55 // The transform of the camera in 3D
56 const TRANSFORM_3D: Transform = Transform {
57 translation: Vec3::ZERO,
58 // The camera is pointing at the 3D shape
59 rotation: Quat::from_xyzw(-0.2669336, -0.0, -0.0, 0.96371484),
60 scale: Vec3::ONE,
61 };
62 // The projection used for the camera in 3D
63 const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection {
64 fov: PI / 4.0,
65 near: 0.1,
66 far: 1000.0,
67 aspect_ratio: 1.0,
68 near_clip_plane: vec4(0.0, 0.0, -1.0, -0.1),
69 });
70
71 /// State for tracking the currently displayed shape
72 ///
73 /// Also a component for associating the entity with this state, for toggling visibility
74 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect, Component)]
75 enum ShapeActive {
76 #[default]
77 /// The 2D heart shape is displayed
78 Heart,
79 /// The 2D heart ring shape is displayed
80 Ring,
81 /// The 3D extruded heart shape is displayed
82 Extrusion,
83 /// The 3D extruded heart ring shape is displayed
84 RingExtrusion,
85 }
86
87 impl ShapeActive {
88 const SHAPES: [ShapeActive; 4] = [
89 ShapeActive::Heart,
90 ShapeActive::Ring,
91 ShapeActive::Extrusion,
92 ShapeActive::RingExtrusion,
93 ];
94
95 fn next_shape(self) -> Self {
96 Self::SHAPES
97 .into_iter()
98 .cycle()
99 .skip_while(|shape| *shape != self)
100 .nth(1) // move to the next element
101 .unwrap()
102 }
103 }
104
105 /// State for tracking the currently displayed shape
106 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
107 enum BoundingShape {
108 #[default]
109 /// No bounding shapes
110 None,
111 /// The bounding sphere or circle of the shape
112 BoundingSphere,
113 /// The Axis Aligned Bounding Box (AABB) of the shape
114 BoundingBox,
115 }
116
117 /// A marker component for our 2D shapes so we can query them separately from the camera
118 #[derive(Component)]
119 struct Shape2d;
120
121 /// A marker component for our 3D shapes so we can query them separately from the camera
122 #[derive(Component)]
123 struct Shape3d;
124
125 fn main() {
126 let mut app = App::new();
127
128 app.add_plugins(DefaultPlugins);
129
130 #[cfg(not(target_family = "wasm"))]
131 app.add_plugins(WireframePlugin::default());
132
133 app.init_state::<BoundingShape>()
134 .init_state::<ShapeActive>()
135 .add_systems(Startup, setup)
136 .add_systems(
137 Update,
138 (
139 (
140 rotate_2d_shapes.run_if(input_toggle_active(true, KeyCode::KeyR)),
141 bounding_shapes_2d,
142 )
143 .run_if(state_in_one_of([ShapeActive::Heart, ShapeActive::Ring])),
144 (
145 rotate_3d_shapes.run_if(input_toggle_active(true, KeyCode::KeyR)),
146 bounding_shapes_3d,
147 )
148 .run_if(state_in_one_of([
149 ShapeActive::Extrusion,
150 ShapeActive::RingExtrusion,
151 ])),
152 update_bounding_shape.run_if(input_just_pressed(KeyCode::KeyB)),
153 switch_shapes.run_if(input_just_pressed(KeyCode::Tab)),
154 ),
155 );
156
157 #[cfg(not(target_family = "wasm"))]
158 app.add_systems(
159 Update,
160 toggle_wireframes.run_if(input_just_pressed(KeyCode::Space)),
161 );
162
163 app.run();
164 }
165
166 fn setup(
167 mut commands: Commands,
168 mut meshes: ResMut<Assets<Mesh>>,
169 mut materials: ResMut<Assets<StandardMaterial>>,
170 ) {
171 // Spawn the camera
172 commands.spawn((Camera3d::default(), TRANSFORM_2D, PROJECTION_2D));
173
174 // Spawn the 2D heart
175 commands.spawn((
176 // We can use the methods defined on the `MeshBuilder` to customize the mesh.
177 Mesh3d(meshes.add(HEART.mesh().resolution(50))),
178 MeshMaterial3d(materials.add(StandardMaterial {
179 emissive: RED.into(),
180 base_color: RED.into(),
181 ..Default::default()
182 })),
183 Transform::from_xyz(0.0, 0.0, 0.0),
184 Shape2d,
185 Visibility::Visible,
186 ShapeActive::Heart,
187 ));
188
189 // Spawn the 2D heart ring
190 commands.spawn((
191 // We can use the methods defined on the `MeshBuilder` to customize the mesh.
192 Mesh3d(meshes.add(RING.mesh().with_inner(|heart| heart.resolution(50)))),
193 MeshMaterial3d(materials.add(StandardMaterial {
194 emissive: RED.into(),
195 base_color: RED.into(),
196 ..Default::default()
197 })),
198 Transform::from_xyz(0.0, 0.0, 0.0),
199 Shape2d,
200 Visibility::Hidden,
201 ShapeActive::Ring,
202 ));
203
204 // Spawn an extrusion of the heart
205 commands.spawn((
206 // We can set a custom resolution for the round parts of the extrusion as well.
207 Mesh3d(meshes.add(EXTRUSION.mesh().resolution(50))),
208 MeshMaterial3d(materials.add(StandardMaterial {
209 base_color: RED.into(),
210 ..Default::default()
211 })),
212 Transform::from_xyz(0., -3., -5.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
213 Shape3d,
214 Visibility::Hidden,
215 ShapeActive::Extrusion,
216 ));
217
218 // Spawn an extrusion of the heart ring
219 commands.spawn((
220 // We can set a custom resolution for the round parts of the extrusion as well.
221 Mesh3d(
222 meshes.add(
223 RING_EXTRUSION
224 .mesh()
225 .with_inner(|ring| ring.with_inner(|heart| heart.resolution(50))),
226 ),
227 ),
228 MeshMaterial3d(materials.add(StandardMaterial {
229 base_color: RED.into(),
230 ..Default::default()
231 })),
232 Transform::from_xyz(0., -3., -5.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
233 Shape3d,
234 Visibility::Hidden,
235 ShapeActive::RingExtrusion,
236 ));
237
238 // Point light for 3D
239 commands.spawn((
240 PointLight {
241 shadow_maps_enabled: true,
242 intensity: 10_000_000.,
243 range: 100.0,
244 shadow_depth_bias: 0.2,
245 ..default()
246 },
247 Transform::from_xyz(8.0, 12.0, 1.0),
248 ));
249
250 let mut text = "\
251 Press 'B' to cycle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
252 Press 'Tab' to cycle between 2D and 3D shapes\n\
253 Press 'R' to pause/resume rotation".to_string();
254 #[cfg(not(target_family = "wasm"))]
255 text.push_str("\nPress 'Space' to toggle display of wireframes");
256 // Example instructions
257 commands.spawn((
258 Text::new(text),
259 Node {
260 position_type: PositionType::Absolute,
261 top: px(12),
262 left: px(12),
263 ..default()
264 },
265 ));
266 }
267
268 // Rotate the 2D shapes.
269 fn rotate_2d_shapes(mut shapes: Query<&mut Transform, With<Shape2d>>, time: Res<Time>) {
270 let elapsed_seconds = time.elapsed_secs();
271
272 for mut transform in shapes.iter_mut() {
273 transform.rotation = Quat::from_rotation_z(elapsed_seconds);
274 }
275 }
276
277 // Draw bounding boxes or circles for the 2D shapes.
278 fn bounding_shapes_2d(
279 shapes: Query<&Transform, With<Shape2d>>,
280 mut gizmos: Gizmos,
281 bounding_shape: Res<State<BoundingShape>>,
282 ) {
283 for transform in shapes.iter() {
284 // Get the rotation angle from the 3D rotation.
285 let rotation = transform.rotation.to_scaled_axis().z;
286 let rotation = Rot2::radians(rotation);
287 let isometry = Isometry2d::new(transform.translation.xy(), rotation);
288
289 match bounding_shape.get() {
290 BoundingShape::None => (),
291 BoundingShape::BoundingBox => {
292 // Get the AABB of the primitive with the rotation and translation of the mesh.
293 let aabb = HEART.aabb_2d(isometry);
294 gizmos.rect_2d(aabb.center(), aabb.half_size() * 2., WHITE);
295 }
296 BoundingShape::BoundingSphere => {
297 // Get the bounding sphere of the primitive with the rotation and translation of the mesh.
298 let bounding_circle = HEART.bounding_circle(isometry);
299 gizmos
300 .circle_2d(bounding_circle.center(), bounding_circle.radius(), WHITE)
301 .resolution(64);
302 }
303 }
304 }
305 }
306
307 // Rotate the 3D shapes.
308 fn rotate_3d_shapes(mut shapes: Query<&mut Transform, With<Shape3d>>, time: Res<Time>) {
309 let delta_seconds = time.delta_secs();
310
311 for mut transform in shapes.iter_mut() {
312 transform.rotate_y(delta_seconds);
313 }
314 }
315
316 // Draw the AABBs or bounding spheres for the 3D shapes.
317 fn bounding_shapes_3d(
318 shapes: Query<&Transform, With<Shape3d>>,
319 mut gizmos: Gizmos,
320 bounding_shape: Res<State<BoundingShape>>,
321 ) {
322 for transform in shapes.iter() {
323 match bounding_shape.get() {
324 BoundingShape::None => (),
325 BoundingShape::BoundingBox => {
326 // Get the AABB of the extrusion with the rotation and translation of the mesh.
327 let aabb = EXTRUSION.aabb_3d(transform.to_isometry());
328
329 gizmos.primitive_3d(
330 &Cuboid::from_size(Vec3::from(aabb.half_size()) * 2.),
331 aabb.center(),
332 WHITE,
333 );
334 }
335 BoundingShape::BoundingSphere => {
336 // Get the bounding sphere of the extrusion with the rotation and translation of the mesh.
337 let bounding_sphere = EXTRUSION.bounding_sphere(transform.to_isometry());
338
339 gizmos.sphere(bounding_sphere.center(), bounding_sphere.radius(), WHITE);
340 }
341 }
342 }
343 }
344
345 // Switch to the next bounding shape.
346 fn update_bounding_shape(
347 current: Res<State<BoundingShape>>,
348 mut next: ResMut<NextState<BoundingShape>>,
349 ) {
350 next.set(match current.get() {
351 BoundingShape::None => BoundingShape::BoundingBox,
352 BoundingShape::BoundingBox => BoundingShape::BoundingSphere,
353 BoundingShape::BoundingSphere => BoundingShape::None,
354 });
355 }
356
357 // Switch between shapes, and update 2D and 3D cameras.
358 fn switch_shapes(
359 current: Res<State<ShapeActive>>,
360 mut next: ResMut<NextState<ShapeActive>>,
361 camera: Single<(&mut Transform, &mut Projection)>,
362 mut shapes: Query<(&mut Visibility, &ShapeActive)>,
363 ) {
364 let next_state = current.get().next_shape();
365 next.set(next_state);
366
367 for (mut visibility, shape) in &mut shapes {
368 if next_state == *shape {
369 *visibility = Visibility::Visible;
370 } else {
371 *visibility = Visibility::Hidden;
372 }
373 }
374
375 let (mut transform, mut projection) = camera.into_inner();
376 match next_state {
377 ShapeActive::Heart | ShapeActive::Ring => {
378 *transform = TRANSFORM_2D;
379 *projection = PROJECTION_2D;
380 }
381 ShapeActive::Extrusion | ShapeActive::RingExtrusion => {
382 *transform = TRANSFORM_3D;
383 *projection = PROJECTION_3D;
384 }
385 };
386 }
387
388 #[cfg(not(target_family = "wasm"))]
389 fn toggle_wireframes(mut wireframe_config: ResMut<WireframeConfig>) {
390 wireframe_config.global = !wireframe_config.global;
391 }
392
393 /// A custom 2D heart primitive. The heart is made up of two circles centered at `Vec2::new(±radius, 0.)` each with the same `radius`.
394 ///
395 /// The tip of the heart connects the two circles at a 45° angle from `Vec3::NEG_Y`.
396 #[derive(Copy, Clone)]
397 struct Heart {
398 /// The radius of each wing of the heart
399 radius: f32,
400 }
401
402 // The `Primitive2d` or `Primitive3d` trait is required by almost all other traits for primitives in bevy.
403 // Depending on your shape, you should implement either one of them.
404 impl Primitive2d for Heart {}
405
406 impl Heart {
407 const fn new(radius: f32) -> Self {
408 Self { radius }
409 }
410 }
411
412 // The `Measured2d` and `Measured3d` traits are used to compute the perimeter, the area or the volume of a primitive.
413 // If you implement `Measured2d` for a 2D primitive, `Measured3d` is automatically implemented for `Extrusion<T>`.
414 impl Measured2d for Heart {
415 fn perimeter(&self) -> f32 {
416 self.radius * (2.5 * PI + ops::powf(2f32, 1.5) + 2.0)
417 }
418
419 fn area(&self) -> f32 {
420 let circle_area = PI * self.radius * self.radius;
421 let triangle_area = self.radius * self.radius * (1.0 + 2f32.sqrt()) / 2.0;
422 let cutout = triangle_area - circle_area * 3.0 / 16.0;
423
424 2.0 * circle_area + 4.0 * cutout
425 }
426 }
427
428 // The `Bounded2d` or `Bounded3d` traits are used to compute the Axis Aligned Bounding Boxes or bounding circles / spheres for primitives.
429 impl Bounded2d for Heart {
430 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
431 let isometry = isometry.into();
432
433 // The center of the circle at the center of the right wing of the heart
434 let circle_center = isometry.rotation * Vec2::new(self.radius, 0.0);
435 // The maximum X and Y positions of the two circles of the wings of the heart.
436 let max_circle = circle_center.abs() + Vec2::splat(self.radius);
437 // Since the two circles of the heart are mirrored around the origin, the minimum position is the negative of the maximum.
438 let min_circle = -max_circle;
439
440 // The position of the tip at the bottom of the heart
441 let tip_position = isometry.rotation * Vec2::new(0.0, -self.radius * (1. + SQRT_2));
442
443 Aabb2d {
444 min: isometry.translation + min_circle.min(tip_position),
445 max: isometry.translation + max_circle.max(tip_position),
446 }
447 }
448
449 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
450 let isometry = isometry.into();
451
452 // The bounding circle of the heart is not at its origin. This `offset` is the offset between the center of the bounding circle and its translation.
453 let offset = self.radius / ops::powf(2f32, 1.5);
454 // The center of the bounding circle
455 let center = isometry * Vec2::new(0.0, -offset);
456 // The radius of the bounding circle
457 let radius = self.radius * (1.0 + 2f32.sqrt()) - offset;
458
459 BoundingCircle::new(center, radius)
460 }
461 }
462 // You can implement the `BoundedExtrusion` trait to implement `Bounded3d for Extrusion<Heart>`. There is a default implementation for both AABBs and bounding spheres,
463 // but you may be able to find faster solutions for your specific primitives.
464 impl BoundedExtrusion for Heart {}
465
466 // You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
467 impl Meshable for Heart {
468 // The `MeshBuilder` can be used to create the actual mesh for that primitive.
469 type Output = HeartMeshBuilder;
470
471 fn mesh(&self) -> Self::Output {
472 Self::Output {
473 heart: *self,
474 resolution: 32,
475 }
476 }
477 }
478
479 // You can include any additional information needed for meshing the primitive in the `MeshBuilder`.
480 struct HeartMeshBuilder {
481 heart: Heart,
482 // The resolution determines the amount of vertices used for each wing of the heart
483 resolution: usize,
484 }
485
486 // This trait is needed so that the configuration methods of the builder of the primitive are also available for the builder for the extrusion.
487 // If you do not want to support these configuration options for extrusions you can just implement them for your 2D `MeshBuilder`.
488 trait HeartBuilder {
489 /// Set the resolution for each of the wings of the heart.
490 fn resolution(self, resolution: usize) -> Self;
491 }
492
493 impl HeartBuilder for HeartMeshBuilder {
494 fn resolution(mut self, resolution: usize) -> Self {
495 self.resolution = resolution;
496 self
497 }
498 }
499
500 impl HeartBuilder for ExtrusionBuilder<Heart> {
501 fn resolution(mut self, resolution: usize) -> Self {
502 self.base_builder.resolution = resolution;
503 self
504 }
505 }
506
507 impl MeshBuilder for HeartMeshBuilder {
508 // This is where you should build the actual mesh.
509 fn build(&self) -> Mesh {
510 let radius = self.heart.radius;
511 // The curved parts of each wing (half) of the heart have an angle of `PI * 1.25` or 225°
512 let wing_angle = PI * 1.25;
513
514 // We create buffers for the vertices, their normals and UVs, as well as the indices used to connect the vertices.
515 let mut vertices = Vec::with_capacity(2 * self.resolution);
516 let mut uvs = Vec::with_capacity(2 * self.resolution);
517 let mut indices = Vec::with_capacity(6 * self.resolution - 9);
518 // Since the heart is flat, we know all the normals are identical already.
519 let normals = vec![[0f32, 0f32, 1f32]; 2 * self.resolution];
520
521 // The point in the middle of the two curved parts of the heart
522 vertices.push([0.0; 3]);
523 uvs.push([0.5, 0.5]);
524
525 // The left wing of the heart, starting from the point in the middle.
526 for i in 1..self.resolution {
527 let angle = (i as f32 / self.resolution as f32) * wing_angle;
528 let (sin, cos) = ops::sin_cos(angle);
529 vertices.push([radius * (cos - 1.0), radius * sin, 0.0]);
530 uvs.push([0.5 - (cos - 1.0) / 4., 0.5 - sin / 2.]);
531 }
532
533 // The bottom tip of the heart
534 vertices.push([0.0, radius * (-1. - SQRT_2), 0.0]);
535 uvs.push([0.5, 1.]);
536
537 // The right wing of the heart, starting from the bottom most point and going towards the middle point.
538 for i in 0..self.resolution - 1 {
539 let angle = (i as f32 / self.resolution as f32) * wing_angle - PI / 4.;
540 let (sin, cos) = ops::sin_cos(angle);
541 vertices.push([radius * (cos + 1.0), radius * sin, 0.0]);
542 uvs.push([0.5 - (cos + 1.0) / 4., 0.5 - sin / 2.]);
543 }
544
545 // This is where we build all the triangles from the points created above.
546 // Each triangle has one corner on the middle point with the other two being adjacent points on the perimeter of the heart.
547 for i in 2..2 * self.resolution as u32 {
548 indices.extend_from_slice(&[i - 1, i, 0]);
549 }
550
551 // Here, the actual `Mesh` is created. We set the indices, vertices, normals and UVs created above and specify the topology of the mesh.
552 Mesh::new(
553 bevy::mesh::PrimitiveTopology::TriangleList,
554 RenderAssetUsages::default(),
555 )
556 .with_inserted_indices(bevy::mesh::Indices::U32(indices))
557 .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
558 .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
559 .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
560 }
561 }
562
563 // The `Extrudable` trait can be used to easily implement meshing for extrusions.
564 impl Extrudable for HeartMeshBuilder {
565 fn perimeter(&self) -> Vec<PerimeterSegment> {
566 let resolution = self.resolution as u32;
567 vec![
568 // The left wing of the heart
569 PerimeterSegment::Smooth {
570 // The normals of the first and last vertices of smooth segments have to be specified manually.
571 first_normal: Vec2::X,
572 last_normal: Vec2::new(-1.0, -1.0).normalize(),
573 // These indices are used to index into the `ATTRIBUTE_POSITION` vec of your 2D mesh.
574 indices: (0..resolution).collect(),
575 },
576 // The bottom tip of the heart
577 PerimeterSegment::Flat {
578 indices: vec![resolution - 1, resolution, resolution + 1],
579 },
580 // The right wing of the heart
581 PerimeterSegment::Smooth {
582 first_normal: Vec2::new(1.0, -1.0).normalize(),
583 last_normal: Vec2::NEG_X,
584 indices: (resolution + 1..2 * resolution).chain([0]).collect(),
585 },
586 ]
587 }
588 }
589
590 // Helper run condition for matching multiple states
591 fn state_in_one_of<S: States, const N: usize>(
592 states: [S; N],
593 ) -> impl FnMut(Option<Res<State<S>>>) -> bool + Clone {
594 move |current_state: Option<Res<State<S>>>| match current_state {
595 Some(current_state) => states.contains(&current_state),
596 None => false,
597 }
598 }