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