Meshable extrusions (#13478)

# Objective

- Implement `Meshable` for `Extrusion<T>`

## Solution

- `Meshable` requires `Meshable::Output: MeshBuilder` now. This means
that all `some_primitive.mesh()` calls now return a `MeshBuilder`. These
were added for primitives that did not have one prior to this.
- A new trait `Extrudable: MeshBuilder` has been added. This trait
allows you to specify the indices of the perimeter of the mesh created
by this `MeshBuilder` and whether they are to be shaded smooth or flat.
- `Extrusion<P: Primitive2d + Meshable>` is now `Meshable` aswell. The
associated `MeshBuilder` is `ExtrusionMeshBuilder` which is generic over
`P` and uses the `MeshBuilder` of its baseshape internally.
- `ExtrusionMeshBuilder` exposes the configuration functions of its
base-shapes builder.
- Updated the `3d_shapes` example to include `Extrusion`s

## Migration Guide

- Depending on the context, you may need to explicitly call
`.mesh().build()` on primitives where you have previously called
`.mesh()`
- The `Output` type of custom `Meshable` implementations must now derive
`MeshBuilder`.

## Additional information
- The extrusions UVs are done so that 
- the front face (`+Z`) is in the area between `(0, 0)` and `(0.5,
0.5)`,
- the back face (`-Z`) is in the area between `(0.5, 0)` and `(1, 0.5)`
- the mantle is in the area between `(0, 0.5)` and `(1, 1)`. Each
`PerimeterSegment` you specified in the `Extrudable` implementation will
be allocated an equal portion of this area.
- The UVs of the base shape are scaled to be in the front/back area so
whatever method of filling the full UV-space the base shape used is how
these areas will be filled.

Here is an example of what that looks like on a capsule:


https://github.com/bevyengine/bevy/assets/62256001/425ad288-fbbc-4634-9d3f-5e846cdce85f

This is the texture used:

![extrusion
uvs](https://github.com/bevyengine/bevy/assets/62256001/4e54e421-bfda-44b9-8571-412525cebddf)

The `3d_shapes` example now looks like this:


![image_2024-05-22_235915753](https://github.com/bevyengine/bevy/assets/62256001/3d8bc86d-9ed1-47f2-899a-27aac0a265dd)

---------

Co-authored-by: Lynn Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com>
Co-authored-by: Matty <weatherleymatthew@gmail.com>
Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
This commit is contained in:
Lynn 2024-06-04 19:27:32 +02:00 committed by GitHub
parent 5e1c841f4e
commit fd82ef8956
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 689 additions and 91 deletions

View File

@ -1,12 +1,11 @@
use std::f32::consts::FRAC_PI_2;
use crate::{
mesh::primitives::dim3::triangle3d,
mesh::{Indices, Mesh},
mesh::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment},
render_asset::RenderAssetUsages,
};
use super::{MeshBuilder, Meshable};
use super::{Extrudable, MeshBuilder, Meshable};
use bevy_math::{
primitives::{
Annulus, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Rectangle,
@ -57,7 +56,19 @@ impl CircleMeshBuilder {
impl MeshBuilder for CircleMeshBuilder {
fn build(&self) -> Mesh {
RegularPolygon::new(self.circle.radius, self.resolution).mesh()
RegularPolygon::new(self.circle.radius, self.resolution)
.mesh()
.build()
}
}
impl Extrudable for CircleMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
vec![PerimeterSegment::Smooth {
first_normal: Vec2::Y,
last_normal: Vec2::Y,
indices: (0..self.resolution as u32).chain([0]).collect(),
}]
}
}
@ -154,9 +165,10 @@ impl CircularSectorMeshBuilder {
self.uv_mode = uv_mode;
self
}
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
impl MeshBuilder for CircularSectorMeshBuilder {
fn build(&self) -> Mesh {
let mut indices = Vec::with_capacity((self.resolution - 1) * 3);
let mut positions = Vec::with_capacity(self.resolution + 1);
let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1];
@ -221,12 +233,6 @@ impl From<CircularSector> for Mesh {
}
}
impl From<CircularSectorMeshBuilder> for Mesh {
fn from(sector: CircularSectorMeshBuilder) -> Self {
sector.build()
}
}
/// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape.
///
/// The resulting mesh will have a UV-map such that the center of the circle is
@ -277,9 +283,10 @@ impl CircularSegmentMeshBuilder {
self.uv_mode = uv_mode;
self
}
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
impl MeshBuilder for CircularSegmentMeshBuilder {
fn build(&self) -> Mesh {
let mut indices = Vec::with_capacity((self.resolution - 1) * 3);
let mut positions = Vec::with_capacity(self.resolution + 1);
let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1];
@ -353,27 +360,43 @@ impl From<CircularSegment> for Mesh {
}
}
impl From<CircularSegmentMeshBuilder> for Mesh {
fn from(sector: CircularSegmentMeshBuilder) -> Self {
sector.build()
/// A builder used for creating a [`Mesh`] with a [`RegularPolygon`] shape.
pub struct RegularPolygonMeshBuilder {
circumradius: f32,
sides: usize,
}
impl Meshable for RegularPolygon {
type Output = RegularPolygonMeshBuilder;
fn mesh(&self) -> Self::Output {
Self::Output {
circumradius: self.circumcircle.radius,
sides: self.sides,
}
}
}
impl Meshable for RegularPolygon {
type Output = Mesh;
fn mesh(&self) -> Self::Output {
impl MeshBuilder for RegularPolygonMeshBuilder {
fn build(&self) -> Mesh {
// The ellipse mesh is just a regular polygon with two radii
Ellipse::new(self.circumcircle.radius, self.circumcircle.radius)
Ellipse::new(self.circumradius, self.circumradius)
.mesh()
.resolution(self.sides)
.build()
}
}
impl Extrudable for RegularPolygonMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
vec![PerimeterSegment::Flat {
indices: (0..self.sides as u32).chain([0]).collect(),
}]
}
}
impl From<RegularPolygon> for Mesh {
fn from(polygon: RegularPolygon) -> Self {
polygon.mesh()
polygon.mesh().build()
}
}
@ -453,6 +476,16 @@ impl MeshBuilder for EllipseMeshBuilder {
}
}
impl Extrudable for EllipseMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
vec![PerimeterSegment::Smooth {
first_normal: Vec2::Y,
last_normal: Vec2::Y,
indices: (0..self.resolution as u32).chain([0]).collect(),
}]
}
}
impl Meshable for Ellipse {
type Output = EllipseMeshBuilder;
@ -566,6 +599,24 @@ impl MeshBuilder for AnnulusMeshBuilder {
}
}
impl Extrudable for AnnulusMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
let vert_count = 2 * self.resolution as u32;
vec![
PerimeterSegment::Smooth {
first_normal: Vec2::NEG_Y,
last_normal: Vec2::NEG_Y,
indices: (0..vert_count).step_by(2).chain([0]).rev().collect(), // Inner hole
},
PerimeterSegment::Smooth {
first_normal: Vec2::Y,
last_normal: Vec2::Y,
indices: (1..vert_count).step_by(2).chain([1]).collect(), // Outer perimeter
},
]
}
}
impl Meshable for Annulus {
type Output = AnnulusMeshBuilder;
@ -583,10 +634,12 @@ impl From<Annulus> for Mesh {
}
}
impl Meshable for Rhombus {
type Output = Mesh;
pub struct RhombusMeshBuilder {
half_diagonals: Vec2,
}
fn mesh(&self) -> Self::Output {
impl MeshBuilder for RhombusMeshBuilder {
fn build(&self) -> Mesh {
let [hhd, vhd] = [self.half_diagonals.x, self.half_diagonals.y];
let positions = vec![
[hhd, 0.0, 0.0],
@ -609,17 +662,36 @@ impl Meshable for Rhombus {
}
}
impl From<Rhombus> for Mesh {
fn from(rhombus: Rhombus) -> Self {
rhombus.mesh()
impl Meshable for Rhombus {
type Output = RhombusMeshBuilder;
fn mesh(&self) -> Self::Output {
Self::Output {
half_diagonals: self.half_diagonals,
}
}
}
impl From<Rhombus> for Mesh {
fn from(rhombus: Rhombus) -> Self {
rhombus.mesh().build()
}
}
/// A builder used for creating a [`Mesh`] with a [`Triangle2d`] shape.
pub struct Triangle2dMeshBuilder {
triangle: Triangle2d,
}
impl Meshable for Triangle2d {
type Output = Mesh;
type Output = Triangle2dMeshBuilder;
fn mesh(&self) -> Self::Output {
let vertices_3d = self.vertices.map(|v| v.extend(0.));
Self::Output { triangle: *self }
}
}
impl MeshBuilder for Triangle2dMeshBuilder {
fn build(&self) -> Mesh {
let vertices_3d = self.triangle.vertices.map(|v| v.extend(0.));
let positions: Vec<_> = vertices_3d.into();
let normals = vec![[0.0, 0.0, 1.0]; 3];
@ -631,7 +703,7 @@ impl Meshable for Triangle2d {
))
.into();
let is_ccw = self.winding_order() == WindingOrder::CounterClockwise;
let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise;
let indices = if is_ccw {
Indices::U32(vec![0, 1, 2])
} else {
@ -649,16 +721,34 @@ impl Meshable for Triangle2d {
}
}
impl From<Triangle2d> for Mesh {
fn from(triangle: Triangle2d) -> Self {
triangle.mesh()
impl Extrudable for Triangle2dMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise;
if is_ccw {
vec![PerimeterSegment::Flat {
indices: vec![0, 1, 2, 0],
}]
} else {
vec![PerimeterSegment::Flat {
indices: vec![2, 1, 0, 2],
}]
}
}
}
impl Meshable for Rectangle {
type Output = Mesh;
impl From<Triangle2d> for Mesh {
fn from(triangle: Triangle2d) -> Self {
triangle.mesh().build()
}
}
fn mesh(&self) -> Self::Output {
/// A builder used for creating a [`Mesh`] with a [`Rectangle`] shape.
pub struct RectangleMeshBuilder {
half_size: Vec2,
}
impl MeshBuilder for RectangleMeshBuilder {
fn build(&self) -> Mesh {
let [hw, hh] = [self.half_size.x, self.half_size.y];
let positions = vec![
[hw, hh, 0.0],
@ -681,9 +771,27 @@ impl Meshable for Rectangle {
}
}
impl Extrudable for RectangleMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
vec![PerimeterSegment::Flat {
indices: vec![0, 1, 2, 3, 0],
}]
}
}
impl Meshable for Rectangle {
type Output = RectangleMeshBuilder;
fn mesh(&self) -> Self::Output {
RectangleMeshBuilder {
half_size: self.half_size,
}
}
}
impl From<Rectangle> for Mesh {
fn from(rectangle: Rectangle) -> Self {
rectangle.mesh()
rectangle.mesh().build()
}
}
@ -804,6 +912,32 @@ impl MeshBuilder for Capsule2dMeshBuilder {
}
}
impl Extrudable for Capsule2dMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
let resolution = self.resolution as u32;
let top_semi_indices = (0..resolution).collect();
let bottom_semi_indices = (resolution..(2 * resolution)).collect();
vec![
PerimeterSegment::Smooth {
first_normal: Vec2::X,
last_normal: Vec2::NEG_X,
indices: top_semi_indices,
}, // Top semi-circle
PerimeterSegment::Flat {
indices: vec![resolution - 1, resolution],
}, // Left edge
PerimeterSegment::Smooth {
first_normal: Vec2::NEG_X,
last_normal: Vec2::X,
indices: bottom_semi_indices,
}, // Bottom semi-circle
PerimeterSegment::Flat {
indices: vec![2 * resolution - 1, 0],
}, // Right edge
]
}
}
impl Meshable for Capsule2d {
type Output = Capsule2dMeshBuilder;

View File

@ -1,5 +1,5 @@
use crate::{
mesh::{Indices, Mesh, Meshable},
mesh::{Indices, Mesh, MeshBuilder, Meshable},
render_asset::RenderAssetUsages,
};
use bevy_math::{primitives::ConicalFrustum, Vec3};
@ -59,9 +59,10 @@ impl ConicalFrustumMeshBuilder {
self.segments = segments;
self
}
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
impl MeshBuilder for ConicalFrustumMeshBuilder {
fn build(&self) -> Mesh {
debug_assert!(self.resolution > 2);
debug_assert!(self.segments > 0);
@ -182,9 +183,3 @@ impl From<ConicalFrustum> for Mesh {
frustum.mesh().build()
}
}
impl From<ConicalFrustumMeshBuilder> for Mesh {
fn from(frustum: ConicalFrustumMeshBuilder) -> Self {
frustum.build()
}
}

View File

@ -1,15 +1,18 @@
use bevy_math::primitives::Cuboid;
use bevy_math::{primitives::Cuboid, Vec3};
use wgpu::PrimitiveTopology;
use crate::{
mesh::{Indices, Mesh, Meshable},
mesh::{Indices, Mesh, MeshBuilder, Meshable},
render_asset::RenderAssetUsages,
};
impl Meshable for Cuboid {
type Output = Mesh;
/// A builder used for creating a [`Mesh`] with a [`Cuboid`] shape.
pub struct CuboidMeshBuilder {
half_size: Vec3,
}
fn mesh(&self) -> Self::Output {
impl MeshBuilder for CuboidMeshBuilder {
fn build(&self) -> Mesh {
let min = -self.half_size;
let max = self.half_size;
@ -71,8 +74,18 @@ impl Meshable for Cuboid {
}
}
impl From<Cuboid> for Mesh {
fn from(cuboid: Cuboid) -> Self {
cuboid.mesh()
impl Meshable for Cuboid {
type Output = CuboidMeshBuilder;
fn mesh(&self) -> Self::Output {
CuboidMeshBuilder {
half_size: self.half_size,
}
}
}
impl From<Cuboid> for Mesh {
fn from(cuboid: Cuboid) -> Self {
cuboid.mesh().build()
}
}

View File

@ -1,20 +1,23 @@
use super::triangle3d;
use crate::{
mesh::{Indices, Mesh, Meshable},
mesh::{Indices, Mesh, MeshBuilder, Meshable},
render_asset::RenderAssetUsages,
};
use bevy_math::primitives::{Tetrahedron, Triangle3d};
use wgpu::PrimitiveTopology;
impl Meshable for Tetrahedron {
type Output = Mesh;
/// A builder used for creating a [`Mesh`] with a [`Tetrahedron`] shape.
pub struct TetrahedronMeshBuilder {
tetrahedron: Tetrahedron,
}
fn mesh(&self) -> Self::Output {
let mut faces: Vec<_> = self.faces().into();
impl MeshBuilder for TetrahedronMeshBuilder {
fn build(&self) -> Mesh {
let mut faces: Vec<_> = self.tetrahedron.faces().into();
// If the tetrahedron has negative orientation, reverse all the triangles so that
// they still face outward.
if self.signed_volume().is_sign_negative() {
if self.tetrahedron.signed_volume().is_sign_negative() {
faces.iter_mut().for_each(Triangle3d::reverse);
}
@ -48,8 +51,16 @@ impl Meshable for Tetrahedron {
}
}
impl From<Tetrahedron> for Mesh {
fn from(tetrahedron: Tetrahedron) -> Self {
tetrahedron.mesh()
impl Meshable for Tetrahedron {
type Output = TetrahedronMeshBuilder;
fn mesh(&self) -> Self::Output {
TetrahedronMeshBuilder { tetrahedron: *self }
}
}
impl From<Tetrahedron> for Mesh {
fn from(tetrahedron: Tetrahedron) -> Self {
tetrahedron.mesh().build()
}
}

View File

@ -2,19 +2,22 @@ use bevy_math::{primitives::Triangle3d, Vec3};
use wgpu::PrimitiveTopology;
use crate::{
mesh::{Indices, Mesh, Meshable},
mesh::{Indices, Mesh, MeshBuilder, Meshable},
render_asset::RenderAssetUsages,
};
impl Meshable for Triangle3d {
type Output = Mesh;
/// A builder used for creating a [`Mesh`] with a [`Triangle3d`] shape.
pub struct Triangle3dMeshBuilder {
triangle: Triangle3d,
}
fn mesh(&self) -> Self::Output {
let positions: Vec<_> = self.vertices.into();
let uvs: Vec<_> = uv_coords(self).into();
impl MeshBuilder for Triangle3dMeshBuilder {
fn build(&self) -> Mesh {
let positions: Vec<_> = self.triangle.vertices.into();
let uvs: Vec<_> = uv_coords(&self.triangle).into();
// Every vertex has the normal of the face of the triangle (or zero if the triangle is degenerate).
let normal: Vec3 = normal_vec(self);
let normal: Vec3 = normal_vec(&self.triangle);
let normals = vec![normal; 3];
let indices = Indices::U32(vec![0, 1, 2]);
@ -30,6 +33,14 @@ impl Meshable for Triangle3d {
}
}
impl Meshable for Triangle3d {
type Output = Triangle3dMeshBuilder;
fn mesh(&self) -> Self::Output {
Triangle3dMeshBuilder { triangle: *self }
}
}
/// The normal of a [`Triangle3d`] with zeroing so that a [`Vec3`] is always obtained for meshing.
#[inline]
pub(crate) fn normal_vec(triangle: &Triangle3d) -> Vec3 {
@ -86,7 +97,7 @@ pub(crate) fn uv_coords(triangle: &Triangle3d) -> [[f32; 2]; 3] {
impl From<Triangle3d> for Mesh {
fn from(triangle: Triangle3d) -> Self {
triangle.mesh()
triangle.mesh().build()
}
}

View File

@ -0,0 +1,391 @@
use bevy_math::{
primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d},
Vec2, Vec3,
};
use crate::mesh::{Indices, Mesh, VertexAttributeValues};
use super::{MeshBuilder, Meshable};
/// A type representing a segment of the perimeter of an extrudable mesh.
pub enum PerimeterSegment {
/// This segment of the perimeter will be shaded smooth.
///
/// This has the effect of rendering the segment's faces with softened edges, so it is appropriate for curved shapes.
///
/// The normals for the vertices that are part of this segment will be calculated based on the positions of their neighbours.
/// Each normal is interpolated between the normals of the two line segments connecting it with its neighbours.
/// Closer vertices have a stronger effect on the normal than more distant ones.
///
/// Since the vertices corresponding to the first and last indices do not have two neighbouring vertices, their normals must be provided manually.
Smooth {
/// The normal of the first vertex.
first_normal: Vec2,
/// The normal of the last vertex.
last_normal: Vec2,
/// A list of indices representing this segment of the perimeter of the mesh.
///
/// The indices must be ordered such that the *outside* of the mesh is to the right
/// when walking along the vertices of the mesh in the order provided by the indices.
///
/// For geometry to be rendered, you must provide at least two indices.
indices: Vec<u32>,
},
/// This segment of the perimeter will be shaded flat.
///
/// This has the effect of rendering the segment's faces with hard edges.
Flat {
/// A list of indices representing this segment of the perimeter of the mesh.
///
/// The indices must be ordered such that the *outside* of the mesh is to the right
/// when walking along the vertices of the mesh in the order provided by indices.
///
/// For geometry to be rendered, you must provide at least two indices.
indices: Vec<u32>,
},
}
impl PerimeterSegment {
/// Returns the amount of vertices each 'layer' of the extrusion should include for this perimeter segment.
///
/// A layer is the set of vertices sharing a common Z value or depth.
fn vertices_per_layer(&self) -> usize {
match self {
PerimeterSegment::Smooth { indices, .. } => indices.len(),
PerimeterSegment::Flat { indices } => 2 * (indices.len() - 1),
}
}
/// Returns the amount of indices each 'segment' of the extrusion should include for this perimeter segment.
///
/// A segment is the set of faces on the mantel of the extrusion between two layers of vertices.
fn indices_per_segment(&self) -> usize {
match self {
PerimeterSegment::Smooth { indices, .. } | PerimeterSegment::Flat { indices } => {
6 * (indices.len() - 1)
}
}
}
}
/// A trait for required for implementing `Meshable` for `Extrusion<T>`.
///
/// ## Warning
///
/// By implementing this trait you guarantee that the `primitive_topology` of the mesh returned by
/// this builder is [`PrimitiveTopology::TriangleList`](wgpu::PrimitiveTopology::TriangleList)
/// and that your mesh has a [`Mesh::ATTRIBUTE_POSITION`] attribute.
pub trait Extrudable: MeshBuilder {
/// A list of the indices each representing a part of the perimeter of the mesh.
fn perimeter(&self) -> Vec<PerimeterSegment>;
}
impl<P> Meshable for Extrusion<P>
where
P: Primitive2d + Meshable,
P::Output: Extrudable,
{
type Output = ExtrusionBuilder<P>;
fn mesh(&self) -> Self::Output {
ExtrusionBuilder {
base_builder: self.base_shape.mesh(),
half_depth: self.half_depth,
}
}
}
/// A builder used for creating a [`Mesh`] with an [`Extrusion`] shape.
pub struct ExtrusionBuilder<P>
where
P: Primitive2d + Meshable,
P::Output: Extrudable,
{
base_builder: P::Output,
half_depth: f32,
}
impl<P> ExtrusionBuilder<P>
where
P: Primitive2d + Meshable,
P::Output: Extrudable,
{
/// Create a new `ExtrusionBuilder<P>` from a given `base_shape` and the full `depth` of the extrusion.
pub fn new(base_shape: &P, depth: f32) -> Self {
Self {
base_builder: base_shape.mesh(),
half_depth: depth / 2.,
}
}
}
impl ExtrusionBuilder<Circle> {
/// Sets the number of vertices used for the circle mesh at each end of the extrusion.
pub fn resolution(mut self, resolution: usize) -> Self {
self.base_builder.resolution = resolution;
self
}
}
impl ExtrusionBuilder<Ellipse> {
/// Sets the number of vertices used for the ellipse mesh at each end of the extrusion.
pub fn resolution(mut self, resolution: usize) -> Self {
self.base_builder.resolution = resolution;
self
}
}
impl ExtrusionBuilder<Annulus> {
/// Sets the number of vertices used in constructing the concentric circles of the annulus mesh at each end of the extrusion.
pub fn resolution(mut self, resolution: usize) -> Self {
self.base_builder.resolution = resolution;
self
}
}
impl ExtrusionBuilder<Capsule2d> {
/// Sets the number of vertices used for each hemicircle at the ends of the extrusion.
pub fn resolution(mut self, resolution: usize) -> Self {
self.base_builder.resolution = resolution;
self
}
}
impl<P> MeshBuilder for ExtrusionBuilder<P>
where
P: Primitive2d + Meshable,
P::Output: Extrudable,
{
fn build(&self) -> Mesh {
// Create and move the base mesh to the front
let mut front_face =
self.base_builder
.build()
.translated_by(Vec3::new(0., 0., self.half_depth));
// Move the uvs of the front face to be between (0., 0.) and (0.5, 0.5)
if let Some(VertexAttributeValues::Float32x2(uvs)) =
front_face.attribute_mut(Mesh::ATTRIBUTE_UV_0)
{
for uv in uvs {
*uv = uv.map(|coord| coord * 0.5);
}
}
let back_face = {
let topology = front_face.primitive_topology();
// Flip the normals, etc. and move mesh to the back
let mut back_face = front_face.clone().scaled_by(Vec3::new(1., 1., -1.));
// Move the uvs of the back face to be between (0.5, 0.) and (1., 0.5)
if let Some(VertexAttributeValues::Float32x2(uvs)) =
back_face.attribute_mut(Mesh::ATTRIBUTE_UV_0)
{
for uv in uvs {
*uv = [uv[0] + 0.5, uv[1]];
}
}
// By swapping the first and second indices of each triangle we invert the winding order thus making the mesh visible from the other side
if let Some(indices) = back_face.indices_mut() {
match topology {
wgpu::PrimitiveTopology::TriangleList => match indices {
Indices::U16(indices) => {
indices.chunks_exact_mut(3).for_each(|arr| arr.swap(1, 0));
}
Indices::U32(indices) => {
indices.chunks_exact_mut(3).for_each(|arr| arr.swap(1, 0));
}
},
_ => {
panic!("Meshes used with Extrusions must have a primitive topology of `PrimitiveTopology::TriangleList`");
}
};
}
back_face
};
// An extrusion of depth 0 does not need a mantel
if self.half_depth == 0. {
front_face.merge(back_face);
return front_face;
}
let mantel = {
let Some(VertexAttributeValues::Float32x3(cap_verts)) =
front_face.attribute(Mesh::ATTRIBUTE_POSITION)
else {
panic!("The base mesh did not have vertex positions");
};
let perimeter = self.base_builder.perimeter();
let (vert_count, index_count) =
perimeter
.iter()
.fold((0, 0), |(verts, indices), perimeter| {
(
verts + 2 * perimeter.vertices_per_layer(),
indices + perimeter.indices_per_segment(),
)
});
let mut positions = Vec::with_capacity(vert_count);
let mut normals = Vec::with_capacity(vert_count);
let mut indices = Vec::with_capacity(index_count);
let mut uvs = Vec::with_capacity(vert_count);
// Compute the amount of horizontal space allocated to each segment of the perimeter.
let uv_segment_delta = 1. / perimeter.len() as f32;
for (i, segment) in perimeter.into_iter().enumerate() {
// The start of the x range of the area of the current perimeter-segment.
let uv_start = i as f32 * uv_segment_delta;
match segment {
PerimeterSegment::Flat {
indices: segment_indices,
} => {
let uv_delta = uv_segment_delta / (segment_indices.len() - 1) as f32;
for i in 0..(segment_indices.len() - 1) {
let uv_x = uv_start + uv_delta * i as f32;
// Get the positions for the current and the next index.
let a = cap_verts[segment_indices[i] as usize];
let b = cap_verts[segment_indices[i + 1] as usize];
// Get the index of the next vertex added to the mantel.
let index = positions.len() as u32;
// Push the positions of the two indices and their equivalent points on the back face.
// This works, since the front face has already been moved to the correct depth.
positions.push(a);
positions.push(b);
positions.push([a[0], a[1], -a[2]]);
positions.push([b[0], b[1], -b[2]]);
// UVs for the mantel are between (0, 0.5) and (1, 1).
uvs.extend_from_slice(&[
[uv_x, 0.5],
[uv_x + uv_delta, 0.5],
[uv_x, 1.],
[uv_x + uv_delta, 1.],
]);
// The normal is calculated to be the normal of the line segment connecting a and b.
let n = Vec3::from_array([b[1] - a[1], a[0] - b[0], 0.])
.normalize_or_zero()
.to_array();
normals.extend_from_slice(&[n; 4]);
// Add the indices for the vertices created above to the mesh.
indices.extend_from_slice(&[
index,
index + 2,
index + 1,
index + 1,
index + 2,
index + 3,
]);
}
}
PerimeterSegment::Smooth {
first_normal,
last_normal,
indices: segment_indices,
} => {
let uv_delta = uv_segment_delta / (segment_indices.len() - 1) as f32;
// Since the indices for this segment will be added after its vertices have been added,
// we need to store the index of the first vertex that is part of this segment.
let base_index = positions.len() as u32;
// If there is a first vertex, we need to add it and its counterpart on the back face.
// The normal is provided by `segment.first_normal`.
if let Some(i) = segment_indices.first() {
let p = cap_verts[*i as usize];
positions.push(p);
positions.push([p[0], p[1], -p[2]]);
uvs.extend_from_slice(&[[uv_start, 0.5], [uv_start, 1.]]);
normals.extend_from_slice(&[first_normal.extend(0.).to_array(); 2]);
}
// For all points inbetween the first and last vertices, we can automatically compute the normals.
for i in 1..(segment_indices.len() - 1) {
let uv_x = uv_start + uv_delta * i as f32;
// Get the positions for the last, current and the next index.
let a = cap_verts[segment_indices[i - 1] as usize];
let b = cap_verts[segment_indices[i] as usize];
let c = cap_verts[segment_indices[i + 1] as usize];
// Add the current vertex and its counterpart on the backface
positions.push(b);
positions.push([b[0], b[1], -b[2]]);
uvs.extend_from_slice(&[[uv_x, 0.5], [uv_x, 1.]]);
// The normal for the current vertices can be calculated based on the two neighbouring vertices.
// The normal is interpolated between the normals of the two line segments connecting the current vertex with its neighbours.
// Closer vertices have a stronger effect on the normal than more distant ones.
let n = {
let ab = Vec2::from_slice(&b) - Vec2::from_slice(&a);
let bc = Vec2::from_slice(&c) - Vec2::from_slice(&b);
let n = ab.normalize_or_zero() + bc.normalize_or_zero();
Vec2::new(n.y, -n.x)
.normalize_or_zero()
.extend(0.)
.to_array()
};
normals.extend_from_slice(&[n; 2]);
}
// If there is a last vertex, we need to add it and its counterpart on the back face.
// The normal is provided by `segment.last_normal`.
if let Some(i) = segment_indices.last() {
let p = cap_verts[*i as usize];
positions.push(p);
positions.push([p[0], p[1], -p[2]]);
uvs.extend_from_slice(&[
[uv_start + uv_segment_delta, 0.5],
[uv_start + uv_segment_delta, 1.],
]);
normals.extend_from_slice(&[last_normal.extend(0.).to_array(); 2]);
}
for i in 0..(segment_indices.len() as u32 - 1) {
let index = base_index + 2 * i;
indices.extend_from_slice(&[
index,
index + 1,
index + 2,
index + 2,
index + 1,
index + 3,
]);
}
}
}
}
Mesh::new(
wgpu::PrimitiveTopology::TriangleList,
front_face.asset_usage,
)
.with_inserted_indices(Indices::U32(indices))
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
};
front_face.merge(back_face);
front_face.merge(mantel);
front_face
}
}
impl<P> From<Extrusion<P>> for Mesh
where
P: Primitive2d + Meshable,
P::Output: Extrudable,
{
fn from(value: Extrusion<P>) -> Self {
value.mesh().build()
}
}

View File

@ -25,13 +25,15 @@ pub use dim2::*;
mod dim3;
pub use dim3::*;
mod extrusion;
pub use extrusion::*;
use super::Mesh;
/// A trait for shapes that can be turned into a [`Mesh`](super::Mesh).
pub trait Meshable {
/// The output of [`Self::mesh`]. This can either be a [`Mesh`](super::Mesh)
/// or a [`MeshBuilder`] used for creating a [`Mesh`](super::Mesh).
type Output;
/// The output of [`Self::mesh`]. This will be a [`MeshBuilder`] used for creating a [`Mesh`](super::Mesh).
type Output: MeshBuilder;
/// Creates a [`Mesh`](super::Mesh) for a shape.
fn mesh(&self) -> Self::Output;

View File

@ -28,7 +28,9 @@ fn main() {
#[derive(Component)]
struct Shape;
const X_EXTENT: f32 = 14.0;
const SHAPES_X_EXTENT: f32 = 14.0;
const EXTRUSION_X_EXTENT: f32 = 16.0;
const Z_EXTENT: f32 = 5.0;
fn setup(
mut commands: Commands,
@ -53,6 +55,16 @@ fn setup(
meshes.add(Sphere::default().mesh().uv(32, 18)),
];
let extrusions = [
meshes.add(Extrusion::new(Rectangle::default(), 1.)),
meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
meshes.add(Extrusion::new(Annulus::default(), 1.)),
meshes.add(Extrusion::new(Circle::default(), 1.)),
meshes.add(Extrusion::new(Ellipse::default(), 1.)),
meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
meshes.add(Extrusion::new(Triangle2d::default(), 1.)),
];
let num_shapes = shapes.len();
for (i, shape) in shapes.into_iter().enumerate() {
@ -61,9 +73,29 @@ fn setup(
mesh: shape,
material: debug_material.clone(),
transform: Transform::from_xyz(
-X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
-SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT,
2.0,
0.0,
Z_EXTENT / 2.,
)
.with_rotation(Quat::from_rotation_x(-PI / 4.)),
..default()
},
Shape,
));
}
let num_extrusions = extrusions.len();
for (i, shape) in extrusions.into_iter().enumerate() {
commands.spawn((
PbrBundle {
mesh: shape,
material: debug_material.clone(),
transform: Transform::from_xyz(
-EXTRUSION_X_EXTENT / 2.
+ i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT,
2.0,
-Z_EXTENT / 2.,
)
.with_rotation(Quat::from_rotation_x(-PI / 4.)),
..default()
@ -92,7 +124,7 @@ fn setup(
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
transform: Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
..default()
});

View File

@ -507,16 +507,16 @@ fn spawn_primitive_2d(
let material: Handle<ColorMaterial> = materials.add(Color::WHITE);
let camera_mode = CameraActive::Dim2;
[
Some(RECTANGLE.mesh()),
Some(RECTANGLE.mesh().build()),
Some(CIRCLE.mesh().build()),
Some(ELLIPSE.mesh().build()),
Some(TRIANGLE_2D.mesh()),
Some(TRIANGLE_2D.mesh().build()),
None, // plane
None, // line
None, // segment
None, // polyline
None, // polygon
Some(REGULAR_POLYGON.mesh()),
Some(REGULAR_POLYGON.mesh().build()),
Some(CAPSULE_2D.mesh().build()),
None, // cylinder
None, // cone
@ -554,10 +554,10 @@ fn spawn_primitive_3d(
let material: Handle<StandardMaterial> = materials.add(Color::WHITE);
let camera_mode = CameraActive::Dim3;
[
Some(CUBOID.mesh()),
Some(CUBOID.mesh().build()),
Some(SPHERE.mesh().build()),
None, // ellipse
Some(TRIANGLE_3D.mesh()),
Some(TRIANGLE_3D.mesh().build()),
Some(PLANE_3D.mesh().build()),
None, // line
None, // segment
@ -569,7 +569,7 @@ fn spawn_primitive_3d(
None, // cone
None, // conical frustum
Some(TORUS.mesh().build()),
Some(TETRAHEDRON.mesh()),
Some(TETRAHEDRON.mesh().build()),
]
.into_iter()
.zip(PrimitiveSelected::ALL)

View File

@ -154,6 +154,9 @@ enum Shape {
Tetrahedron,
Triangle,
}
struct ShapeMeshBuilder {
shape: Shape,
}
impl Shape {
/// Return a vector containing all implemented shapes
@ -195,16 +198,22 @@ impl ShapeSample for Shape {
}
impl Meshable for Shape {
type Output = Mesh;
type Output = ShapeMeshBuilder;
fn mesh(&self) -> Self::Output {
match self {
Shape::Cuboid => CUBOID.mesh(),
ShapeMeshBuilder { shape: *self }
}
}
impl MeshBuilder for ShapeMeshBuilder {
fn build(&self) -> Mesh {
match self.shape {
Shape::Cuboid => CUBOID.mesh().into(),
Shape::Sphere => SPHERE.mesh().into(),
Shape::Capsule => CAPSULE_3D.mesh().into(),
Shape::Cylinder => CYLINDER.mesh().into(),
Shape::Tetrahedron => TETRAHEDRON.mesh(),
Shape::Triangle => TRIANGLE_3D.mesh(),
Shape::Tetrahedron => TETRAHEDRON.mesh().into(),
Shape::Triangle => TRIANGLE_3D.mesh().into(),
}
}
}