
# 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:  The `3d_shapes` example now looks like this:  --------- 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>
1016 lines
32 KiB
Rust
1016 lines
32 KiB
Rust
use std::f32::consts::FRAC_PI_2;
|
|
|
|
use crate::{
|
|
mesh::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment},
|
|
render_asset::RenderAssetUsages,
|
|
};
|
|
|
|
use super::{Extrudable, MeshBuilder, Meshable};
|
|
use bevy_math::{
|
|
primitives::{
|
|
Annulus, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Rectangle,
|
|
RegularPolygon, Rhombus, Triangle2d, Triangle3d, WindingOrder,
|
|
},
|
|
FloatExt, Vec2,
|
|
};
|
|
use wgpu::PrimitiveTopology;
|
|
|
|
/// A builder used for creating a [`Mesh`] with a [`Circle`] shape.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct CircleMeshBuilder {
|
|
/// The [`Circle`] shape.
|
|
pub circle: Circle,
|
|
/// The number of vertices used for the circle mesh.
|
|
/// The default is `32`.
|
|
#[doc(alias = "vertices")]
|
|
pub resolution: usize,
|
|
}
|
|
|
|
impl Default for CircleMeshBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
circle: Circle::default(),
|
|
resolution: 32,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CircleMeshBuilder {
|
|
/// Creates a new [`CircleMeshBuilder`] from a given radius and vertex count.
|
|
#[inline]
|
|
pub const fn new(radius: f32, resolution: usize) -> Self {
|
|
Self {
|
|
circle: Circle { radius },
|
|
resolution,
|
|
}
|
|
}
|
|
|
|
/// Sets the number of vertices used for the circle mesh.
|
|
#[inline]
|
|
#[doc(alias = "vertices")]
|
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
|
self.resolution = resolution;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl MeshBuilder for CircleMeshBuilder {
|
|
fn build(&self) -> 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(),
|
|
}]
|
|
}
|
|
}
|
|
|
|
impl Meshable for Circle {
|
|
type Output = CircleMeshBuilder;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
CircleMeshBuilder {
|
|
circle: *self,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Circle> for Mesh {
|
|
fn from(circle: Circle) -> Self {
|
|
circle.mesh().build()
|
|
}
|
|
}
|
|
|
|
/// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes.
|
|
///
|
|
/// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes
|
|
/// the entire circle, particularly the same texture will be displayed with different fractions of a
|
|
/// complete circle.
|
|
///
|
|
/// It's expected that more will be added in the future, such as a variant that causes the texture to be
|
|
/// scaled to fit the bounding box of the shape, which would be good for packed textures only including the
|
|
/// portion of the circle that is needed to display.
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
#[non_exhaustive]
|
|
pub enum CircularMeshUvMode {
|
|
/// Treats the shape as a mask over a circle of equal size and radius,
|
|
/// with the center of the circle at the center of the texture.
|
|
Mask {
|
|
/// Angle by which to rotate the shape when generating the UV map.
|
|
angle: f32,
|
|
},
|
|
}
|
|
|
|
impl Default for CircularMeshUvMode {
|
|
fn default() -> Self {
|
|
CircularMeshUvMode::Mask { angle: 0.0 }
|
|
}
|
|
}
|
|
|
|
/// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape.
|
|
///
|
|
/// The resulting mesh will have a UV-map such that the center of the circle is
|
|
/// at the center of the texture.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CircularSectorMeshBuilder {
|
|
/// The sector shape.
|
|
pub sector: CircularSector,
|
|
/// The number of vertices used for the arc portion of the sector mesh.
|
|
/// The default is `32`.
|
|
#[doc(alias = "vertices")]
|
|
pub resolution: usize,
|
|
/// The UV mapping mode
|
|
pub uv_mode: CircularMeshUvMode,
|
|
}
|
|
|
|
impl Default for CircularSectorMeshBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
sector: CircularSector::default(),
|
|
resolution: 32,
|
|
uv_mode: CircularMeshUvMode::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CircularSectorMeshBuilder {
|
|
/// Creates a new [`CircularSectorMeshBuilder`] from a given sector
|
|
#[inline]
|
|
pub fn new(sector: CircularSector) -> Self {
|
|
Self {
|
|
sector,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Sets the number of vertices used for the sector mesh.
|
|
#[inline]
|
|
#[doc(alias = "vertices")]
|
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
|
self.resolution = resolution;
|
|
self
|
|
}
|
|
|
|
/// Sets the uv mode used for the sector mesh
|
|
#[inline]
|
|
pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self {
|
|
self.uv_mode = uv_mode;
|
|
self
|
|
}
|
|
}
|
|
|
|
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];
|
|
let mut uvs = Vec::with_capacity(self.resolution + 1);
|
|
|
|
let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode;
|
|
|
|
// Push the center of the circle.
|
|
positions.push([0.0; 3]);
|
|
uvs.push([0.5; 2]);
|
|
|
|
let first_angle = FRAC_PI_2 - self.sector.half_angle();
|
|
let last_angle = FRAC_PI_2 + self.sector.half_angle();
|
|
let last_i = (self.resolution - 1) as f32;
|
|
for i in 0..self.resolution {
|
|
let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i);
|
|
|
|
// Compute the vertex
|
|
let vertex = self.sector.radius() * Vec2::from_angle(angle);
|
|
// Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5).
|
|
// We accomplish the Y axis flip by negating the angle.
|
|
let uv =
|
|
Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5));
|
|
|
|
positions.push([vertex.x, vertex.y, 0.0]);
|
|
uvs.push([uv.x, uv.y]);
|
|
}
|
|
|
|
for i in 1..(self.resolution as u32) {
|
|
// Index 0 is the center.
|
|
indices.extend_from_slice(&[0, i, i + 1]);
|
|
}
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
.with_inserted_indices(Indices::U32(indices))
|
|
}
|
|
}
|
|
|
|
impl Meshable for CircularSector {
|
|
type Output = CircularSectorMeshBuilder;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
CircularSectorMeshBuilder {
|
|
sector: *self,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CircularSector> for Mesh {
|
|
/// Converts this sector into a [`Mesh`] using a default [`CircularSectorMeshBuilder`].
|
|
///
|
|
/// See the documentation of [`CircularSectorMeshBuilder`] for more details.
|
|
fn from(sector: CircularSector) -> Self {
|
|
sector.mesh().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
|
|
/// at the center of the texture.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct CircularSegmentMeshBuilder {
|
|
/// The segment shape.
|
|
pub segment: CircularSegment,
|
|
/// The number of vertices used for the arc portion of the segment mesh.
|
|
/// The default is `32`.
|
|
#[doc(alias = "vertices")]
|
|
pub resolution: usize,
|
|
/// The UV mapping mode
|
|
pub uv_mode: CircularMeshUvMode,
|
|
}
|
|
|
|
impl Default for CircularSegmentMeshBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
segment: CircularSegment::default(),
|
|
resolution: 32,
|
|
uv_mode: CircularMeshUvMode::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CircularSegmentMeshBuilder {
|
|
/// Creates a new [`CircularSegmentMeshBuilder`] from a given segment
|
|
#[inline]
|
|
pub fn new(segment: CircularSegment) -> Self {
|
|
Self {
|
|
segment,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Sets the number of vertices used for the segment mesh.
|
|
#[inline]
|
|
#[doc(alias = "vertices")]
|
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
|
self.resolution = resolution;
|
|
self
|
|
}
|
|
|
|
/// Sets the uv mode used for the segment mesh
|
|
#[inline]
|
|
pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self {
|
|
self.uv_mode = uv_mode;
|
|
self
|
|
}
|
|
}
|
|
|
|
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];
|
|
let mut uvs = Vec::with_capacity(self.resolution + 1);
|
|
|
|
let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode;
|
|
|
|
// Push the center of the chord.
|
|
let midpoint_vertex = self.segment.chord_midpoint();
|
|
positions.push([midpoint_vertex.x, midpoint_vertex.y, 0.0]);
|
|
// Compute the UV coordinate of the midpoint vertex.
|
|
// This is similar to the computation inside the loop for the arc vertices,
|
|
// but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius
|
|
// to correctly position the vertex.
|
|
let midpoint_uv = Vec2::from_angle(-uv_angle - FRAC_PI_2).mul_add(
|
|
Vec2::splat(0.5 * (self.segment.apothem() / self.segment.radius())),
|
|
Vec2::splat(0.5),
|
|
);
|
|
uvs.push([midpoint_uv.x, midpoint_uv.y]);
|
|
|
|
let first_angle = FRAC_PI_2 - self.segment.half_angle();
|
|
let last_angle = FRAC_PI_2 + self.segment.half_angle();
|
|
let last_i = (self.resolution - 1) as f32;
|
|
for i in 0..self.resolution {
|
|
let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i);
|
|
|
|
// Compute the vertex
|
|
let vertex = self.segment.radius() * Vec2::from_angle(angle);
|
|
// Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5).
|
|
// We accomplish the Y axis flip by negating the angle.
|
|
let uv =
|
|
Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5));
|
|
|
|
positions.push([vertex.x, vertex.y, 0.0]);
|
|
uvs.push([uv.x, uv.y]);
|
|
}
|
|
|
|
for i in 1..(self.resolution as u32) {
|
|
// Index 0 is the midpoint of the chord.
|
|
indices.extend_from_slice(&[0, i, i + 1]);
|
|
}
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
.with_inserted_indices(Indices::U32(indices))
|
|
}
|
|
}
|
|
|
|
impl Meshable for CircularSegment {
|
|
type Output = CircularSegmentMeshBuilder;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
CircularSegmentMeshBuilder {
|
|
segment: *self,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CircularSegment> for Mesh {
|
|
/// Converts this sector into a [`Mesh`] using a default [`CircularSegmentMeshBuilder`].
|
|
///
|
|
/// See the documentation of [`CircularSegmentMeshBuilder`] for more details.
|
|
fn from(segment: CircularSegment) -> Self {
|
|
segment.mesh().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 MeshBuilder for RegularPolygonMeshBuilder {
|
|
fn build(&self) -> Mesh {
|
|
// The ellipse mesh is just a regular polygon with two radii
|
|
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().build()
|
|
}
|
|
}
|
|
|
|
/// A builder used for creating a [`Mesh`] with an [`Ellipse`] shape.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct EllipseMeshBuilder {
|
|
/// The [`Ellipse`] shape.
|
|
pub ellipse: Ellipse,
|
|
/// The number of vertices used for the ellipse mesh.
|
|
/// The default is `32`.
|
|
#[doc(alias = "vertices")]
|
|
pub resolution: usize,
|
|
}
|
|
|
|
impl Default for EllipseMeshBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
ellipse: Ellipse::default(),
|
|
resolution: 32,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EllipseMeshBuilder {
|
|
/// Creates a new [`EllipseMeshBuilder`] from a given half width and half height and a vertex count.
|
|
#[inline]
|
|
pub const fn new(half_width: f32, half_height: f32, resolution: usize) -> Self {
|
|
Self {
|
|
ellipse: Ellipse::new(half_width, half_height),
|
|
resolution,
|
|
}
|
|
}
|
|
|
|
/// Sets the number of vertices used for the ellipse mesh.
|
|
#[inline]
|
|
#[doc(alias = "vertices")]
|
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
|
self.resolution = resolution;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl MeshBuilder for EllipseMeshBuilder {
|
|
fn build(&self) -> Mesh {
|
|
let mut indices = Vec::with_capacity((self.resolution - 2) * 3);
|
|
let mut positions = Vec::with_capacity(self.resolution);
|
|
let normals = vec![[0.0, 0.0, 1.0]; self.resolution];
|
|
let mut uvs = Vec::with_capacity(self.resolution);
|
|
|
|
// Add pi/2 so that there is a vertex at the top (sin is 1.0 and cos is 0.0)
|
|
let start_angle = std::f32::consts::FRAC_PI_2;
|
|
let step = std::f32::consts::TAU / self.resolution as f32;
|
|
|
|
for i in 0..self.resolution {
|
|
// Compute vertex position at angle theta
|
|
let theta = start_angle + i as f32 * step;
|
|
let (sin, cos) = theta.sin_cos();
|
|
let x = cos * self.ellipse.half_size.x;
|
|
let y = sin * self.ellipse.half_size.y;
|
|
|
|
positions.push([x, y, 0.0]);
|
|
uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]);
|
|
}
|
|
|
|
for i in 1..(self.resolution as u32 - 1) {
|
|
indices.extend_from_slice(&[0, i, i + 1]);
|
|
}
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
.with_inserted_indices(Indices::U32(indices))
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
EllipseMeshBuilder {
|
|
ellipse: *self,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Ellipse> for Mesh {
|
|
fn from(ellipse: Ellipse) -> Self {
|
|
ellipse.mesh().build()
|
|
}
|
|
}
|
|
|
|
/// A builder for creating a [`Mesh`] with an [`Annulus`] shape.
|
|
pub struct AnnulusMeshBuilder {
|
|
/// The [`Annulus`] shape.
|
|
pub annulus: Annulus,
|
|
|
|
/// The number of vertices used in constructing each concentric circle of the annulus mesh.
|
|
/// The default is `32`.
|
|
pub resolution: usize,
|
|
}
|
|
|
|
impl Default for AnnulusMeshBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
annulus: Annulus::default(),
|
|
resolution: 32,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AnnulusMeshBuilder {
|
|
/// Create an [`AnnulusMeshBuilder`] with the given inner radius, outer radius, and angular vertex count.
|
|
#[inline]
|
|
pub fn new(inner_radius: f32, outer_radius: f32, resolution: usize) -> Self {
|
|
Self {
|
|
annulus: Annulus::new(inner_radius, outer_radius),
|
|
resolution,
|
|
}
|
|
}
|
|
|
|
/// Sets the number of vertices used in constructing the concentric circles of the annulus mesh.
|
|
#[inline]
|
|
pub fn resolution(mut self, resolution: usize) -> Self {
|
|
self.resolution = resolution;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl MeshBuilder for AnnulusMeshBuilder {
|
|
fn build(&self) -> Mesh {
|
|
let inner_radius = self.annulus.inner_circle.radius;
|
|
let outer_radius = self.annulus.outer_circle.radius;
|
|
|
|
let num_vertices = (self.resolution + 1) * 2;
|
|
let mut indices = Vec::with_capacity(self.resolution * 6);
|
|
let mut positions = Vec::with_capacity(num_vertices);
|
|
let mut uvs = Vec::with_capacity(num_vertices);
|
|
let normals = vec![[0.0, 0.0, 1.0]; num_vertices];
|
|
|
|
// We have one more set of vertices than might be naïvely expected;
|
|
// the vertices at `start_angle` are duplicated for the purposes of UV
|
|
// mapping. Here, each iteration places a pair of vertices at a fixed
|
|
// angle from the center of the annulus.
|
|
let start_angle = std::f32::consts::FRAC_PI_2;
|
|
let step = std::f32::consts::TAU / self.resolution as f32;
|
|
for i in 0..=self.resolution {
|
|
let theta = start_angle + i as f32 * step;
|
|
let (sin, cos) = theta.sin_cos();
|
|
let inner_pos = [cos * inner_radius, sin * inner_radius, 0.];
|
|
let outer_pos = [cos * outer_radius, sin * outer_radius, 0.];
|
|
positions.push(inner_pos);
|
|
positions.push(outer_pos);
|
|
|
|
// The first UV direction is radial and the second is angular;
|
|
// i.e., a single UV rectangle is stretched around the annulus, with
|
|
// its top and bottom meeting as the circle closes. Lines of constant
|
|
// U map to circles, and lines of constant V map to radial line segments.
|
|
let inner_uv = [0., i as f32 / self.resolution as f32];
|
|
let outer_uv = [1., i as f32 / self.resolution as f32];
|
|
uvs.push(inner_uv);
|
|
uvs.push(outer_uv);
|
|
}
|
|
|
|
// Adjacent pairs of vertices form two triangles with each other; here,
|
|
// we are just making sure that they both have the right orientation,
|
|
// which is the CCW order of
|
|
// `inner_vertex` -> `outer_vertex` -> `next_outer` -> `next_inner`
|
|
for i in 0..(self.resolution as u32) {
|
|
let inner_vertex = 2 * i;
|
|
let outer_vertex = 2 * i + 1;
|
|
let next_inner = inner_vertex + 2;
|
|
let next_outer = outer_vertex + 2;
|
|
indices.extend_from_slice(&[inner_vertex, outer_vertex, next_outer]);
|
|
indices.extend_from_slice(&[next_outer, next_inner, inner_vertex]);
|
|
}
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
.with_inserted_indices(Indices::U32(indices))
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
AnnulusMeshBuilder {
|
|
annulus: *self,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Annulus> for Mesh {
|
|
fn from(annulus: Annulus) -> Self {
|
|
annulus.mesh().build()
|
|
}
|
|
}
|
|
|
|
pub struct RhombusMeshBuilder {
|
|
half_diagonals: Vec2,
|
|
}
|
|
|
|
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],
|
|
[-hhd, 0.0, 0.0],
|
|
[0.0, vhd, 0.0],
|
|
[0.0, -vhd, 0.0],
|
|
];
|
|
let normals = vec![[0.0, 0.0, 1.0]; 4];
|
|
let uvs = vec![[1.0, 0.5], [0.0, 0.5], [0.5, 0.0], [0.5, 1.0]];
|
|
let indices = Indices::U32(vec![1, 0, 2, 1, 3, 0]);
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_indices(indices)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
}
|
|
}
|
|
|
|
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 = Triangle2dMeshBuilder;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
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];
|
|
|
|
let uvs: Vec<_> = triangle3d::uv_coords(&Triangle3d::new(
|
|
vertices_3d[0],
|
|
vertices_3d[1],
|
|
vertices_3d[2],
|
|
))
|
|
.into();
|
|
|
|
let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise;
|
|
let indices = if is_ccw {
|
|
Indices::U32(vec![0, 1, 2])
|
|
} else {
|
|
Indices::U32(vec![2, 1, 0])
|
|
};
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_indices(indices)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
}
|
|
}
|
|
|
|
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 From<Triangle2d> for Mesh {
|
|
fn from(triangle: Triangle2d) -> Self {
|
|
triangle.mesh().build()
|
|
}
|
|
}
|
|
|
|
/// 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],
|
|
[-hw, hh, 0.0],
|
|
[-hw, -hh, 0.0],
|
|
[hw, -hh, 0.0],
|
|
];
|
|
let normals = vec![[0.0, 0.0, 1.0]; 4];
|
|
let uvs = vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
|
|
let indices = Indices::U32(vec![0, 1, 2, 0, 2, 3]);
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_indices(indices)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
}
|
|
}
|
|
|
|
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().build()
|
|
}
|
|
}
|
|
|
|
/// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct Capsule2dMeshBuilder {
|
|
/// The [`Capsule2d`] shape.
|
|
pub capsule: Capsule2d,
|
|
/// The number of vertices used for one hemicircle.
|
|
/// The total number of vertices for the capsule mesh will be two times the resolution.
|
|
///
|
|
/// The default is `16`.
|
|
pub resolution: usize,
|
|
}
|
|
|
|
impl Default for Capsule2dMeshBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
capsule: Capsule2d::default(),
|
|
resolution: 16,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Capsule2dMeshBuilder {
|
|
/// Creates a new [`Capsule2dMeshBuilder`] from a given radius, length, and the number of vertices
|
|
/// used for one hemicircle. The total number of vertices for the capsule mesh will be two times the resolution.
|
|
#[inline]
|
|
pub fn new(radius: f32, length: f32, resolution: usize) -> Self {
|
|
Self {
|
|
capsule: Capsule2d::new(radius, length),
|
|
resolution,
|
|
}
|
|
}
|
|
|
|
/// Sets the number of vertices used for one hemicircle.
|
|
/// The total number of vertices for the capsule mesh will be two times the resolution.
|
|
#[inline]
|
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
|
self.resolution = resolution;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl MeshBuilder for Capsule2dMeshBuilder {
|
|
fn build(&self) -> Mesh {
|
|
// The resolution is the number of vertices for one semicircle
|
|
let resolution = self.resolution as u32;
|
|
let vertex_count = 2 * self.resolution;
|
|
|
|
// Six extra indices for the two triangles between the hemicircles
|
|
let mut indices = Vec::with_capacity((self.resolution - 2) * 2 * 3 + 6);
|
|
let mut positions = Vec::with_capacity(vertex_count);
|
|
let normals = vec![[0.0, 0.0, 1.0]; vertex_count];
|
|
let mut uvs = Vec::with_capacity(vertex_count);
|
|
|
|
let radius = self.capsule.radius;
|
|
let step = std::f32::consts::TAU / vertex_count as f32;
|
|
|
|
// If the vertex count is even, offset starting angle of top semicircle by half a step
|
|
// to position the vertices evenly.
|
|
let start_angle = if vertex_count % 2 == 0 {
|
|
step / 2.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// How much the hemicircle radius is of the total half-height of the capsule.
|
|
// This is used to prevent the UVs from stretching between the hemicircles.
|
|
let radius_frac = self.capsule.radius / (self.capsule.half_length + self.capsule.radius);
|
|
|
|
// Create top semicircle
|
|
for i in 0..resolution {
|
|
// Compute vertex position at angle theta
|
|
let theta = start_angle + i as f32 * step;
|
|
let (sin, cos) = theta.sin_cos();
|
|
let (x, y) = (cos * radius, sin * radius + self.capsule.half_length);
|
|
|
|
positions.push([x, y, 0.0]);
|
|
uvs.push([0.5 * (cos + 1.0), radius_frac * (1.0 - 0.5 * (sin + 1.0))]);
|
|
}
|
|
|
|
// Add top semicircle indices
|
|
for i in 1..resolution - 1 {
|
|
indices.extend_from_slice(&[0, i, i + 1]);
|
|
}
|
|
|
|
// Add indices for top left triangle of the part between the hemicircles
|
|
indices.extend_from_slice(&[0, resolution - 1, resolution]);
|
|
|
|
// Create bottom semicircle
|
|
for i in resolution..vertex_count as u32 {
|
|
// Compute vertex position at angle theta
|
|
let theta = start_angle + i as f32 * step;
|
|
let (sin, cos) = theta.sin_cos();
|
|
let (x, y) = (cos * radius, sin * radius - self.capsule.half_length);
|
|
|
|
positions.push([x, y, 0.0]);
|
|
uvs.push([0.5 * (cos + 1.0), 1.0 - radius_frac * 0.5 * (sin + 1.0)]);
|
|
}
|
|
|
|
// Add bottom semicircle indices
|
|
for i in 1..resolution - 1 {
|
|
indices.extend_from_slice(&[resolution, resolution + i, resolution + i + 1]);
|
|
}
|
|
|
|
// Add indices for bottom right triangle of the part between the hemicircles
|
|
indices.extend_from_slice(&[resolution, vertex_count as u32 - 1, 0]);
|
|
|
|
Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
|
.with_inserted_indices(Indices::U32(indices))
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
fn mesh(&self) -> Self::Output {
|
|
Capsule2dMeshBuilder {
|
|
capsule: *self,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Capsule2d> for Mesh {
|
|
fn from(capsule: Capsule2d) -> Self {
|
|
capsule.mesh().build()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use bevy_math::primitives::RegularPolygon;
|
|
|
|
use crate::mesh::{Mesh, VertexAttributeValues};
|
|
|
|
/// Sin/cos and multiplication computations result in numbers like 0.4999999.
|
|
/// Round these to numbers we expect like 0.5.
|
|
fn fix_floats<const N: usize>(points: &mut [[f32; N]]) {
|
|
for point in points.iter_mut() {
|
|
for coord in point.iter_mut() {
|
|
let round = (*coord * 2.).round() / 2.;
|
|
if (*coord - round).abs() < 0.00001 {
|
|
*coord = round;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_regular_polygon() {
|
|
let mut mesh = Mesh::from(RegularPolygon::new(7.0, 4));
|
|
|
|
let Some(VertexAttributeValues::Float32x3(mut positions)) =
|
|
mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION)
|
|
else {
|
|
panic!("Expected positions f32x3");
|
|
};
|
|
let Some(VertexAttributeValues::Float32x2(mut uvs)) =
|
|
mesh.remove_attribute(Mesh::ATTRIBUTE_UV_0)
|
|
else {
|
|
panic!("Expected uvs f32x2");
|
|
};
|
|
let Some(VertexAttributeValues::Float32x3(normals)) =
|
|
mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL)
|
|
else {
|
|
panic!("Expected normals f32x3");
|
|
};
|
|
|
|
fix_floats(&mut positions);
|
|
fix_floats(&mut uvs);
|
|
|
|
assert_eq!(
|
|
[
|
|
[0.0, 7.0, 0.0],
|
|
[-7.0, 0.0, 0.0],
|
|
[0.0, -7.0, 0.0],
|
|
[7.0, 0.0, 0.0],
|
|
],
|
|
&positions[..]
|
|
);
|
|
|
|
// Note V coordinate increases in the opposite direction to the Y coordinate.
|
|
assert_eq!([[0.5, 0.0], [0.0, 0.5], [0.5, 1.0], [1.0, 0.5],], &uvs[..]);
|
|
|
|
assert_eq!(&[[0.0, 0.0, 1.0]; 4], &normals[..]);
|
|
}
|
|
}
|