Implement Meshable for some 3D primitives (#11688)

# Objective

Split up from #11007, fixing most of the remaining work for #10569.

Implement `Meshable` for `Cuboid`, `Sphere`, `Cylinder`, `Capsule`,
`Torus`, and `Plane3d`. This covers all shapes that Bevy has mesh
structs for in `bevy_render::mesh::shapes`.

`Cone` and `ConicalFrustum` are new shapes, so I can add them in a
follow-up, or I could just add them here directly if that's preferrable.

## Solution

Implement `Meshable` for `Cuboid`, `Sphere`, `Cylinder`, `Capsule`,
`Torus`, and `Plane3d`.

The logic is mostly just a copy of the the existing `bevy_render`
shapes, but `Plane3d` has a configurable surface normal that affects the
orientation. Some property names have also been changed to be more
consistent.

The default values differ from the old shapes to make them a bit more
logical:

- Spheres now have a radius of 0.5 instead of 1.0. The default capsule
is equivalent to the default cylinder with the sphere's halves glued on.
- The inner and outer radius of the torus are now 0.5 and 1.0 instead of
0.5 and 1.5 (i.e. the new minor and major radii are 0.25 and 0.75). It's
double the width of the default cuboid, half of its height, and the
default sphere matches the size of the hole.
- `Cuboid` is 1x1x1 by default unlike the dreaded `Box` which is 2x1x1.

Before, with "old" shapes:


![old](https://github.com/bevyengine/bevy/assets/57632562/733f3dda-258c-4491-8152-9829e056a1a3)

Now, with primitive meshing:


![new](https://github.com/bevyengine/bevy/assets/57632562/5a1af14f-bb98-401d-82cf-de8072fea4ec)

I only changed the `3d_shapes` example to use primitives for now. I can
change them all in this PR or a follow-up though, whichever way is
preferrable.

### Sphere API

Spheres have had separate `Icosphere` and `UVSphere` structs, but with
primitives we only have one `Sphere`.

We need to handle this with builders:

```rust
// Existing structs
let ico = Mesh::try_from(Icophere::default()).unwrap();
let uv = Mesh::from(UVSphere::default());

// Primitives
let ico = Sphere::default().mesh().ico(5).unwrap();
let uv = Sphere::default().mesh().uv(32, 18);
```

We could add methods on `Sphere` directly to skip calling `.mesh()`.

I also added a `SphereKind` enum that can be used with the `kind`
method:

```rust
let ico = Sphere::default()
    .mesh()
    .kind(SphereKind::Ico { subdivisions: 8 })
    .build();
```

The default mesh for a `Sphere` is an icosphere with 5 subdivisions
(like the default `Icosphere`).

---

## Changelog

- Implement `Meshable` and `Default` for `Cuboid`, `Sphere`, `Cylinder`,
`Capsule`, `Torus`, and `Plane3d`
- Use primitives in `3d_shapes` example

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Joona Aalto 2024-02-06 23:44:13 +02:00 committed by GitHub
parent 9f2eabb02f
commit cf15e6bba3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1330 additions and 9 deletions

View file

@ -155,6 +155,13 @@ pub struct Sphere {
}
impl Primitive3d for Sphere {}
impl Default for Sphere {
/// Returns the default [`Sphere`] with a radius of `0.5`.
fn default() -> Self {
Self { radius: 0.5 }
}
}
impl Sphere {
/// Create a new [`Sphere`] from a `radius`
#[inline(always)]
@ -210,6 +217,15 @@ pub struct Plane3d {
}
impl Primitive3d for Plane3d {}
impl Default for Plane3d {
/// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction.
fn default() -> Self {
Self {
normal: Direction3d::Y,
}
}
}
impl Plane3d {
/// Create a new `Plane3d` from a normal
///
@ -374,6 +390,15 @@ pub struct Cuboid {
}
impl Primitive3d for Cuboid {}
impl Default for Cuboid {
/// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`.
fn default() -> Self {
Self {
half_size: Vec3::splat(0.5),
}
}
}
impl Cuboid {
/// Create a new `Cuboid` from a full x, y, and z length
#[inline(always)]
@ -439,6 +464,16 @@ pub struct Cylinder {
}
impl Primitive3d for Cylinder {}
impl Default for Cylinder {
/// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`.
fn default() -> Self {
Self {
radius: 0.5,
half_height: 0.5,
}
}
}
impl Cylinder {
/// Create a new `Cylinder` from a radius and full height
#[inline(always)]
@ -496,6 +531,17 @@ pub struct Capsule3d {
}
impl Primitive3d for Capsule3d {}
impl Default for Capsule3d {
/// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`.
/// The total height is `2.0`.
fn default() -> Self {
Self {
radius: 0.5,
half_length: 0.5,
}
}
}
impl Capsule3d {
/// Create a new `Capsule3d` from a radius and length
pub fn new(radius: f32, length: f32) -> Self {
@ -636,6 +682,16 @@ pub struct Torus {
}
impl Primitive3d for Torus {}
impl Default for Torus {
/// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`.
fn default() -> Self {
Self {
minor_radius: 0.25,
major_radius: 0.75,
}
}
}
impl Torus {
/// Create a new `Torus` from an inner and outer radius.
///

View file

@ -0,0 +1,445 @@
use crate::{
mesh::{Indices, Mesh, Meshable},
render_asset::RenderAssetUsages,
};
use bevy_math::{primitives::Capsule3d, Vec2, Vec3};
use wgpu::PrimitiveTopology;
/// Manner in which UV coordinates are distributed vertically.
#[derive(Clone, Copy, Debug, Default)]
pub enum CapsuleUvProfile {
/// UV space is distributed by how much of the capsule consists of the hemispheres.
#[default]
Aspect,
/// Hemispheres get UV space according to the ratio of latitudes to rings.
Uniform,
/// Upper third of the texture goes to the northern hemisphere, middle third to the cylinder
/// and lower third to the southern one.
Fixed,
}
/// A builder used for creating a [`Mesh`] with a [`Capsule3d`] shape.
#[derive(Clone, Copy, Debug)]
pub struct Capsule3dMeshBuilder {
/// The [`Capsule3d`] shape.
pub capsule: Capsule3d,
/// The number of horizontal lines subdividing the cylindrical part of the capsule.
/// The default is `0`.
pub rings: usize,
/// The number of vertical lines subdividing the hemispheres of the capsule.
/// The default is `32`.
pub longitudes: usize,
/// The number of horizontal lines subdividing the hemispheres of the capsule.
/// The default is `16`.
pub latitudes: usize,
/// The manner in which UV coordinates are distributed vertically.
/// The default is [`CapsuleUvProfile::Aspect`].
pub uv_profile: CapsuleUvProfile,
}
impl Default for Capsule3dMeshBuilder {
fn default() -> Self {
Self {
capsule: Capsule3d::default(),
rings: 0,
longitudes: 32,
latitudes: 16,
uv_profile: CapsuleUvProfile::default(),
}
}
}
impl Capsule3dMeshBuilder {
/// Creates a new [`Capsule3dMeshBuilder`] from a given radius, height, longitudes, and latitudes.
///
/// Note that `height` is the distance between the centers of the hemispheres.
/// `radius` will be added to both ends to get the real height of the mesh.
#[inline]
pub fn new(radius: f32, height: f32, longitudes: usize, latitudes: usize) -> Self {
Self {
capsule: Capsule3d::new(radius, height),
longitudes,
latitudes,
..Default::default()
}
}
/// Sets the number of horizontal lines subdividing the cylindrical part of the capsule.
#[inline]
pub const fn rings(mut self, rings: usize) -> Self {
self.rings = rings;
self
}
/// Sets the number of vertical lines subdividing the hemispheres of the capsule.
#[inline]
pub const fn longitudes(mut self, longitudes: usize) -> Self {
self.longitudes = longitudes;
self
}
/// Sets the number of horizontal lines subdividing the hemispheres of the capsule.
#[inline]
pub const fn latitudes(mut self, latitudes: usize) -> Self {
self.latitudes = latitudes;
self
}
/// Sets the manner in which UV coordinates are distributed vertically.
#[inline]
pub const fn uv_profile(mut self, uv_profile: CapsuleUvProfile) -> Self {
self.uv_profile = uv_profile;
self
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
// code adapted from https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db
let Capsule3dMeshBuilder {
capsule,
rings,
longitudes,
latitudes,
uv_profile,
} = *self;
let Capsule3d {
radius,
half_length,
} = capsule;
let calc_middle = rings > 0;
let half_lats = latitudes / 2;
let half_latsn1 = half_lats - 1;
let half_latsn2 = half_lats - 2;
let ringsp1 = rings + 1;
let lonsp1 = longitudes + 1;
let summit = half_length + radius;
// Vertex index offsets.
let vert_offset_north_hemi = longitudes;
let vert_offset_north_equator = vert_offset_north_hemi + lonsp1 * half_latsn1;
let vert_offset_cylinder = vert_offset_north_equator + lonsp1;
let vert_offset_south_equator = if calc_middle {
vert_offset_cylinder + lonsp1 * rings
} else {
vert_offset_cylinder
};
let vert_offset_south_hemi = vert_offset_south_equator + lonsp1;
let vert_offset_south_polar = vert_offset_south_hemi + lonsp1 * half_latsn2;
let vert_offset_south_cap = vert_offset_south_polar + lonsp1;
// Initialize arrays.
let vert_len = vert_offset_south_cap + longitudes;
let mut vs: Vec<Vec3> = vec![Vec3::ZERO; vert_len];
let mut vts: Vec<Vec2> = vec![Vec2::ZERO; vert_len];
let mut vns: Vec<Vec3> = vec![Vec3::ZERO; vert_len];
let to_theta = 2.0 * std::f32::consts::PI / longitudes as f32;
let to_phi = std::f32::consts::PI / latitudes as f32;
let to_tex_horizontal = 1.0 / longitudes as f32;
let to_tex_vertical = 1.0 / half_lats as f32;
let vt_aspect_ratio = match uv_profile {
CapsuleUvProfile::Aspect => radius / (2.0 * half_length + radius + radius),
CapsuleUvProfile::Uniform => half_lats as f32 / (ringsp1 + latitudes) as f32,
CapsuleUvProfile::Fixed => 1.0 / 3.0,
};
let vt_aspect_north = 1.0 - vt_aspect_ratio;
let vt_aspect_south = vt_aspect_ratio;
let mut theta_cartesian: Vec<Vec2> = vec![Vec2::ZERO; longitudes];
let mut rho_theta_cartesian: Vec<Vec2> = vec![Vec2::ZERO; longitudes];
let mut s_texture_cache: Vec<f32> = vec![0.0; lonsp1];
for j in 0..longitudes {
let jf = j as f32;
let s_texture_polar = 1.0 - ((jf + 0.5) * to_tex_horizontal);
let theta = jf * to_theta;
let cos_theta = theta.cos();
let sin_theta = theta.sin();
theta_cartesian[j] = Vec2::new(cos_theta, sin_theta);
rho_theta_cartesian[j] = Vec2::new(radius * cos_theta, radius * sin_theta);
// North.
vs[j] = Vec3::new(0.0, summit, 0.0);
vts[j] = Vec2::new(s_texture_polar, 1.0);
vns[j] = Vec3::Y;
// South.
let idx = vert_offset_south_cap + j;
vs[idx] = Vec3::new(0.0, -summit, 0.0);
vts[idx] = Vec2::new(s_texture_polar, 0.0);
vns[idx] = Vec3::new(0.0, -1.0, 0.0);
}
// Equatorial vertices.
for (j, s_texture_cache_j) in s_texture_cache.iter_mut().enumerate().take(lonsp1) {
let s_texture = 1.0 - j as f32 * to_tex_horizontal;
*s_texture_cache_j = s_texture;
// Wrap to first element upon reaching last.
let j_mod = j % longitudes;
let tc = theta_cartesian[j_mod];
let rtc = rho_theta_cartesian[j_mod];
// North equator.
let idxn = vert_offset_north_equator + j;
vs[idxn] = Vec3::new(rtc.x, half_length, -rtc.y);
vts[idxn] = Vec2::new(s_texture, vt_aspect_north);
vns[idxn] = Vec3::new(tc.x, 0.0, -tc.y);
// South equator.
let idxs = vert_offset_south_equator + j;
vs[idxs] = Vec3::new(rtc.x, -half_length, -rtc.y);
vts[idxs] = Vec2::new(s_texture, vt_aspect_south);
vns[idxs] = Vec3::new(tc.x, 0.0, -tc.y);
}
// Hemisphere vertices.
for i in 0..half_latsn1 {
let ip1f = i as f32 + 1.0;
let phi = ip1f * to_phi;
// For coordinates.
let cos_phi_south = phi.cos();
let sin_phi_south = phi.sin();
// Symmetrical hemispheres mean cosine and sine only needs
// to be calculated once.
let cos_phi_north = sin_phi_south;
let sin_phi_north = -cos_phi_south;
let rho_cos_phi_north = radius * cos_phi_north;
let rho_sin_phi_north = radius * sin_phi_north;
let z_offset_north = half_length - rho_sin_phi_north;
let rho_cos_phi_south = radius * cos_phi_south;
let rho_sin_phi_south = radius * sin_phi_south;
let z_offset_sout = -half_length - rho_sin_phi_south;
// For texture coordinates.
let t_tex_fac = ip1f * to_tex_vertical;
let cmpl_tex_fac = 1.0 - t_tex_fac;
let t_tex_north = cmpl_tex_fac + vt_aspect_north * t_tex_fac;
let t_tex_south = cmpl_tex_fac * vt_aspect_south;
let i_lonsp1 = i * lonsp1;
let vert_curr_lat_north = vert_offset_north_hemi + i_lonsp1;
let vert_curr_lat_south = vert_offset_south_hemi + i_lonsp1;
for (j, s_texture) in s_texture_cache.iter().enumerate().take(lonsp1) {
let j_mod = j % longitudes;
let tc = theta_cartesian[j_mod];
// North hemisphere.
let idxn = vert_curr_lat_north + j;
vs[idxn] = Vec3::new(
rho_cos_phi_north * tc.x,
z_offset_north,
-rho_cos_phi_north * tc.y,
);
vts[idxn] = Vec2::new(*s_texture, t_tex_north);
vns[idxn] = Vec3::new(cos_phi_north * tc.x, -sin_phi_north, -cos_phi_north * tc.y);
// South hemisphere.
let idxs = vert_curr_lat_south + j;
vs[idxs] = Vec3::new(
rho_cos_phi_south * tc.x,
z_offset_sout,
-rho_cos_phi_south * tc.y,
);
vts[idxs] = Vec2::new(*s_texture, t_tex_south);
vns[idxs] = Vec3::new(cos_phi_south * tc.x, -sin_phi_south, -cos_phi_south * tc.y);
}
}
// Cylinder vertices.
if calc_middle {
// Exclude both origin and destination edges
// (North and South equators) from the interpolation.
let to_fac = 1.0 / ringsp1 as f32;
let mut idx_cyl_lat = vert_offset_cylinder;
for h in 1..ringsp1 {
let fac = h as f32 * to_fac;
let cmpl_fac = 1.0 - fac;
let t_texture = cmpl_fac * vt_aspect_north + fac * vt_aspect_south;
let z = half_length - 2.0 * half_length * fac;
for (j, s_texture) in s_texture_cache.iter().enumerate().take(lonsp1) {
let j_mod = j % longitudes;
let tc = theta_cartesian[j_mod];
let rtc = rho_theta_cartesian[j_mod];
vs[idx_cyl_lat] = Vec3::new(rtc.x, z, -rtc.y);
vts[idx_cyl_lat] = Vec2::new(*s_texture, t_texture);
vns[idx_cyl_lat] = Vec3::new(tc.x, 0.0, -tc.y);
idx_cyl_lat += 1;
}
}
}
// Triangle indices.
// Stride is 3 for polar triangles;
// stride is 6 for two triangles forming a quad.
let lons3 = longitudes * 3;
let lons6 = longitudes * 6;
let hemi_lons = half_latsn1 * lons6;
let tri_offset_north_hemi = lons3;
let tri_offset_cylinder = tri_offset_north_hemi + hemi_lons;
let tri_offset_south_hemi = tri_offset_cylinder + ringsp1 * lons6;
let tri_offset_south_cap = tri_offset_south_hemi + hemi_lons;
let fs_len = tri_offset_south_cap + lons3;
let mut tris: Vec<u32> = vec![0; fs_len];
// Polar caps.
let mut i = 0;
let mut k = 0;
let mut m = tri_offset_south_cap;
while i < longitudes {
// North.
tris[k] = i as u32;
tris[k + 1] = (vert_offset_north_hemi + i) as u32;
tris[k + 2] = (vert_offset_north_hemi + i + 1) as u32;
// South.
tris[m] = (vert_offset_south_cap + i) as u32;
tris[m + 1] = (vert_offset_south_polar + i + 1) as u32;
tris[m + 2] = (vert_offset_south_polar + i) as u32;
i += 1;
k += 3;
m += 3;
}
// Hemispheres.
let mut i = 0;
let mut k = tri_offset_north_hemi;
let mut m = tri_offset_south_hemi;
while i < half_latsn1 {
let i_lonsp1 = i * lonsp1;
let vert_curr_lat_north = vert_offset_north_hemi + i_lonsp1;
let vert_next_lat_north = vert_curr_lat_north + lonsp1;
let vert_curr_lat_south = vert_offset_south_equator + i_lonsp1;
let vert_next_lat_south = vert_curr_lat_south + lonsp1;
let mut j = 0;
while j < longitudes {
// North.
let north00 = vert_curr_lat_north + j;
let north01 = vert_next_lat_north + j;
let north11 = vert_next_lat_north + j + 1;
let north10 = vert_curr_lat_north + j + 1;
tris[k] = north00 as u32;
tris[k + 1] = north11 as u32;
tris[k + 2] = north10 as u32;
tris[k + 3] = north00 as u32;
tris[k + 4] = north01 as u32;
tris[k + 5] = north11 as u32;
// South.
let south00 = vert_curr_lat_south + j;
let south01 = vert_next_lat_south + j;
let south11 = vert_next_lat_south + j + 1;
let south10 = vert_curr_lat_south + j + 1;
tris[m] = south00 as u32;
tris[m + 1] = south11 as u32;
tris[m + 2] = south10 as u32;
tris[m + 3] = south00 as u32;
tris[m + 4] = south01 as u32;
tris[m + 5] = south11 as u32;
j += 1;
k += 6;
m += 6;
}
i += 1;
}
// Cylinder.
let mut i = 0;
let mut k = tri_offset_cylinder;
while i < ringsp1 {
let vert_curr_lat = vert_offset_north_equator + i * lonsp1;
let vert_next_lat = vert_curr_lat + lonsp1;
let mut j = 0;
while j < longitudes {
let cy00 = vert_curr_lat + j;
let cy01 = vert_next_lat + j;
let cy11 = vert_next_lat + j + 1;
let cy10 = vert_curr_lat + j + 1;
tris[k] = cy00 as u32;
tris[k + 1] = cy11 as u32;
tris[k + 2] = cy10 as u32;
tris[k + 3] = cy00 as u32;
tris[k + 4] = cy01 as u32;
tris[k + 5] = cy11 as u32;
j += 1;
k += 6;
}
i += 1;
}
let vs: Vec<[f32; 3]> = vs.into_iter().map(Into::into).collect();
let vns: Vec<[f32; 3]> = vns.into_iter().map(Into::into).collect();
let vts: Vec<[f32; 2]> = vts.into_iter().map(Into::into).collect();
assert_eq!(vs.len(), vert_len);
assert_eq!(tris.len(), fs_len);
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts)
.with_indices(Some(Indices::U32(tris)))
}
}
impl Meshable for Capsule3d {
type Output = Capsule3dMeshBuilder;
fn mesh(&self) -> Self::Output {
Capsule3dMeshBuilder {
capsule: *self,
..Default::default()
}
}
}
impl From<Capsule3d> for Mesh {
fn from(capsule: Capsule3d) -> Self {
capsule.mesh().build()
}
}
impl From<Capsule3dMeshBuilder> for Mesh {
fn from(capsule: Capsule3dMeshBuilder) -> Self {
capsule.build()
}
}

View file

@ -0,0 +1,78 @@
use bevy_math::primitives::Cuboid;
use wgpu::PrimitiveTopology;
use crate::{
mesh::{Indices, Mesh, Meshable},
render_asset::RenderAssetUsages,
};
impl Meshable for Cuboid {
type Output = Mesh;
fn mesh(&self) -> Self::Output {
let min = -self.half_size;
let max = self.half_size;
// Suppose Y-up right hand, and camera look from +Z to -Z
let vertices = &[
// Front
([min.x, min.y, max.z], [0.0, 0.0, 1.0], [0.0, 0.0]),
([max.x, min.y, max.z], [0.0, 0.0, 1.0], [1.0, 0.0]),
([max.x, max.y, max.z], [0.0, 0.0, 1.0], [1.0, 1.0]),
([min.x, max.y, max.z], [0.0, 0.0, 1.0], [0.0, 1.0]),
// Back
([min.x, max.y, min.z], [0.0, 0.0, -1.0], [1.0, 0.0]),
([max.x, max.y, min.z], [0.0, 0.0, -1.0], [0.0, 0.0]),
([max.x, min.y, min.z], [0.0, 0.0, -1.0], [0.0, 1.0]),
([min.x, min.y, min.z], [0.0, 0.0, -1.0], [1.0, 1.0]),
// Right
([max.x, min.y, min.z], [1.0, 0.0, 0.0], [0.0, 0.0]),
([max.x, max.y, min.z], [1.0, 0.0, 0.0], [1.0, 0.0]),
([max.x, max.y, max.z], [1.0, 0.0, 0.0], [1.0, 1.0]),
([max.x, min.y, max.z], [1.0, 0.0, 0.0], [0.0, 1.0]),
// Left
([min.x, min.y, max.z], [-1.0, 0.0, 0.0], [1.0, 0.0]),
([min.x, max.y, max.z], [-1.0, 0.0, 0.0], [0.0, 0.0]),
([min.x, max.y, min.z], [-1.0, 0.0, 0.0], [0.0, 1.0]),
([min.x, min.y, min.z], [-1.0, 0.0, 0.0], [1.0, 1.0]),
// Top
([max.x, max.y, min.z], [0.0, 1.0, 0.0], [1.0, 0.0]),
([min.x, max.y, min.z], [0.0, 1.0, 0.0], [0.0, 0.0]),
([min.x, max.y, max.z], [0.0, 1.0, 0.0], [0.0, 1.0]),
([max.x, max.y, max.z], [0.0, 1.0, 0.0], [1.0, 1.0]),
// Bottom
([max.x, min.y, max.z], [0.0, -1.0, 0.0], [0.0, 0.0]),
([min.x, min.y, max.z], [0.0, -1.0, 0.0], [1.0, 0.0]),
([min.x, min.y, min.z], [0.0, -1.0, 0.0], [1.0, 1.0]),
([max.x, min.y, min.z], [0.0, -1.0, 0.0], [0.0, 1.0]),
];
let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect();
let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect();
let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect();
let indices = Indices::U32(vec![
0, 1, 2, 2, 3, 0, // front
4, 5, 6, 6, 7, 4, // back
8, 9, 10, 10, 11, 8, // right
12, 13, 14, 14, 15, 12, // left
16, 17, 18, 18, 19, 16, // top
20, 21, 22, 22, 23, 20, // bottom
]);
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_indices(Some(indices))
}
}
impl From<Cuboid> for Mesh {
fn from(cuboid: Cuboid) -> Self {
cuboid.mesh()
}
}

View file

@ -0,0 +1,184 @@
use bevy_math::primitives::Cylinder;
use wgpu::PrimitiveTopology;
use crate::{
mesh::{Indices, Mesh, Meshable},
render_asset::RenderAssetUsages,
};
/// A builder used for creating a [`Mesh`] with a [`Cylinder`] shape.
#[derive(Clone, Copy, Debug)]
pub struct CylinderMeshBuilder {
/// The [`Cylinder`] shape.
pub cylinder: Cylinder,
/// The number of vertices used for the top and bottom of the cylinder.
///
/// The default is `32`.
pub resolution: u32,
/// The number of segments along the height of the cylinder.
/// Must be greater than `0` for geometry to be generated.
///
/// The default is `1`.
pub segments: u32,
}
impl Default for CylinderMeshBuilder {
fn default() -> Self {
Self {
cylinder: Cylinder::default(),
resolution: 32,
segments: 1,
}
}
}
impl CylinderMeshBuilder {
/// Creates a new [`CylinderMeshBuilder`] from the given radius, a height,
/// and a resolution used for the top and bottom.
#[inline]
pub fn new(radius: f32, height: f32, resolution: u32) -> Self {
Self {
cylinder: Cylinder::new(radius, height),
resolution,
..Default::default()
}
}
/// Sets the number of vertices used for the top and bottom of the cylinder.
#[inline]
pub const fn resolution(mut self, resolution: u32) -> Self {
self.resolution = resolution;
self
}
/// Sets the number of segments along the height of the cylinder.
/// Must be greater than `0` for geometry to be generated.
#[inline]
pub const fn segments(mut self, segments: u32) -> Self {
self.segments = segments;
self
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
let resolution = self.resolution;
let segments = self.segments;
debug_assert!(resolution > 2);
debug_assert!(segments > 0);
let num_rings = segments + 1;
let num_vertices = resolution * 2 + num_rings * (resolution + 1);
let num_faces = resolution * (num_rings - 2);
let num_indices = (2 * num_faces + 2 * (resolution - 1) * 2) * 3;
let mut positions = Vec::with_capacity(num_vertices as usize);
let mut normals = Vec::with_capacity(num_vertices as usize);
let mut uvs = Vec::with_capacity(num_vertices as usize);
let mut indices = Vec::with_capacity(num_indices as usize);
let step_theta = std::f32::consts::TAU / resolution as f32;
let step_y = 2.0 * self.cylinder.half_height / segments as f32;
// rings
for ring in 0..num_rings {
let y = -self.cylinder.half_height + ring as f32 * step_y;
for segment in 0..=resolution {
let theta = segment as f32 * step_theta;
let (sin, cos) = theta.sin_cos();
positions.push([self.cylinder.radius * cos, y, self.cylinder.radius * sin]);
normals.push([cos, 0., sin]);
uvs.push([
segment as f32 / resolution as f32,
ring as f32 / segments as f32,
]);
}
}
// barrel skin
for i in 0..segments {
let ring = i * (resolution + 1);
let next_ring = (i + 1) * (resolution + 1);
for j in 0..resolution {
indices.extend_from_slice(&[
ring + j,
next_ring + j,
ring + j + 1,
next_ring + j,
next_ring + j + 1,
ring + j + 1,
]);
}
}
// caps
let mut build_cap = |top: bool| {
let offset = positions.len() as u32;
let (y, normal_y, winding) = if top {
(self.cylinder.half_height, 1., (1, 0))
} else {
(-self.cylinder.half_height, -1., (0, 1))
};
for i in 0..self.resolution {
let theta = i as f32 * step_theta;
let (sin, cos) = theta.sin_cos();
positions.push([cos * self.cylinder.radius, y, sin * self.cylinder.radius]);
normals.push([0.0, normal_y, 0.0]);
uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]);
}
for i in 1..(self.resolution - 1) {
indices.extend_from_slice(&[
offset,
offset + i + winding.0,
offset + i + winding.1,
]);
}
};
// top
build_cap(true);
build_cap(false);
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_indices(Some(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)
}
}
impl Meshable for Cylinder {
type Output = CylinderMeshBuilder;
fn mesh(&self) -> Self::Output {
CylinderMeshBuilder {
cylinder: *self,
..Default::default()
}
}
}
impl From<Cylinder> for Mesh {
fn from(cylinder: Cylinder) -> Self {
cylinder.mesh().build()
}
}
impl From<CylinderMeshBuilder> for Mesh {
fn from(cylinder: CylinderMeshBuilder) -> Self {
cylinder.build()
}
}

View file

@ -0,0 +1,12 @@
mod capsule;
mod cuboid;
mod cylinder;
mod plane;
mod sphere;
mod torus;
pub use capsule::*;
pub use cylinder::*;
pub use plane::*;
pub use sphere::*;
pub use torus::*;

View file

@ -0,0 +1,110 @@
use bevy_math::{
primitives::{Direction3d, Plane3d},
Quat, Vec2, Vec3,
};
use wgpu::PrimitiveTopology;
use crate::{
mesh::{Indices, Mesh, Meshable},
render_asset::RenderAssetUsages,
};
/// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape.
#[derive(Clone, Copy, Debug)]
pub struct PlaneMeshBuilder {
/// The [`Plane3d`] shape.
pub plane: Plane3d,
/// Half the size of the plane mesh.
pub half_size: Vec2,
}
impl Default for PlaneMeshBuilder {
fn default() -> Self {
Self {
plane: Plane3d::default(),
half_size: Vec2::ONE,
}
}
}
impl PlaneMeshBuilder {
/// Creates a new [`PlaneMeshBuilder`] from a given normal and size.
#[inline]
pub fn new(normal: Direction3d, size: Vec2) -> Self {
Self {
plane: Plane3d { normal },
half_size: size / 2.0,
}
}
/// Creates a new [`PlaneMeshBuilder`] from the given size, with the normal pointing upwards.
#[inline]
pub fn from_size(size: Vec2) -> Self {
Self {
half_size: size / 2.0,
..Default::default()
}
}
/// Sets the normal of the plane, aka the direction the plane is facing.
#[inline]
#[doc(alias = "facing")]
pub fn normal(mut self, normal: Direction3d) -> Self {
self.plane = Plane3d { normal };
self
}
/// Sets the size of the plane mesh.
#[inline]
pub fn size(mut self, width: f32, height: f32) -> Self {
self.half_size = Vec2::new(width, height) / 2.0;
self
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
let rotation = Quat::from_rotation_arc(Vec3::Y, *self.plane.normal);
let positions = vec![
rotation * Vec3::new(self.half_size.x, 0.0, -self.half_size.y),
rotation * Vec3::new(-self.half_size.x, 0.0, -self.half_size.y),
rotation * Vec3::new(-self.half_size.x, 0.0, self.half_size.y),
rotation * Vec3::new(self.half_size.x, 0.0, self.half_size.y),
];
let normals = vec![self.plane.normal.to_array(); 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_indices(Some(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 Plane3d {
type Output = PlaneMeshBuilder;
fn mesh(&self) -> Self::Output {
PlaneMeshBuilder {
plane: *self,
..Default::default()
}
}
}
impl From<Plane3d> for Mesh {
fn from(plane: Plane3d) -> Self {
plane.mesh().build()
}
}
impl From<PlaneMeshBuilder> for Mesh {
fn from(plane: PlaneMeshBuilder) -> Self {
plane.build()
}
}

View file

@ -0,0 +1,268 @@
use std::f32::consts::PI;
use crate::{
mesh::{Indices, Mesh, Meshable},
render_asset::RenderAssetUsages,
};
use bevy_math::primitives::Sphere;
use hexasphere::shapes::IcoSphere;
use thiserror::Error;
use wgpu::PrimitiveTopology;
/// An error when creating an icosphere [`Mesh`] from a [`SphereMeshBuilder`].
#[derive(Clone, Copy, Debug, Error)]
pub enum IcosphereError {
/// The icosphere has too many vertices.
#[error("Cannot create an icosphere of {subdivisions} subdivisions due to there being too many vertices being generated: {number_of_resulting_points}. (Limited to 65535 vertices or 79 subdivisions)")]
TooManyVertices {
/// The number of subdivisions used. 79 is the largest allowed value for a mesh to be generated.
subdivisions: usize,
/// The number of vertices generated. 65535 is the largest allowed value for a mesh to be generated.
number_of_resulting_points: usize,
},
}
/// A type of sphere mesh.
#[derive(Clone, Copy, Debug)]
pub enum SphereKind {
/// An icosphere, a spherical mesh that consists of equally sized triangles.
Ico {
/// The number of subdivisions applied.
/// The number of faces quadruples with each subdivision.
subdivisions: usize,
},
/// A UV sphere, a spherical mesh that consists of quadrilaterals
/// apart from triangles at the top and bottom.
Uv {
/// The number of longitudinal sectors, aka the horizontal resolution.
#[doc(alias = "horizontal_resolution")]
sectors: usize,
/// The number of latitudinal stacks, aka the vertical resolution.
#[doc(alias = "vertical_resolution")]
stacks: usize,
},
}
impl Default for SphereKind {
fn default() -> Self {
Self::Ico { subdivisions: 5 }
}
}
/// A builder used for creating a [`Mesh`] with an [`Sphere`] shape.
#[derive(Clone, Copy, Debug, Default)]
pub struct SphereMeshBuilder {
/// The [`Sphere`] shape.
pub sphere: Sphere,
/// The type of sphere mesh that will be built.
pub kind: SphereKind,
}
impl SphereMeshBuilder {
/// Creates a new [`SphereMeshBuilder`] from a radius and [`SphereKind`].
#[inline]
pub const fn new(radius: f32, kind: SphereKind) -> Self {
Self {
sphere: Sphere { radius },
kind,
}
}
/// Sets the [`SphereKind`] that will be used for building the mesh.
#[inline]
pub const fn kind(mut self, kind: SphereKind) -> Self {
self.kind = kind;
self
}
/// Builds a [`Mesh`] according to the configuration in `self`.
///
/// # Panics
///
/// Panics if the sphere is a [`SphereKind::Ico`] with a subdivision count
/// that is greater than or equal to `80` because there will be too many vertices.
pub fn build(&self) -> Mesh {
match self.kind {
SphereKind::Ico { subdivisions } => self.ico(subdivisions).unwrap(),
SphereKind::Uv { sectors, stacks } => self.uv(sectors, stacks),
}
}
/// Creates an icosphere mesh with the given number of subdivisions.
///
/// The number of faces quadruples with each subdivision.
/// If there are `80` or more subdivisions, the vertex count will be too large,
/// and an [`IcosphereError`] is returned.
///
/// A good default is `5` subdivisions.
pub fn ico(&self, subdivisions: usize) -> Result<Mesh, IcosphereError> {
if subdivisions >= 80 {
/*
Number of triangles:
N = 20
Number of edges:
E = 30
Number of vertices:
V = 12
Number of points within a triangle (triangular numbers):
inner(s) = (s^2 + s) / 2
Number of points on an edge:
edges(s) = s
Add up all vertices on the surface:
vertices(s) = edges(s) * E + inner(s - 1) * N + V
Expand and simplify. Notice that the triangular number formula has roots at -1, and 0, so translating it one to the right fixes it.
subdivisions(s) = 30s + 20((s^2 - 2s + 1 + s - 1) / 2) + 12
subdivisions(s) = 30s + 10s^2 - 10s + 12
subdivisions(s) = 10(s^2 + 2s) + 12
Factor an (s + 1) term to simplify in terms of calculation
subdivisions(s) = 10(s + 1)^2 + 12 - 10
resulting_vertices(s) = 10(s + 1)^2 + 2
*/
let temp = subdivisions + 1;
let number_of_resulting_points = temp * temp * 10 + 2;
return Err(IcosphereError::TooManyVertices {
subdivisions,
number_of_resulting_points,
});
}
let generated = IcoSphere::new(subdivisions, |point| {
let inclination = point.y.acos();
let azimuth = point.z.atan2(point.x);
let norm_inclination = inclination / std::f32::consts::PI;
let norm_azimuth = 0.5 - (azimuth / std::f32::consts::TAU);
[norm_azimuth, norm_inclination]
});
let raw_points = generated.raw_points();
let points = raw_points
.iter()
.map(|&p| (p * self.sphere.radius).into())
.collect::<Vec<[f32; 3]>>();
let normals = raw_points
.iter()
.copied()
.map(Into::into)
.collect::<Vec<[f32; 3]>>();
let uvs = generated.raw_data().to_owned();
let mut indices = Vec::with_capacity(generated.indices_per_main_triangle() * 20);
for i in 0..20 {
generated.get_indices(i, &mut indices);
}
let indices = Indices::U32(indices);
Ok(Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_indices(Some(indices))
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs))
}
/// Creates a UV sphere [`Mesh`] with the given number of
/// longitudinal sectors and latitudinal stacks, aka horizontal and vertical resolution.
///
/// A good default is `32` sectors and `18` stacks.
pub fn uv(&self, sectors: usize, stacks: usize) -> Mesh {
// Largely inspired from http://www.songho.ca/opengl/gl_sphere.html
let sectors_f32 = sectors as f32;
let stacks_f32 = stacks as f32;
let length_inv = 1. / self.sphere.radius;
let sector_step = 2. * PI / sectors_f32;
let stack_step = PI / stacks_f32;
let mut vertices: Vec<[f32; 3]> = Vec::with_capacity(stacks * sectors);
let mut normals: Vec<[f32; 3]> = Vec::with_capacity(stacks * sectors);
let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(stacks * sectors);
let mut indices: Vec<u32> = Vec::with_capacity(stacks * sectors * 2 * 3);
for i in 0..stacks + 1 {
let stack_angle = PI / 2. - (i as f32) * stack_step;
let xy = self.sphere.radius * stack_angle.cos();
let z = self.sphere.radius * stack_angle.sin();
for j in 0..sectors + 1 {
let sector_angle = (j as f32) * sector_step;
let x = xy * sector_angle.cos();
let y = xy * sector_angle.sin();
vertices.push([x, y, z]);
normals.push([x * length_inv, y * length_inv, z * length_inv]);
uvs.push([(j as f32) / sectors_f32, (i as f32) / stacks_f32]);
}
}
// indices
// k1--k1+1
// | / |
// | / |
// k2--k2+1
for i in 0..stacks {
let mut k1 = i * (sectors + 1);
let mut k2 = k1 + sectors + 1;
for _j in 0..sectors {
if i != 0 {
indices.push(k1 as u32);
indices.push(k2 as u32);
indices.push((k1 + 1) as u32);
}
if i != stacks - 1 {
indices.push((k1 + 1) as u32);
indices.push(k2 as u32);
indices.push((k2 + 1) as u32);
}
k1 += 1;
k2 += 1;
}
}
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_indices(Some(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)
}
}
impl Meshable for Sphere {
type Output = SphereMeshBuilder;
fn mesh(&self) -> Self::Output {
SphereMeshBuilder {
sphere: *self,
..Default::default()
}
}
}
impl From<Sphere> for Mesh {
fn from(sphere: Sphere) -> Self {
sphere.mesh().build()
}
}
impl From<SphereMeshBuilder> for Mesh {
fn from(sphere: SphereMeshBuilder) -> Self {
sphere.build()
}
}

View file

@ -0,0 +1,166 @@
use bevy_math::{primitives::Torus, Vec3};
use wgpu::PrimitiveTopology;
use crate::{
mesh::{Indices, Mesh, Meshable},
render_asset::RenderAssetUsages,
};
/// A builder used for creating a [`Mesh`] with a [`Torus`] shape.
#[derive(Clone, Copy, Debug)]
pub struct TorusMeshBuilder {
/// The [`Torus`] shape.
pub torus: Torus,
/// The number of vertices used for each circular segment
/// in the ring or tube of the torus.
///
/// The default is `24`.
pub minor_resolution: usize,
/// The number of segments used for the main ring of the torus.
///
/// A resolution of `4` would make the torus appear rectangular,
/// while a resolution of `32` resembles a circular ring.
///
/// The default is `32`.
pub major_resolution: usize,
}
impl Default for TorusMeshBuilder {
fn default() -> Self {
Self {
torus: Torus::default(),
minor_resolution: 24,
major_resolution: 32,
}
}
}
impl TorusMeshBuilder {
/// Creates a new [`TorusMeshBuilder`] from an inner and outer radius.
///
/// The inner radius is the radius of the hole, and the outer radius
/// is the radius of the entire object.
#[inline]
pub fn new(inner_radius: f32, outer_radius: f32) -> Self {
Self {
torus: Torus::new(inner_radius, outer_radius),
..Default::default()
}
}
/// Sets the number of vertices used for each circular segment
/// in the ring or tube of the torus.
#[inline]
pub const fn minor_resolution(mut self, resolution: usize) -> Self {
self.minor_resolution = resolution;
self
}
/// Sets the number of segments used for the main ring of the torus.
///
/// A resolution of `4` would make the torus appear rectangular,
/// while a resolution of `32` resembles a circular ring.
#[inline]
pub const fn major_resolution(mut self, resolution: usize) -> Self {
self.major_resolution = resolution;
self
}
/// Builds a [`Mesh`] according to the configuration in `self`.
pub fn build(&self) -> Mesh {
// code adapted from http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html
let n_vertices = (self.major_resolution + 1) * (self.minor_resolution + 1);
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(n_vertices);
let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n_vertices);
let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n_vertices);
let segment_stride = 2.0 * std::f32::consts::PI / self.major_resolution as f32;
let side_stride = 2.0 * std::f32::consts::PI / self.minor_resolution as f32;
for segment in 0..=self.major_resolution {
let theta = segment_stride * segment as f32;
for side in 0..=self.minor_resolution {
let phi = side_stride * side as f32;
let position = Vec3::new(
theta.cos() * (self.torus.major_radius + self.torus.minor_radius * phi.cos()),
self.torus.minor_radius * phi.sin(),
theta.sin() * (self.torus.major_radius + self.torus.minor_radius * phi.cos()),
);
let center = Vec3::new(
self.torus.major_radius * theta.cos(),
0.,
self.torus.major_radius * theta.sin(),
);
let normal = (position - center).normalize();
positions.push(position.into());
normals.push(normal.into());
uvs.push([
segment as f32 / self.major_resolution as f32,
side as f32 / self.minor_resolution as f32,
]);
}
}
let n_faces = (self.major_resolution) * (self.minor_resolution);
let n_triangles = n_faces * 2;
let n_indices = n_triangles * 3;
let mut indices: Vec<u32> = Vec::with_capacity(n_indices);
let n_vertices_per_row = self.minor_resolution + 1;
for segment in 0..self.major_resolution {
for side in 0..self.minor_resolution {
let lt = side + segment * n_vertices_per_row;
let rt = (side + 1) + segment * n_vertices_per_row;
let lb = side + (segment + 1) * n_vertices_per_row;
let rb = (side + 1) + (segment + 1) * n_vertices_per_row;
indices.push(lt as u32);
indices.push(rt as u32);
indices.push(lb as u32);
indices.push(rt as u32);
indices.push(rb as u32);
indices.push(lb as u32);
}
}
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_indices(Some(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)
}
}
impl Meshable for Torus {
type Output = TorusMeshBuilder;
fn mesh(&self) -> Self::Output {
TorusMeshBuilder {
torus: *self,
..Default::default()
}
}
}
impl From<Torus> for Mesh {
fn from(torus: Torus) -> Self {
torus.mesh().build()
}
}
impl From<TorusMeshBuilder> for Mesh {
fn from(torus: TorusMeshBuilder) -> Self {
torus.build()
}
}

View file

@ -22,6 +22,9 @@
mod dim2;
pub use dim2::{CircleMeshBuilder, EllipseMeshBuilder};
mod dim3;
pub use dim3::*;
/// 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)

View file

@ -23,7 +23,7 @@ fn main() {
#[derive(Component)]
struct Shape;
const X_EXTENT: f32 = 14.5;
const X_EXTENT: f32 = 12.0;
fn setup(
mut commands: Commands,
@ -37,13 +37,12 @@ fn setup(
});
let shapes = [
meshes.add(shape::Cube::default()),
meshes.add(shape::Box::default()),
meshes.add(shape::Capsule::default()),
meshes.add(shape::Torus::default()),
meshes.add(shape::Cylinder::default()),
meshes.add(Mesh::try_from(shape::Icosphere::default()).unwrap()),
meshes.add(shape::UVSphere::default()),
meshes.add(Cuboid::default()),
meshes.add(Capsule3d::default()),
meshes.add(Torus::default()),
meshes.add(Cylinder::default()),
meshes.add(Sphere::default().mesh().ico(5).unwrap()),
meshes.add(Sphere::default().mesh().uv(32, 18)),
];
let num_shapes = shapes.len();
@ -78,7 +77,7 @@ fn setup(
// ground plane
commands.spawn(PbrBundle {
mesh: meshes.add(shape::Plane::from_size(50.0)),
mesh: meshes.add(Plane3d::default().mesh().size(50.0, 50.0)),
material: materials.add(Color::SILVER),
..default()
});