diff --git a/assets/docs/Cone.drawio b/assets/docs/Cone.drawio new file mode 100644 index 0000000000000..54cb92c04690f --- /dev/null +++ b/assets/docs/Cone.drawio @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/docs/Cone.png b/assets/docs/Cone.png new file mode 100644 index 0000000000000..37025fea3b29d Binary files /dev/null and b/assets/docs/Cone.png differ diff --git a/crates/bevy_render/src/mesh/shape/cone.rs b/crates/bevy_render/src/mesh/shape/cone.rs new file mode 100644 index 0000000000000..cf9f5bf573763 --- /dev/null +++ b/crates/bevy_render/src/mesh/shape/cone.rs @@ -0,0 +1,173 @@ +use crate::mesh::{Indices, Mesh}; +use bevy_math::Vec3; +use wgpu::PrimitiveTopology; + +/// A cone which stands on the XZ plane. +/// +/// If either radius is 0., the tip will be sharp. Otherwise, the cone will appear truncated, with flat surfaces on either end. +/// +/// # Diagram +/// +/// See an illustrative diagram [here](https://github.com/bevyengine/bevy/blob/main/assets/docs/Cone.png). +/// The diagram illustrates the following properties: +/// +/// * The height of the cone is evenly split by the XZ plane, so half height above, half below. +/// * A resolution of 8 means each horizontal (XZ) face has 8 vertices. +/// * A top radius of 0.0 means a sharp tip. +/// * 3 segments (divided by the dotted lines) means there are 3 segments of quads enveloping the cone. +/// +/// Note that the sharp tip still contains the same amount of vertices as the bottom. +#[derive(Clone, Copy, Debug)] +pub struct Cone { + /// Radius in the XZ plane at the upper face. + /// + /// Can be zero for a sharp tip, but both top and bottom should not be zero at the same time. + pub top_radius: f32, + + /// Radius in the XZ plane at the bottom face. + /// + /// Can be zero for a sharp tip, but both top and bottom should not be zero at the same time. + pub bottom_radius: f32, + + /// Height of the cone in the Y axis. + /// + /// Should be greater than zero. + pub height: f32, + + /// The number of vertices around each horizontal slice of the cone. If you are looking at the cone from + /// above, this is the number of points you will see on the circle. + /// A higher number will make it appear more circular. + /// + /// Should be above two. + pub resolution: u32, + + /// The number of segments between the two ends. Setting this to 1 will have triangles spanning the full + /// height of the cone. Setting it to 2 will have two sets of triangles with a horizontal slice in the middle of + /// the cone. Greater numbers increase triangles/slices in the same way. + /// + /// Should be greater than zero. + pub segments: u32, +} + +impl Default for Cone { + fn default() -> Self { + Self { + top_radius: 0.0, + bottom_radius: 0.5, + height: 1.0, + resolution: 64, + segments: 4, + } + } +} + +impl From for Mesh { + fn from(c: Cone) -> Self { + debug_assert!(c.top_radius >= 0.0); + debug_assert!(c.bottom_radius >= 0.0); + debug_assert!(!(c.bottom_radius == 0.0 && c.top_radius == 0.0)); + debug_assert!(c.height > 0.0); + debug_assert!(c.resolution > 2); + debug_assert!(c.segments > 0); + + let num_rings = c.segments + 1; + let num_vertices = c.resolution * 2 + num_rings * (c.resolution + 1); + let num_faces = c.resolution * (num_rings - 2); + let num_indices = (2 * num_faces + 2 * (c.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 / c.resolution as f32; + let step_y = c.height / c.segments as f32; + let step_radius = (c.top_radius - c.bottom_radius) / c.segments as f32; + + // rings + + for ring in 0..num_rings { + let y = -c.height / 2.0 + ring as f32 * step_y; + let radius = c.bottom_radius + ring as f32 * step_radius; + + for segment in 0..=c.resolution { + let theta = segment as f32 * step_theta; + let (sin, cos) = theta.sin_cos(); + + positions.push([radius * cos, y, radius * sin]); + normals.push( + Vec3::new(cos, (c.bottom_radius - c.top_radius) / c.height, sin) + .normalize() + .to_array(), + ); + uvs.push([ + segment as f32 / c.resolution as f32, + ring as f32 / c.segments as f32, + ]); + } + } + + // barrel skin + + for i in 0..c.segments { + let ring = i * (c.resolution + 1); + let next_ring = (i + 1) * (c.resolution + 1); + + for j in 0..c.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 { + (c.height / 2., 1., (1, 0)) + } else { + (c.height / -2., -1., (0, 1)) + }; + + let radius = if top { c.top_radius } else { c.bottom_radius }; + + if radius == 0.0 { + return; + } + + for i in 0..c.resolution { + let theta = i as f32 * step_theta; + let (sin, cos) = theta.sin_cos(); + + positions.push([cos * radius, y, sin * 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..(c.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) + .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) + } +} diff --git a/crates/bevy_render/src/mesh/shape/cylinder.rs b/crates/bevy_render/src/mesh/shape/cylinder.rs index a4d517ac73952..a5a97ee094c48 100644 --- a/crates/bevy_render/src/mesh/shape/cylinder.rs +++ b/crates/bevy_render/src/mesh/shape/cylinder.rs @@ -1,11 +1,13 @@ -use crate::mesh::{Indices, Mesh}; -use wgpu::PrimitiveTopology; +use crate::mesh::Mesh; + +use super::Cone; /// A cylinder which stands on the XZ plane #[derive(Clone, Copy, Debug)] pub struct Cylinder { /// Radius in the XZ plane. pub radius: f32, + /// Height of the cylinder in the Y axis. pub height: f32, /// The number of vertices around each horizontal slice of the cylinder. If you are looking at the cylinder from @@ -31,97 +33,13 @@ impl Default for Cylinder { impl From for Mesh { fn from(c: Cylinder) -> Self { - debug_assert!(c.radius > 0.0); - debug_assert!(c.height > 0.0); - debug_assert!(c.resolution > 2); - debug_assert!(c.segments > 0); - - let num_rings = c.segments + 1; - let num_vertices = c.resolution * 2 + num_rings * (c.resolution + 1); - let num_faces = c.resolution * (num_rings - 2); - let num_indices = (2 * num_faces + 2 * (c.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 / c.resolution as f32; - let step_y = c.height / c.segments as f32; - - // rings - - for ring in 0..num_rings { - let y = -c.height / 2.0 + ring as f32 * step_y; - - for segment in 0..=c.resolution { - let theta = segment as f32 * step_theta; - let (sin, cos) = theta.sin_cos(); - - positions.push([c.radius * cos, y, c.radius * sin]); - normals.push([cos, 0., sin]); - uvs.push([ - segment as f32 / c.resolution as f32, - ring as f32 / c.segments as f32, - ]); - } + Cone { + top_radius: c.radius, + bottom_radius: c.radius, + height: c.height, + resolution: c.resolution, + segments: c.segments, } - - // barrel skin - - for i in 0..c.segments { - let ring = i * (c.resolution + 1); - let next_ring = (i + 1) * (c.resolution + 1); - - for j in 0..c.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 { - (c.height / 2., 1., (1, 0)) - } else { - (c.height / -2., -1., (0, 1)) - }; - - for i in 0..c.resolution { - let theta = i as f32 * step_theta; - let (sin, cos) = theta.sin_cos(); - - positions.push([cos * c.radius, y, sin * c.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..(c.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) - .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) + .into() } } diff --git a/crates/bevy_render/src/mesh/shape/mod.rs b/crates/bevy_render/src/mesh/shape/mod.rs index c9f6b9e1492bf..10927bd650d3d 100644 --- a/crates/bevy_render/src/mesh/shape/mod.rs +++ b/crates/bevy_render/src/mesh/shape/mod.rs @@ -262,6 +262,7 @@ impl From for Mesh { } mod capsule; +mod cone; mod cylinder; mod icosphere; mod regular_polygon; @@ -269,6 +270,7 @@ mod torus; mod uvsphere; pub use capsule::{Capsule, CapsuleUvProfile}; +pub use cone::Cone; pub use cylinder::Cylinder; pub use icosphere::Icosphere; pub use regular_polygon::{Circle, RegularPolygon}; diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index 999d11a74c878..0123b7b888e44 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -20,7 +20,7 @@ fn main() { #[derive(Component)] struct Shape; -const X_EXTENT: f32 = 14.5; +const X_EXTENT: f32 = 16.0; fn setup( mut commands: Commands, @@ -39,6 +39,7 @@ fn setup( meshes.add(shape::Capsule::default().into()), meshes.add(shape::Torus::default().into()), meshes.add(shape::Cylinder::default().into()), + meshes.add(shape::Cone::default().into()), meshes.add(shape::Icosphere::default().try_into().unwrap()), meshes.add(shape::UVSphere::default().into()), ];