Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/math/custom_primitives.rs
9311 views
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::{
15
bounding::{
16
Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume,
17
},
18
Isometry2d,
19
},
20
mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment},
21
prelude::*,
22
};
23
24
const HEART: Heart = Heart::new(0.5);
25
const HOLLOW: Heart = Heart::new(0.3);
26
// By implementing these traits we can construct the 2D ring version of this shape
27
const RING: Ring<Heart> = Ring::new(HEART, HOLLOW);
28
// By implementing these traits we can construct the 3D extrusion of this shape
29
const EXTRUSION: Extrusion<Heart> = Extrusion {
30
base_shape: HEART,
31
half_depth: 0.5,
32
};
33
const 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
39
const 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
45
const 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
61
const 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
68
const 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)]
80
enum 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
92
impl 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)]
112
enum 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)]
124
struct Shape2d;
125
126
/// A marker component for our 3D shapes so we can query them separately from the camera
127
#[derive(Component)]
128
struct Shape3d;
129
130
fn 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
171
fn 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.
274
fn 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.
283
fn 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.
313
fn 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.
322
fn 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.
351
fn 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.
363
fn 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"))]
394
fn 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)]
402
struct 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.
409
impl Primitive2d for Heart {}
410
411
impl 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>`.
419
impl 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.
434
impl 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.
469
impl BoundedExtrusion for Heart {}
470
471
// You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
472
impl 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`.
485
struct 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`.
493
trait HeartBuilder {
494
/// Set the resolution for each of the wings of the heart.
495
fn resolution(self, resolution: usize) -> Self;
496
}
497
498
impl HeartBuilder for HeartMeshBuilder {
499
fn resolution(mut self, resolution: usize) -> Self {
500
self.resolution = resolution;
501
self
502
}
503
}
504
505
impl HeartBuilder for ExtrusionBuilder<Heart> {
506
fn resolution(mut self, resolution: usize) -> Self {
507
self.base_builder.resolution = resolution;
508
self
509
}
510
}
511
512
impl 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.
569
impl 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
596
fn 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
}
604
605