bevy/examples/math/custom_primitives.rs
Joona Aalto 54006b107b
Migrate meshes and materials to required components (#15524)
# Objective

A big step in the migration to required components: meshes and
materials!

## Solution

As per the [selected
proposal](https://hackmd.io/@bevy/required_components/%2Fj9-PnF-2QKK0on1KQ29UWQ):

- Deprecate `MaterialMesh2dBundle`, `MaterialMeshBundle`, and
`PbrBundle`.
- Add `Mesh2d` and `Mesh3d` components, which wrap a `Handle<Mesh>`.
- Add `MeshMaterial2d<M: Material2d>` and `MeshMaterial3d<M: Material>`,
which wrap a `Handle<M>`.
- Meshes *without* a mesh material should be rendered with a default
material. The existence of a material is determined by
`HasMaterial2d`/`HasMaterial3d`, which is required by
`MeshMaterial2d`/`MeshMaterial3d`. This gets around problems with the
generics.

Previously:

```rust
commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle::new(100.0)).into(),
    material: materials.add(Color::srgb(7.5, 0.0, 7.5)),
    transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ..default()
});
```

Now:

```rust
commands.spawn((
    Mesh2d(meshes.add(Circle::new(100.0))),
    MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
    Transform::from_translation(Vec3::new(-200., 0., 0.)),
));
```

If the mesh material is missing, previously nothing was rendered. Now,
it renders a white default `ColorMaterial` in 2D and a
`StandardMaterial` in 3D (this can be overridden). Below, only every
other entity has a material:

![Näyttökuva 2024-09-29
181746](https://github.com/user-attachments/assets/5c8be029-d2fe-4b8c-ae89-17a72ff82c9a)

![Näyttökuva 2024-09-29
181918](https://github.com/user-attachments/assets/58adbc55-5a1e-4c7d-a2c7-ed456227b909)

Why white? This is still open for discussion, but I think white makes
sense for a *default* material, while *invalid* asset handles pointing
to nothing should have something like a pink material to indicate that
something is broken (I don't handle that in this PR yet). This is kind
of a mix of Godot and Unity: Godot just renders a white material for
non-existent materials, while Unity renders nothing when no materials
exist, but renders pink for invalid materials. I can also change the
default material to pink if that is preferable though.

## Testing

I ran some 2D and 3D examples to test if anything changed visually. I
have not tested all examples or features yet however. If anyone wants to
test more extensively, it would be appreciated!

## Implementation Notes

- The relationship between `bevy_render` and `bevy_pbr` is weird here.
`bevy_render` needs `Mesh3d` for its own systems, but `bevy_pbr` has all
of the material logic, and `bevy_render` doesn't depend on it. I feel
like the two crates should be refactored in some way, but I think that's
out of scope for this PR.
- I didn't migrate meshlets to required components yet. That can
probably be done in a follow-up, as this is already a huge PR.
- It is becoming increasingly clear to me that we really, *really* want
to disallow raw asset handles as components. They caused me a *ton* of
headache here already, and it took me a long time to find every place
that queried for them or inserted them directly on entities, since there
were no compiler errors for it. If we don't remove the `Component`
derive, I expect raw asset handles to be a *huge* footgun for users as
we transition to wrapper components, especially as handles as components
have been the norm so far. I personally consider this to be a blocker
for 0.15: we need to migrate to wrapper components for asset handles
everywhere, and remove the `Component` derive. Also see
https://github.com/bevyengine/bevy/issues/14124.

---

## Migration Guide

Asset handles for meshes and mesh materials must now be wrapped in the
`Mesh2d` and `MeshMaterial2d` or `Mesh3d` and `MeshMaterial3d`
components for 2D and 3D respectively. Raw handles as components no
longer render meshes.

Additionally, `MaterialMesh2dBundle`, `MaterialMeshBundle`, and
`PbrBundle` have been deprecated. Instead, use the mesh and material
components directly.

Previously:

```rust
commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle::new(100.0)).into(),
    material: materials.add(Color::srgb(7.5, 0.0, 7.5)),
    transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ..default()
});
```

Now:

```rust
commands.spawn((
    Mesh2d(meshes.add(Circle::new(100.0))),
    MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
    Transform::from_translation(Vec3::new(-200., 0., 0.)),
));
```

If the mesh material is missing, a white default material is now used.
Previously, nothing was rendered if the material was missing.

The `WithMesh2d` and `WithMesh3d` query filter type aliases have also
been removed. Simply use `With<Mesh2d>` or `With<Mesh3d>`.

---------

Co-authored-by: Tim Blackbird <justthecooldude@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
2024-10-01 21:33:17 +00:00

497 lines
18 KiB
Rust

//! This example demonstrates how you can add your own custom primitives to bevy highlighting
//! traits you may want to implement for your primitives to achieve different functionalities.
use std::f32::consts::{PI, SQRT_2};
use bevy::{
color::palettes::css::{RED, WHITE},
input::common_conditions::input_just_pressed,
math::{
bounding::{
Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume,
},
Isometry2d,
},
prelude::*,
render::{
camera::ScalingMode,
mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment},
render_asset::RenderAssetUsages,
},
};
const HEART: Heart = Heart::new(0.5);
const EXTRUSION: Extrusion<Heart> = Extrusion {
base_shape: Heart::new(0.5),
half_depth: 0.5,
};
// The transform of the camera in 2D
const TRANSFORM_2D: Transform = Transform {
translation: Vec3::ZERO,
rotation: Quat::IDENTITY,
scale: Vec3::ONE,
};
// The projection used for the camera in 2D
const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection {
near: -1.0,
far: 10.0,
viewport_origin: Vec2::new(0.5, 0.5),
scaling_mode: ScalingMode::AutoMax {
max_width: 8.0,
max_height: 20.0,
},
area: Rect {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
},
});
// The transform of the camera in 3D
const TRANSFORM_3D: Transform = Transform {
translation: Vec3::ZERO,
// The camera is pointing at the 3D shape
rotation: Quat::from_xyzw(-0.14521316, -0.0, -0.0, 0.98940045),
scale: Vec3::ONE,
};
// The projection used for the camera in 3D
const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection {
fov: PI / 4.0,
near: 0.1,
far: 1000.0,
aspect_ratio: 1.0,
});
/// State for tracking the currently displayed shape
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum CameraActive {
#[default]
/// The 2D shape is displayed
Dim2,
/// The 3D shape is displayed
Dim3,
}
/// State for tracking the currently displayed shape
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum BoundingShape {
#[default]
/// No bounding shapes
None,
/// The bounding sphere or circle of the shape
BoundingSphere,
/// The Axis Aligned Bounding Box (AABB) of the shape
BoundingBox,
}
/// A marker component for our 2D shapes so we can query them separately from the camera
#[derive(Component)]
struct Shape2d;
/// A marker component for our 3D shapes so we can query them separately from the camera
#[derive(Component)]
struct Shape3d;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<BoundingShape>()
.init_state::<CameraActive>()
.add_systems(Startup, setup)
.add_systems(
Update,
(
(rotate_2d_shapes, bounding_shapes_2d).run_if(in_state(CameraActive::Dim2)),
(rotate_3d_shapes, bounding_shapes_3d).run_if(in_state(CameraActive::Dim3)),
update_bounding_shape.run_if(input_just_pressed(KeyCode::KeyB)),
switch_cameras.run_if(input_just_pressed(KeyCode::Space)),
),
)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawn the camera
commands.spawn(Camera3dBundle {
transform: TRANSFORM_2D,
projection: PROJECTION_2D,
..Default::default()
});
// Spawn the 2D heart
commands.spawn((
// We can use the methods defined on the meshbuilder to customize the mesh.
Mesh3d(meshes.add(HEART.mesh().resolution(50))),
MeshMaterial3d(materials.add(StandardMaterial {
emissive: RED.into(),
base_color: RED.into(),
..Default::default()
})),
Transform::from_xyz(0.0, 0.0, 0.0),
Shape2d,
));
// Spawn an extrusion of the heart.
commands.spawn((
// We can set a custom resolution for the round parts of the extrusion aswell.
Mesh3d(meshes.add(EXTRUSION.mesh().resolution(50))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED.into(),
..Default::default()
})),
Transform::from_xyz(0., -3., -10.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
Shape3d,
));
// Point light for 3D
commands.spawn((
PointLight {
shadows_enabled: true,
intensity: 10_000_000.,
range: 100.0,
shadow_depth_bias: 0.2,
..default()
},
Transform::from_xyz(8.0, 12.0, 1.0),
));
// Example instructions
commands.spawn(
TextBundle::from_section(
"Press 'B' to toggle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
Press 'Space' to switch between 3D and 2D",
TextStyle::default(),
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
}
// Rotate the 2D shapes.
fn rotate_2d_shapes(mut shapes: Query<&mut Transform, With<Shape2d>>, time: Res<Time>) {
let elapsed_seconds = time.elapsed_seconds();
for mut transform in shapes.iter_mut() {
transform.rotation = Quat::from_rotation_z(elapsed_seconds);
}
}
// Draw bounding boxes or circles for the 2D shapes.
fn bounding_shapes_2d(
shapes: Query<&Transform, With<Shape2d>>,
mut gizmos: Gizmos,
bounding_shape: Res<State<BoundingShape>>,
) {
for transform in shapes.iter() {
// Get the rotation angle from the 3D rotation.
let rotation = transform.rotation.to_scaled_axis().z;
let rotation = Rot2::radians(rotation);
let isometry = Isometry2d::new(transform.translation.xy(), rotation);
match bounding_shape.get() {
BoundingShape::None => (),
BoundingShape::BoundingBox => {
// Get the AABB of the primitive with the rotation and translation of the mesh.
let aabb = HEART.aabb_2d(isometry);
gizmos.rect_2d(
Isometry2d::from_translation(aabb.center()),
aabb.half_size() * 2.,
WHITE,
);
}
BoundingShape::BoundingSphere => {
// Get the bounding sphere of the primitive with the rotation and translation of the mesh.
let bounding_circle = HEART.bounding_circle(isometry);
gizmos
.circle_2d(
Isometry2d::from_translation(bounding_circle.center()),
bounding_circle.radius(),
WHITE,
)
.resolution(64);
}
}
}
}
// Rotate the 3D shapes.
fn rotate_3d_shapes(mut shapes: Query<&mut Transform, With<Shape3d>>, time: Res<Time>) {
let delta_seconds = time.delta_seconds();
for mut transform in shapes.iter_mut() {
transform.rotate_y(delta_seconds);
}
}
// Draw the AABBs or bounding spheres for the 3D shapes.
fn bounding_shapes_3d(
shapes: Query<&Transform, With<Shape3d>>,
mut gizmos: Gizmos,
bounding_shape: Res<State<BoundingShape>>,
) {
for transform in shapes.iter() {
match bounding_shape.get() {
BoundingShape::None => (),
BoundingShape::BoundingBox => {
// Get the AABB of the extrusion with the rotation and translation of the mesh.
let aabb = EXTRUSION.aabb_3d(transform.to_isometry());
gizmos.primitive_3d(
&Cuboid::from_size(Vec3::from(aabb.half_size()) * 2.),
Isometry3d::from_translation(aabb.center()),
WHITE,
);
}
BoundingShape::BoundingSphere => {
// Get the bounding sphere of the extrusion with the rotation and translation of the mesh.
let bounding_sphere = EXTRUSION.bounding_sphere(transform.to_isometry());
gizmos.sphere(
Isometry3d::from_translation(bounding_sphere.center()),
bounding_sphere.radius(),
WHITE,
);
}
}
}
}
// Switch to the next bounding shape.
fn update_bounding_shape(
current: Res<State<BoundingShape>>,
mut next: ResMut<NextState<BoundingShape>>,
) {
next.set(match current.get() {
BoundingShape::None => BoundingShape::BoundingBox,
BoundingShape::BoundingBox => BoundingShape::BoundingSphere,
BoundingShape::BoundingSphere => BoundingShape::None,
});
}
// Switch between 2D and 3D cameras.
fn switch_cameras(
current: Res<State<CameraActive>>,
mut next: ResMut<NextState<CameraActive>>,
mut camera: Query<(&mut Transform, &mut Projection)>,
) {
let next_state = match current.get() {
CameraActive::Dim2 => CameraActive::Dim3,
CameraActive::Dim3 => CameraActive::Dim2,
};
next.set(next_state);
let (mut transform, mut projection) = camera.single_mut();
match next_state {
CameraActive::Dim2 => {
*transform = TRANSFORM_2D;
*projection = PROJECTION_2D;
}
CameraActive::Dim3 => {
*transform = TRANSFORM_3D;
*projection = PROJECTION_3D;
}
};
}
/// A custom 2D heart primitive. The heart is made up of two circles centered at `Vec2::new(±radius, 0.)` each with the same `radius`.
///
/// The tip of the heart connects the two circles at a 45° angle from `Vec3::NEG_Y`.
#[derive(Copy, Clone)]
struct Heart {
/// The radius of each wing of the heart
radius: f32,
}
// The `Primitive2d` or `Primitive3d` trait is required by almost all other traits for primitives in bevy.
// Depending on your shape, you should implement either one of them.
impl Primitive2d for Heart {}
impl Heart {
const fn new(radius: f32) -> Self {
Self { radius }
}
}
// The `Measured2d` and `Measured3d` traits are used to compute the perimeter, the area or the volume of a primitive.
// If you implement `Measured2d` for a 2D primitive, `Measured3d` is automatically implemented for `Extrusion<T>`.
impl Measured2d for Heart {
fn perimeter(&self) -> f32 {
self.radius * (2.5 * PI + ops::powf(2f32, 1.5) + 2.0)
}
fn area(&self) -> f32 {
let circle_area = PI * self.radius * self.radius;
let triangle_area = self.radius * self.radius * (1.0 + 2f32.sqrt()) / 2.0;
let cutout = triangle_area - circle_area * 3.0 / 16.0;
2.0 * circle_area + 4.0 * cutout
}
}
// The `Bounded2d` or `Bounded3d` traits are used to compute the Axis Aligned Bounding Boxes or bounding circles / spheres for primitives.
impl Bounded2d for Heart {
fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d {
// The center of the circle at the center of the right wing of the heart
let circle_center = isometry.rotation * Vec2::new(self.radius, 0.0);
// The maximum X and Y positions of the two circles of the wings of the heart.
let max_circle = circle_center.abs() + Vec2::splat(self.radius);
// Since the two circles of the heart are mirrored around the origin, the minimum position is the negative of the maximum.
let min_circle = -max_circle;
// The position of the tip at the bottom of the heart
let tip_position = isometry.rotation * Vec2::new(0.0, -self.radius * (1. + SQRT_2));
Aabb2d {
min: isometry.translation + min_circle.min(tip_position),
max: isometry.translation + max_circle.max(tip_position),
}
}
fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle {
// 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.
let offset = self.radius / ops::powf(2f32, 1.5);
// The center of the bounding circle
let center = isometry * Vec2::new(0.0, -offset);
// The radius of the bounding circle
let radius = self.radius * (1.0 + 2f32.sqrt()) - offset;
BoundingCircle::new(center, radius)
}
}
// You can implement the `BoundedExtrusion` trait to implement `Bounded3d for Extrusion<Heart>`. There is a default implementation for both AABBs and bounding spheres,
// but you may be able to find faster solutions for your specific primitives.
impl BoundedExtrusion for Heart {}
// You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
impl Meshable for Heart {
// The meshbuilder can be used to create the actual mesh for that primitive.
type Output = HeartMeshBuilder;
fn mesh(&self) -> Self::Output {
Self::Output {
heart: *self,
resolution: 32,
}
}
}
// You can include any additional information needed for meshing the primitive in the meshbuilder.
struct HeartMeshBuilder {
heart: Heart,
// The resolution determines the amount of vertices used for each wing of the heart
resolution: usize,
}
// This trait is needed so that the configuration methods of the builder of the primitive are also available for the builder for the extrusion.
// If you do not want to support these configuration options for extrusions you can just implement them for your 2D mesh builder.
trait HeartBuilder {
/// Set the resolution for each of the wings of the heart.
fn resolution(self, resolution: usize) -> Self;
}
impl HeartBuilder for HeartMeshBuilder {
fn resolution(mut self, resolution: usize) -> Self {
self.resolution = resolution;
self
}
}
impl HeartBuilder for ExtrusionBuilder<Heart> {
fn resolution(mut self, resolution: usize) -> Self {
self.base_builder.resolution = resolution;
self
}
}
impl MeshBuilder for HeartMeshBuilder {
// This is where you should build the actual mesh.
fn build(&self) -> Mesh {
let radius = self.heart.radius;
// The curved parts of each wing (half) of the heart have an angle of `PI * 1.25` or 225°
let wing_angle = PI * 1.25;
// We create buffers for the vertices, their normals and UVs, as well as the indices used to connect the vertices.
let mut vertices = Vec::with_capacity(2 * self.resolution);
let mut uvs = Vec::with_capacity(2 * self.resolution);
let mut indices = Vec::with_capacity(6 * self.resolution - 9);
// Since the heart is flat, we know all the normals are identical already.
let normals = vec![[0f32, 0f32, 1f32]; 2 * self.resolution];
// The point in the middle of the two curved parts of the heart
vertices.push([0.0; 3]);
uvs.push([0.5, 0.5]);
// The left wing of the heart, starting from the point in the middle.
for i in 1..self.resolution {
let angle = (i as f32 / self.resolution as f32) * wing_angle;
let (sin, cos) = ops::sin_cos(angle);
vertices.push([radius * (cos - 1.0), radius * sin, 0.0]);
uvs.push([0.5 - (cos - 1.0) / 4., 0.5 - sin / 2.]);
}
// The bottom tip of the heart
vertices.push([0.0, radius * (-1. - SQRT_2), 0.0]);
uvs.push([0.5, 1.]);
// The right wing of the heart, starting from the bottom most point and going towards the middle point.
for i in 0..self.resolution - 1 {
let angle = (i as f32 / self.resolution as f32) * wing_angle - PI / 4.;
let (sin, cos) = ops::sin_cos(angle);
vertices.push([radius * (cos + 1.0), radius * sin, 0.0]);
uvs.push([0.5 - (cos + 1.0) / 4., 0.5 - sin / 2.]);
}
// This is where we build all the triangles from the points created above.
// Each triangle has one corner on the middle point with the other two being adjacent points on the perimeter of the heart.
for i in 2..2 * self.resolution as u32 {
indices.extend_from_slice(&[i - 1, i, 0]);
}
// Here, the actual `Mesh` is created. We set the indices, vertices, normals and UVs created above and specify the topology of the mesh.
Mesh::new(
bevy::render::mesh::PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_indices(bevy::render::mesh::Indices::U32(indices))
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
}
}
// The `Extrudable` trait can be used to easily implement meshing for extrusions.
impl Extrudable for HeartMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
let resolution = self.resolution as u32;
vec![
// The left wing of the heart
PerimeterSegment::Smooth {
// The normals of the first and last vertices of smooth segments have to be specified manually.
first_normal: Vec2::X,
last_normal: Vec2::new(-1.0, -1.0).normalize(),
// These indices are used to index into the `ATTRIBUTE_POSITION` vec of your 2D mesh.
indices: (0..resolution).collect(),
},
// The bottom tip of the heart
PerimeterSegment::Flat {
indices: vec![resolution - 1, resolution, resolution + 1],
},
// The right wing of the heart
PerimeterSegment::Smooth {
first_normal: Vec2::new(1.0, -1.0).normalize(),
last_normal: Vec2::NEG_X,
indices: (resolution + 1..2 * resolution).chain([0]).collect(),
},
]
}
}