diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index f882487745f5b..4eca0869727f5 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -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. /// diff --git a/crates/bevy_render/src/mesh/primitives/dim3/capsule.rs b/crates/bevy_render/src/mesh/primitives/dim3/capsule.rs new file mode 100644 index 0000000000000..b7f36e2a87b79 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/capsule.rs @@ -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 = vec![Vec3::ZERO; vert_len]; + let mut vts: Vec = vec![Vec2::ZERO; vert_len]; + let mut vns: Vec = 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 = vec![Vec2::ZERO; longitudes]; + let mut rho_theta_cartesian: Vec = vec![Vec2::ZERO; longitudes]; + let mut s_texture_cache: Vec = 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 = 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 for Mesh { + fn from(capsule: Capsule3d) -> Self { + capsule.mesh().build() + } +} + +impl From for Mesh { + fn from(capsule: Capsule3dMeshBuilder) -> Self { + capsule.build() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/cuboid.rs b/crates/bevy_render/src/mesh/primitives/dim3/cuboid.rs new file mode 100644 index 0000000000000..3ad77d60c93f5 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/cuboid.rs @@ -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 for Mesh { + fn from(cuboid: Cuboid) -> Self { + cuboid.mesh() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs b/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs new file mode 100644 index 0000000000000..26aaa4792ee78 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs @@ -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 for Mesh { + fn from(cylinder: Cylinder) -> Self { + cylinder.mesh().build() + } +} + +impl From for Mesh { + fn from(cylinder: CylinderMeshBuilder) -> Self { + cylinder.build() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs new file mode 100644 index 0000000000000..e2b337bb31934 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs @@ -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::*; diff --git a/crates/bevy_render/src/mesh/primitives/dim3/plane.rs b/crates/bevy_render/src/mesh/primitives/dim3/plane.rs new file mode 100644 index 0000000000000..5744ea894a3cc --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/plane.rs @@ -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 for Mesh { + fn from(plane: Plane3d) -> Self { + plane.mesh().build() + } +} + +impl From for Mesh { + fn from(plane: PlaneMeshBuilder) -> Self { + plane.build() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs b/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs new file mode 100644 index 0000000000000..dc1663aea3078 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs @@ -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 { + 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::>(); + + let normals = raw_points + .iter() + .copied() + .map(Into::into) + .collect::>(); + + 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 = 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 for Mesh { + fn from(sphere: Sphere) -> Self { + sphere.mesh().build() + } +} + +impl From for Mesh { + fn from(sphere: SphereMeshBuilder) -> Self { + sphere.build() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/torus.rs b/crates/bevy_render/src/mesh/primitives/dim3/torus.rs new file mode 100644 index 0000000000000..1dd444e04a4b7 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/torus.rs @@ -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 = 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 for Mesh { + fn from(torus: Torus) -> Self { + torus.mesh().build() + } +} + +impl From for Mesh { + fn from(torus: TorusMeshBuilder) -> Self { + torus.build() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/mod.rs b/crates/bevy_render/src/mesh/primitives/mod.rs index de9e7f538984c..a2bb01599b4ee 100644 --- a/crates/bevy_render/src/mesh/primitives/mod.rs +++ b/crates/bevy_render/src/mesh/primitives/mod.rs @@ -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) diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index ec69ddfa42ae4..fe6135cca62aa 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -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() });