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
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
27// The transform of the camera in 2D
28const TRANSFORM_2D: Transform = Transform {
29    translation: Vec3::ZERO,
30    rotation: Quat::IDENTITY,
31    scale: Vec3::ONE,
32};
33// The projection used for the camera in 2D
34const 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
49// The transform of the camera in 3D
50const TRANSFORM_3D: Transform = Transform {
51    translation: Vec3::ZERO,
52    // The camera is pointing at the 3D shape
53    rotation: Quat::from_xyzw(-0.14521316, -0.0, -0.0, 0.98940045),
54    scale: Vec3::ONE,
55};
56// The projection used for the camera in 3D
57const 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/// State for tracking the currently displayed shape
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
66enum CameraActive {
67    #[default]
68    /// The 2D shape is displayed
69    Dim2,
70    /// The 3D shape is displayed
71    Dim3,
72}
73
74/// State for tracking the currently displayed shape
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
76enum BoundingShape {
77    #[default]
78    /// No bounding shapes
79    None,
80    /// The bounding sphere or circle of the shape
81    BoundingSphere,
82    /// The Axis Aligned Bounding Box (AABB) of the shape
83    BoundingBox,
84}
85
86/// A marker component for our 2D shapes so we can query them separately from the camera
87#[derive(Component)]
88struct Shape2d;
89
90/// A marker component for our 3D shapes so we can query them separately from the camera
91#[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    // Spawn the camera
118    commands.spawn((Camera3d::default(), TRANSFORM_2D, PROJECTION_2D));
119
120    // Spawn the 2D heart
121    commands.spawn((
122        // We can use the methods defined on the `MeshBuilder` to customize the mesh.
123        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    // Spawn an extrusion of the heart.
134    commands.spawn((
135        // We can set a custom resolution for the round parts of the extrusion as well.
136        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    // Point light for 3D
146    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    // Example instructions
158    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
170// Rotate the 2D shapes.
171fn 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
179// Draw bounding boxes or circles for the 2D shapes.
180fn 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        // Get the rotation angle from the 3D rotation.
187        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                // Get the AABB of the primitive with the rotation and translation of the mesh.
195                let aabb = HEART.aabb_2d(isometry);
196                gizmos.rect_2d(aabb.center(), aabb.half_size() * 2., WHITE);
197            }
198            BoundingShape::BoundingSphere => {
199                // Get the bounding sphere of the primitive with the rotation and translation of the mesh.
200                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
209// Rotate the 3D shapes.
210fn 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
218// Draw the AABBs or bounding spheres for the 3D shapes.
219fn 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                // Get the AABB of the extrusion with the rotation and translation of the mesh.
229                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                // Get the bounding sphere of the extrusion with the rotation and translation of the mesh.
239                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
247// Switch to the next bounding shape.
248fn 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
259// Switch between 2D and 3D cameras.
260fn 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/// A custom 2D heart primitive. The heart is made up of two circles centered at `Vec2::new(±radius, 0.)` each with the same `radius`.
285///
286/// The tip of the heart connects the two circles at a 45° angle from `Vec3::NEG_Y`.
287#[derive(Copy, Clone)]
288struct Heart {
289    /// The radius of each wing of the heart
290    radius: f32,
291}
292
293// The `Primitive2d` or `Primitive3d` trait is required by almost all other traits for primitives in bevy.
294// Depending on your shape, you should implement either one of them.
295impl Primitive2d for Heart {}
296
297impl Heart {
298    const fn new(radius: f32) -> Self {
299        Self { radius }
300    }
301}
302
303// The `Measured2d` and `Measured3d` traits are used to compute the perimeter, the area or the volume of a primitive.
304// If you implement `Measured2d` for a 2D primitive, `Measured3d` is automatically implemented for `Extrusion<T>`.
305impl 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
319// The `Bounded2d` or `Bounded3d` traits are used to compute the Axis Aligned Bounding Boxes or bounding circles / spheres for primitives.
320impl Bounded2d for Heart {
321    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
322        let isometry = isometry.into();
323
324        // The center of the circle at the center of the right wing of the heart
325        let circle_center = isometry.rotation * Vec2::new(self.radius, 0.0);
326        // The maximum X and Y positions of the two circles of the wings of the heart.
327        let max_circle = circle_center.abs() + Vec2::splat(self.radius);
328        // Since the two circles of the heart are mirrored around the origin, the minimum position is the negative of the maximum.
329        let min_circle = -max_circle;
330
331        // The position of the tip at the bottom of the heart
332        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        // 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.
344        let offset = self.radius / ops::powf(2f32, 1.5);
345        // The center of the bounding circle
346        let center = isometry * Vec2::new(0.0, -offset);
347        // The radius of the bounding circle
348        let radius = self.radius * (1.0 + 2f32.sqrt()) - offset;
349
350        BoundingCircle::new(center, radius)
351    }
352}
353// You can implement the `BoundedExtrusion` trait to implement `Bounded3d for Extrusion<Heart>`. There is a default implementation for both AABBs and bounding spheres,
354// but you may be able to find faster solutions for your specific primitives.
355impl BoundedExtrusion for Heart {}
356
357// You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
358impl Meshable for Heart {
359    // The `MeshBuilder` can be used to create the actual mesh for that primitive.
360    type Output = HeartMeshBuilder;
361
362    fn mesh(&self) -> Self::Output {
363        Self::Output {
364            heart: *self,
365            resolution: 32,
366        }
367    }
368}
369
370// You can include any additional information needed for meshing the primitive in the `MeshBuilder`.
371struct HeartMeshBuilder {
372    heart: Heart,
373    // The resolution determines the amount of vertices used for each wing of the heart
374    resolution: usize,
375}
376
377// This trait is needed so that the configuration methods of the builder of the primitive are also available for the builder for the extrusion.
378// If you do not want to support these configuration options for extrusions you can just implement them for your 2D `MeshBuilder`.
379trait HeartBuilder {
380    /// Set the resolution for each of the wings of the heart.
381    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    // This is where you should build the actual mesh.
400    fn build(&self) -> Mesh {
401        let radius = self.heart.radius;
402        // The curved parts of each wing (half) of the heart have an angle of `PI * 1.25` or 225°
403        let wing_angle = PI * 1.25;
404
405        // We create buffers for the vertices, their normals and UVs, as well as the indices used to connect the vertices.
406        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        // Since the heart is flat, we know all the normals are identical already.
410        let normals = vec![[0f32, 0f32, 1f32]; 2 * self.resolution];
411
412        // The point in the middle of the two curved parts of the heart
413        vertices.push([0.0; 3]);
414        uvs.push([0.5, 0.5]);
415
416        // The left wing of the heart, starting from the point in the middle.
417        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        // The bottom tip of the heart
425        vertices.push([0.0, radius * (-1. - SQRT_2), 0.0]);
426        uvs.push([0.5, 1.]);
427
428        // The right wing of the heart, starting from the bottom most point and going towards the middle point.
429        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        // This is where we build all the triangles from the points created above.
437        // Each triangle has one corner on the middle point with the other two being adjacent points on the perimeter of the heart.
438        for i in 2..2 * self.resolution as u32 {
439            indices.extend_from_slice(&[i - 1, i, 0]);
440        }
441
442        // Here, the actual `Mesh` is created. We set the indices, vertices, normals and UVs created above and specify the topology of the mesh.
443        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
454// The `Extrudable` trait can be used to easily implement meshing for extrusions.
455impl Extrudable for HeartMeshBuilder {
456    fn perimeter(&self) -> Vec<PerimeterSegment> {
457        let resolution = self.resolution as u32;
458        vec![
459            // The left wing of the heart
460            PerimeterSegment::Smooth {
461                // The normals of the first and last vertices of smooth segments have to be specified manually.
462                first_normal: Vec2::X,
463                last_normal: Vec2::new(-1.0, -1.0).normalize(),
464                // These indices are used to index into the `ATTRIBUTE_POSITION` vec of your 2D mesh.
465                indices: (0..resolution).collect(),
466            },
467            // The bottom tip of the heart
468            PerimeterSegment::Flat {
469                indices: vec![resolution - 1, resolution, resolution + 1],
470            },
471            // The right wing of the heart
472            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}