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