Skip to main content

custom_primitives/
custom_primitives.rs

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
4use std::f32::consts::{PI, SQRT_2};
5
6#[cfg(not(target_family = "wasm"))]
7use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin};
8
9use 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
19const HEART: Heart = Heart::new(0.5);
20const HOLLOW: Heart = Heart::new(0.3);
21// By implementing these traits we can construct the 2D ring version of this shape
22const RING: Ring<Heart> = Ring::new(HEART, HOLLOW);
23// By implementing these traits we can construct the 3D extrusion of this shape
24const EXTRUSION: Extrusion<Heart> = Extrusion {
25    base_shape: HEART,
26    half_depth: 0.5,
27};
28const 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
34const 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
40const 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
56const 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
63const 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)]
75enum 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
87impl 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)]
107enum 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)]
119struct Shape2d;
120
121/// A marker component for our 3D shapes so we can query them separately from the camera
122#[derive(Component)]
123struct Shape3d;
124
125fn 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
166fn 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.
269fn 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.
278fn 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.
308fn 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.
317fn 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.
346fn 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.
358fn 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"))]
389fn 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)]
397struct 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.
404impl Primitive2d for Heart {}
405
406impl 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>`.
414impl 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.
429impl 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.
464impl BoundedExtrusion for Heart {}
465
466// You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
467impl 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`.
480struct 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`.
488trait HeartBuilder {
489    /// Set the resolution for each of the wings of the heart.
490    fn resolution(self, resolution: usize) -> Self;
491}
492
493impl HeartBuilder for HeartMeshBuilder {
494    fn resolution(mut self, resolution: usize) -> Self {
495        self.resolution = resolution;
496        self
497    }
498}
499
500impl HeartBuilder for ExtrusionBuilder<Heart> {
501    fn resolution(mut self, resolution: usize) -> Self {
502        self.base_builder.resolution = resolution;
503        self
504    }
505}
506
507impl 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.
564impl 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
591fn 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}