mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
0b3a34032a
# Objective This introduces a generalised 2d `Ring` shape for any underlying primitive (i.e. what an `Annulus` is to a `Circle`). This allows us to have "hollow" shapes or "outlines". `Ring` is also extrudable. It is assumed that the inner and outer meshes have the same number of vertices. ```rs let capsule_ring = Ring::new(Capsule2d::new(50.0, 100.0), Capsule2d::new(45.0, 100.0)); let hexagon_ring = Ring::new(RegularPolygon::new(50.0, 6), RegularPolygon::new(45.0, 6)); // note vertex count must match ``` ## Solution There is a new generic primitive `Ring`, which takes as input any `Primitive2d`, with two instances of that shape: the outer and the inner (or hollow). The mesh for a `RingMeshBuilder` is constructed by concatenating the vertices of the outer and inner meshes, then walking the perimeter to join corresponding vertices like so: <img width="513" height="509" alt="image" src="https://github.com/user-attachments/assets/2cecb458-3b59-44fb-858b-1beffecd1e57" /> ``` # outer vertices, then inner vertices positions = [ 0 1 2 3 4 0' 1' 2' 3' 4' ] # pairs of triangles indices = [ 0 1 0' 0' 1 1' 1 2 1' 1' 2 2' 2 3 2' 2' 3 3' 3 4 3' 3' 4 4' 4 0 4' 4' 0 0' ] ``` Examples of generated meshes: <img width="398" height="351" alt="image" src="https://github.com/user-attachments/assets/348bbd91-9f4e-4040-bfa5-d508a4308c10" /> <img width="472" height="376" alt="image" src="https://github.com/user-attachments/assets/dbaf894e-6f7f-4b79-af3e-69516da85898" /> <img width="388" height="357" alt="image" src="https://github.com/user-attachments/assets/cb9881e5-4518-4743-b8de-5816b632f36f" /> <img width="449" height="402" alt="image" src="https://github.com/user-attachments/assets/7d2022c9-b8cf-4b4b-bb09-cbe4fe49fb89" /> ## Testing I've tested these changes by updating the `2d_shapes`, `3d_shapes` and `custom_primitives` examples. It could potentially benefit from unit tests. --- ## Showcase <img width="1282" height="752" alt="image" src="https://github.com/user-attachments/assets/edab9dbf-1093-43c7-9804-8e5c8a830573" /> _Rings of 2d primitives (bottom row)_ <img width="1282" height="752" alt="image" src="https://github.com/user-attachments/assets/fbeed7f9-42bb-432c-bce9-cfeca87d70af" /> _Extrusions of rings of extrudable primitives (back row)_ --- ## Follow-up work I've only realised this from looking at Extrudable, but because I used the mesh positions but it does assume the positions are well-ordered around the perimeter. Extrudable instead uses the notion of a perimeter (via indices so it doesn't matter what order the mesh positions are in), a follow-up may be to do something similar for Ring. An alternative idea may be to compute the perimeter first as directly a list of Vec2 positions (maybe a Perimeter trait), then construct any needed meshes from that. This potentially makes `Annulus` redundant as it is equivalent to a `Ring<Circle>`. One thing of note is that `Extrusion<Annulus>` is textured differently from `Extrusion<Ring<Circle>>`. Another idea is to have a way to construct `PrimitiveTopology::LineList` meshes from Primitive shapes (which may have a similar effect as creating a Ring).
171 lines
6.9 KiB
Rust
171 lines
6.9 KiB
Rust
//! Here we use shape primitives to build meshes in a 2D rendering context, making each mesh a certain color by giving that mesh's entity a material based off a [`Color`].
|
||
//!
|
||
//! Meshes are better known for their use in 3D rendering, but we can use them in a 2D context too. Without a third dimension, the meshes we're building are flat – like paper on a table. These are still very useful for "vector-style" graphics, picking behavior, or as a foundation to build off of for where to apply a shader.
|
||
//!
|
||
//! A "shape definition" is not a mesh on its own. A circle can be defined with a radius, i.e. [`Circle::new(50.0)`][Circle::new], but rendering tends to happen with meshes built out of triangles. So we need to turn shape descriptions into meshes.
|
||
//!
|
||
//! Thankfully, we can add shape primitives directly to [`Assets<Mesh>`] because [`Mesh`] implements [`From`] for shape primitives and [`Assets<T>::add`] can be given any value that can be "turned into" `T`!
|
||
//!
|
||
//! We apply a material to the shape by first making a [`Color`] then calling [`Assets<ColorMaterial>::add`] with that color as its argument, which will create a material from that color through the same process [`Assets<Mesh>::add`] can take a shape primitive.
|
||
//!
|
||
//! Both the mesh and material need to be wrapped in their own "newtypes". The mesh and material are currently [`Handle<Mesh>`] and [`Handle<ColorMaterial>`] at the moment, which are not components. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh2d`] and [`MeshMaterial2d`] respectively.
|
||
//!
|
||
//! You can toggle wireframes with the space bar except on wasm. Wasm does not support
|
||
//! `POLYGON_MODE_LINE` on the gpu.
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
use bevy::{
|
||
input::common_conditions::input_just_pressed,
|
||
sprite_render::{Wireframe2dConfig, Wireframe2dPlugin},
|
||
};
|
||
use bevy::{input::common_conditions::input_toggle_active, prelude::*};
|
||
|
||
fn main() {
|
||
let mut app = App::new();
|
||
app.add_plugins((
|
||
DefaultPlugins,
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
Wireframe2dPlugin::default(),
|
||
))
|
||
.add_systems(Startup, setup);
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
app.add_systems(
|
||
Update,
|
||
toggle_wireframe.run_if(input_just_pressed(KeyCode::Space)),
|
||
);
|
||
app.add_systems(
|
||
Update,
|
||
rotate.run_if(input_toggle_active(false, KeyCode::KeyR)),
|
||
);
|
||
app.run();
|
||
}
|
||
|
||
const X_EXTENT: f32 = 1000.;
|
||
const Y_EXTENT: f32 = 150.;
|
||
const THICKNESS: f32 = 5.0;
|
||
|
||
fn setup(
|
||
mut commands: Commands,
|
||
mut meshes: ResMut<Assets<Mesh>>,
|
||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||
) {
|
||
commands.spawn(Camera2d);
|
||
|
||
let shapes = [
|
||
meshes.add(Circle::new(50.0)),
|
||
meshes.add(CircularSector::new(50.0, 1.0)),
|
||
meshes.add(CircularSegment::new(50.0, 1.25)),
|
||
meshes.add(Ellipse::new(25.0, 50.0)),
|
||
meshes.add(Annulus::new(25.0, 50.0)),
|
||
meshes.add(Capsule2d::new(25.0, 50.0)),
|
||
meshes.add(Rhombus::new(75.0, 100.0)),
|
||
meshes.add(Rectangle::new(50.0, 100.0)),
|
||
meshes.add(RegularPolygon::new(50.0, 6)),
|
||
meshes.add(Triangle2d::new(
|
||
Vec2::Y * 50.0,
|
||
Vec2::new(-50.0, -50.0),
|
||
Vec2::new(50.0, -50.0),
|
||
)),
|
||
meshes.add(Segment2d::new(
|
||
Vec2::new(-50.0, 50.0),
|
||
Vec2::new(50.0, -50.0),
|
||
)),
|
||
meshes.add(Polyline2d::new(vec![
|
||
Vec2::new(-50.0, 50.0),
|
||
Vec2::new(0.0, -50.0),
|
||
Vec2::new(50.0, 50.0),
|
||
])),
|
||
];
|
||
let num_shapes = shapes.len();
|
||
|
||
for (i, shape) in shapes.into_iter().enumerate() {
|
||
// Distribute colors evenly across the rainbow.
|
||
let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
|
||
|
||
commands.spawn((
|
||
Mesh2d(shape),
|
||
MeshMaterial2d(materials.add(color)),
|
||
Transform::from_xyz(
|
||
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
|
||
-X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
|
||
Y_EXTENT / 2.,
|
||
0.0,
|
||
),
|
||
));
|
||
}
|
||
|
||
let rings = [
|
||
meshes.add(Circle::new(50.0).to_ring(THICKNESS)),
|
||
// this visually produces an arc segment but this is not technically accurate
|
||
meshes.add(Ring::new(
|
||
CircularSector::new(50.0, 1.0),
|
||
CircularSector::new(45.0, 1.0),
|
||
)),
|
||
meshes.add(CircularSegment::new(50.0, 1.25).to_ring(THICKNESS)),
|
||
meshes.add({
|
||
// This is an approximation; Ellipse does not implement Inset as concentric ellipses do not have parallel curves
|
||
let outer = Ellipse::new(25.0, 50.0);
|
||
let mut inner = outer;
|
||
inner.half_size -= Vec2::splat(THICKNESS);
|
||
Ring::new(outer, inner)
|
||
}),
|
||
// this is equivalent to the Annulus::new(25.0, 50.0) above
|
||
meshes.add(Ring::new(Circle::new(50.0), Circle::new(25.0))),
|
||
meshes.add(Capsule2d::new(25.0, 50.0).to_ring(THICKNESS)),
|
||
meshes.add(Rhombus::new(75.0, 100.0).to_ring(THICKNESS)),
|
||
meshes.add(Rectangle::new(50.0, 100.0).to_ring(THICKNESS)),
|
||
meshes.add(RegularPolygon::new(50.0, 6).to_ring(THICKNESS)),
|
||
meshes.add(
|
||
Triangle2d::new(
|
||
Vec2::Y * 50.0,
|
||
Vec2::new(-50.0, -50.0),
|
||
Vec2::new(50.0, -50.0),
|
||
)
|
||
.to_ring(THICKNESS),
|
||
),
|
||
];
|
||
// Allow for 2 empty spaces
|
||
let num_rings = rings.len() + 2;
|
||
|
||
for (i, shape) in rings.into_iter().enumerate() {
|
||
// Distribute colors evenly across the rainbow.
|
||
let color = Color::hsl(360. * i as f32 / num_rings as f32, 0.95, 0.7);
|
||
|
||
commands.spawn((
|
||
Mesh2d(shape),
|
||
MeshMaterial2d(materials.add(color)),
|
||
Transform::from_xyz(
|
||
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
|
||
-X_EXTENT / 2. + i as f32 / (num_rings - 1) as f32 * X_EXTENT,
|
||
-Y_EXTENT / 2.,
|
||
0.0,
|
||
),
|
||
));
|
||
}
|
||
|
||
let mut text = "Press 'R' to pause/resume rotation".to_string();
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
text.push_str("\nPress 'Space' to toggle wireframes");
|
||
|
||
commands.spawn((
|
||
Text::new(text),
|
||
Node {
|
||
position_type: PositionType::Absolute,
|
||
top: px(12),
|
||
left: px(12),
|
||
..default()
|
||
},
|
||
));
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn toggle_wireframe(mut wireframe_config: ResMut<Wireframe2dConfig>) {
|
||
wireframe_config.global = !wireframe_config.global;
|
||
}
|
||
|
||
fn rotate(mut query: Query<&mut Transform, With<Mesh2d>>, time: Res<Time>) {
|
||
for mut transform in &mut query {
|
||
transform.rotate_z(time.delta_secs() / 2.0);
|
||
}
|
||
}
|