Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/math/custom_primitives.rs
6592 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
use 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
21
const HEART: Heart = Heart::new(0.5);
22
const 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
28
const 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
34
const 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
50
const 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
57
const 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)]
66
enum 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)]
76
enum 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)]
88
struct Shape2d;
89
90
/// A marker component for our 3D shapes so we can query them separately from the camera
91
#[derive(Component)]
92
struct Shape3d;
93
94
fn 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
112
fn 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.
171
fn 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.
180
fn 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.
210
fn 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.
219
fn 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.
248
fn 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.
260
fn 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)]
288
struct 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.
295
impl Primitive2d for Heart {}
296
297
impl 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>`.
305
impl 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.
320
impl 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.
355
impl BoundedExtrusion for Heart {}
356
357
// You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
358
impl 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`.
371
struct 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`.
379
trait HeartBuilder {
380
/// Set the resolution for each of the wings of the heart.
381
fn resolution(self, resolution: usize) -> Self;
382
}
383
384
impl HeartBuilder for HeartMeshBuilder {
385
fn resolution(mut self, resolution: usize) -> Self {
386
self.resolution = resolution;
387
self
388
}
389
}
390
391
impl HeartBuilder for ExtrusionBuilder<Heart> {
392
fn resolution(mut self, resolution: usize) -> Self {
393
self.base_builder.resolution = resolution;
394
self
395
}
396
}
397
398
impl 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.
455
impl 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
}
480
481