├── assets ├── fonts │ └── FiraSans-Medium.ttf ├── screenshots │ └── screenshot.png └── textures │ └── checkerboard_1024x1024.png ├── LICENSE ├── .gitignore ├── src ├── lib.rs ├── util.rs ├── grid.rs ├── cone.rs ├── polygon.rs ├── torus.rs ├── cylinder.rs └── tube.rs ├── Cargo.toml ├── migrations.md ├── README.md └── examples └── gallery.rs /assets/fonts/FiraSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redpandamonium/bevy_more_shapes/HEAD/assets/fonts/FiraSans-Medium.ttf -------------------------------------------------------------------------------- /assets/screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redpandamonium/bevy_more_shapes/HEAD/assets/screenshots/screenshot.png -------------------------------------------------------------------------------- /assets/textures/checkerboard_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redpandamonium/bevy_more_shapes/HEAD/assets/textures/checkerboard_1024x1024.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is licensed under either MIT or Apache 2.0 license at your option. 2 | 3 | * MIT: http://opensource.org/licenses/MIT 4 | * Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 | 6 | The included FiraSans font is licensed under the Open Font License, see https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL. 7 | The checkerboard texture is licensed under Creative Commons CC0, see https://creativecommons.org/publicdomain/zero/1.0/legalcode. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # IntelliJ meta info directory 13 | .idea/ 14 | 15 | 16 | # Added by cargo 17 | # 18 | # already existing elements were commented out 19 | 20 | /target 21 | #Cargo.lock 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cone; 2 | pub mod cylinder; 3 | pub mod grid; 4 | pub mod polygon; 5 | pub mod torus; 6 | pub mod tube; 7 | pub(crate) mod util; 8 | 9 | struct MeshData { 10 | positions: Vec, 11 | normals: Vec, 12 | uvs: Vec, 13 | indices: Vec, 14 | } 15 | 16 | impl MeshData { 17 | fn new(num_vertices: usize, num_indices: usize) -> Self { 18 | Self { 19 | positions: Vec::with_capacity(num_vertices as usize), 20 | normals: Vec::with_capacity(num_vertices as usize), 21 | uvs: Vec::with_capacity(num_vertices as usize), 22 | indices: Vec::with_capacity(num_indices as usize), 23 | } 24 | } 25 | } 26 | 27 | use bevy::prelude::{Vec2, Vec3}; 28 | pub use crate::cone::Cone; 29 | pub use crate::cylinder::Cylinder; 30 | pub use crate::grid::Grid; 31 | pub use crate::polygon::Polygon; 32 | pub use crate::torus::Torus; -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_more_shapes" 3 | version = "0.5.0" 4 | edition = "2021" 5 | authors = ["Leon Suchy"] 6 | license = "MIT OR Apache-2.0" 7 | description = "Bevy engine plugin that adds additional shapes to the existing collection of procedurally generated geometry." 8 | readme = "README.md" 9 | keywords = ["gamedev", "graphics", "bevy", "geometry", "shapes"] 10 | categories = ["graphics", "game-development"] 11 | repository = "https://github.com/redpandamonium/bevy_more_shapes" 12 | exclude = ["/assets/screenshots/**"] 13 | 14 | [dependencies] 15 | # The parts of bevy relevant to meshes and shapes as well as math 16 | bevy = { version = "0.10", features = ["bevy_render"] } 17 | # This crate is used to triangulate polygons. 18 | triangulate = "0.2.0" 19 | 20 | [dev-dependencies] 21 | # For the showcase we need the full capabilities of the engine. 22 | bevy = "0.10" 23 | # First person camera controller 24 | smooth-bevy-cameras = "0.8" 25 | bevy_normal_material = "0.2.1" 26 | -------------------------------------------------------------------------------- /migrations.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | This guide documents all breaking changes as well as how to migrate from one version to the next. 4 | If a version is not in here, there were no breaking changes. 5 | 6 | ## 0.3.x -> 0.4.0 7 | 8 | * The Torus shape has had its fields renamed to avoid confusion. Horizontal -> Radial, Vertical -> Tube 9 | * The Torus shape has gained 2 parameters, set them to 2pi for the old behavior 10 | * The default Torus has doubled its segments but is the same otherwise 11 | * Polygon now implements Mesh::try_from instead of Mesh::from, because the underlying library now returns an error if the input data was malformed 12 | 13 | ## 0.4 -> 0.5 14 | 15 | * The cylinder shape supports height segments now. Default is 1 which is the old behavior. 16 | * The cylinder UVs have been reworked to make more sense 17 | * The cylinder normals have been fixed to account for the slope on irregular cylinders 18 | * Cone segment parameter was renamed 19 | * Cone UVs were redone to make more sense 20 | * Cone normals have been fixed to account for the slope -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_more_shapes 2 | ![crates.io](https://img.shields.io/crates/v/bevy_more_shapes.svg) 3 | 4 | ![Gallery Screenshot](https://github.com/redpandamonium/bevy_more_shapes/blob/97220661580d93c0e53ce1a0ae68cd02d4fa2cda/assets/screenshots/screenshot.png) 5 | 6 | More shapes for the bevy game engine. This plugin adds more procedural geometry shapes for bevy. 7 | It works exactly like the default bevy shapes. 8 | 9 | To run the example showcasing all the available shapes, run `cargo run --example gallery`. 10 | 11 | ## Features 12 | 13 | * Cones 14 | * Cylinders 15 | * Grid planes 16 | * Arbitrary non-self-intersecting polygons 17 | * Torus (Including segmented torus) 18 | * Tubes that follow an arbitrary 3d curve 19 | 20 | ## Versions 21 | 22 | This crate tracks bevy's versions. It also follows the semver standard. 23 | Below is a chart which versions of this crate are compatible with which bevy version: 24 | 25 | | Version | Bevy version | 26 | |---------|--------------| 27 | | 0.1.x | 0.6.x | 28 | | 0.2.x | 0.7.x | 29 | | 0.3.x | 0.9.x | 30 | | 0.4.x | 0.10.x | 31 | | 0.5.x | 0.10.x | 32 | 33 | ## Known Issues 34 | 35 | The normals on cones and cylinders aren't properly smoothly interpolated. 36 | 37 | ## Contributing 38 | 39 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as stated in the LICENSE file, without any additional terms or conditions. 40 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::Vec3; 2 | 3 | // When indexing a mesh we commonly find flat (occupying a 2 dimensional subspace) trapezes. 4 | #[derive(Copy, Clone)] 5 | pub(crate) struct FlatTrapezeIndices { 6 | pub lower_left: u32, 7 | pub upper_left: u32, 8 | pub lower_right: u32, 9 | pub upper_right: u32, 10 | } 11 | 12 | impl FlatTrapezeIndices { 13 | 14 | // Triangulate the trapeze 15 | pub fn generate_triangles(&self, indices: &mut Vec) { 16 | indices.push(self.upper_left); 17 | indices.push(self.upper_right); 18 | indices.push(self.lower_left); 19 | indices.push(self.upper_right); 20 | indices.push(self.lower_right); 21 | indices.push(self.lower_left); 22 | } 23 | } 24 | 25 | pub(crate) struct Extent { 26 | min: Vec3, 27 | max: Vec3, 28 | } 29 | 30 | impl Extent { 31 | pub fn new() -> Self { 32 | Extent { 33 | min: Vec3::new(f32::MAX, f32::MAX, f32::MAX), 34 | max: Vec3::new(f32::MIN, f32::MIN, f32::MIN), 35 | } 36 | } 37 | 38 | pub fn extend_to_include(&mut self, v: Vec3) { 39 | // unwrap: we know the size of this array statically 40 | self.min.x = f32::min(self.min.x, v.x); 41 | self.min.y = f32::min(self.min.y, v.y); 42 | self.min.z = f32::min(self.min.z, v.z); 43 | self.max.x = f32::max(self.max.x, v.x); 44 | self.max.y = f32::max(self.max.y, v.y); 45 | self.max.z = f32::max(self.max.z, v.z); 46 | } 47 | 48 | pub fn lengths(&self) -> Vec3 { 49 | self.max - self.min 50 | } 51 | 52 | pub fn center(&self) -> Vec3 { 53 | self.min + (self.max - self.min) / 2.0 54 | } 55 | } -------------------------------------------------------------------------------- /src/grid.rs: -------------------------------------------------------------------------------- 1 | use bevy::render::mesh::{Indices, Mesh}; 2 | use bevy::render::render_resource::PrimitiveTopology; 3 | use crate::util::FlatTrapezeIndices; 4 | 5 | pub struct Grid { 6 | /// Length along the x axis 7 | pub width: f32, 8 | /// Length along the z axis 9 | pub height: f32, 10 | /// Segments on the x axis 11 | pub width_segments: usize, 12 | /// Segments on the z axis 13 | pub height_segments: usize, 14 | } 15 | 16 | impl Default for Grid { 17 | fn default() -> Self { 18 | Grid { 19 | width: 1.0, 20 | height: 1.0, 21 | width_segments: 1, 22 | height_segments: 1 23 | } 24 | } 25 | } 26 | 27 | impl Grid { 28 | pub fn new_square(length: f32, segments: usize) -> Self { 29 | Self { 30 | width: length, 31 | height: length, 32 | width_segments: segments, 33 | height_segments: segments 34 | } 35 | } 36 | } 37 | 38 | impl From for Mesh { 39 | fn from(grid: Grid) -> Self { 40 | 41 | // Validate input parameters 42 | assert!(grid.width_segments > 0, "A grid must have segments"); 43 | assert!(grid.height_segments > 0, "A grid must have segments"); 44 | assert!(grid.width > 0.0, "A grid must have positive width"); 45 | assert!(grid.height > 0.0, "A grid must have positive height"); 46 | 47 | let num_points = (grid.height_segments + 1) * (grid.width_segments + 1); 48 | let num_faces = grid.height_segments * grid.width_segments; 49 | 50 | let mut indices : Vec = Vec::with_capacity(6 * num_faces); // two triangles per rectangle 51 | let mut positions : Vec<[f32; 3]> = Vec::with_capacity(num_points); 52 | let mut uvs : Vec<[f32; 2]> = Vec::with_capacity(num_points); 53 | let mut normals : Vec<[f32; 3]> = Vec::with_capacity(num_points); 54 | 55 | // This is used to center the grid on the origin 56 | let width_half = grid.width / 2.0; 57 | let height_half = grid.height / 2.0; 58 | 59 | // The length of a single segment 60 | let x_segment_len = grid.width / grid.width_segments as f32; 61 | let z_segment_len = grid.height / grid.height_segments as f32; 62 | 63 | // The inverse of the segment lengths 64 | let width_segments_inv = 1.0 / grid.width_segments as f32; 65 | let height_segments_inv = 1.0 / grid.height_segments as f32; 66 | 67 | // Generate vertices 68 | for z in 0..grid.height_segments + 1 { 69 | for x in 0..grid.width_segments + 1 { 70 | 71 | positions.push([x as f32 * x_segment_len - width_half, 0.0, z as f32 * z_segment_len - height_half]); 72 | uvs.push([x as f32 * width_segments_inv, z as f32 * height_segments_inv]); 73 | normals.push([0.0, 1.0, 0.0]); 74 | } 75 | } 76 | 77 | // Generate indices 78 | for face_z in 0..grid.height_segments { 79 | for face_x in 0..grid.width_segments { 80 | 81 | let lower_left = face_z * (grid.width_segments + 1) + face_x; 82 | let face = FlatTrapezeIndices { 83 | lower_left: lower_left as u32, 84 | upper_left: (lower_left + (grid.width_segments + 1)) as u32, 85 | lower_right: (lower_left + 1) as u32, 86 | upper_right: (lower_left + 1 + (grid.width_segments + 1)) as u32, 87 | }; 88 | face.generate_triangles(&mut indices); 89 | } 90 | } 91 | 92 | let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); 93 | mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); 94 | mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); 95 | mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); 96 | mesh.set_indices(Some(Indices::U32(indices))); 97 | mesh 98 | } 99 | } -------------------------------------------------------------------------------- /src/cone.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::Vec3; 2 | use bevy::prelude::Vec2; 3 | use bevy::render::mesh::{Indices, Mesh}; 4 | use bevy::render::render_resource::PrimitiveTopology; 5 | use crate::MeshData; 6 | 7 | // From https://github.com/ForesightMiningSoftwareCorporation/bevy_transform_gizmo/ 8 | 9 | #[derive(Debug, Clone, Copy)] 10 | pub struct Cone { 11 | pub radius: f32, 12 | pub height: f32, 13 | pub segments: u32, 14 | } 15 | 16 | impl Default for Cone { 17 | fn default() -> Self { 18 | Cone { 19 | radius: 0.5, 20 | height: 1.0, 21 | segments: 32, 22 | } 23 | } 24 | } 25 | 26 | fn add_bottom(mesh: &mut MeshData, cone: &Cone) { 27 | 28 | let angle_step = std::f32::consts::TAU / cone.segments as f32; 29 | let base_index = mesh.positions.len() as u32; 30 | 31 | // Center 32 | let center_pos = Vec3::new(0.0, -cone.height / 2.0, 0.0); 33 | mesh.positions.push(center_pos); 34 | mesh.uvs.push(Vec2::new(0.5, 0.5)); 35 | mesh.normals.push(-Vec3::Y); 36 | 37 | // Vertices 38 | for i in 0..=cone.segments { 39 | 40 | let theta = i as f32 * angle_step; 41 | let x_unit = f32::cos(theta); 42 | let z_unit = f32::sin(theta); 43 | 44 | let pos = Vec3::new( 45 | cone.radius * x_unit, 46 | -cone.height / 2.0, 47 | cone.radius * z_unit, 48 | ); 49 | let uv = Vec2::new( 50 | (z_unit * 0.5) + 0.5, 51 | (x_unit * -0.5) + 0.5, 52 | ); 53 | 54 | mesh.positions.push(pos); 55 | mesh.uvs.push(uv); 56 | mesh.normals.push(-Vec3::Y) 57 | } 58 | 59 | // Indices 60 | for i in 0..cone.segments { 61 | mesh.indices.push(base_index + i + 1); 62 | mesh.indices.push(base_index + i + 2); 63 | mesh.indices.push(base_index); 64 | } 65 | } 66 | 67 | fn add_body(mesh: &mut MeshData, cone: &Cone) { 68 | 69 | let angle_step = std::f32::consts::TAU / cone.segments as f32; 70 | let base_index = mesh.positions.len() as u32; 71 | 72 | // Add top vertices. We need to add multiple here because their normals differ 73 | for i in 0..cone.segments { 74 | 75 | let theta = i as f32 * angle_step + angle_step / 2.0; 76 | let x_unit = f32::cos(theta); 77 | let z_unit = f32::sin(theta); 78 | 79 | let slope = cone.radius / cone.height; 80 | let normal = Vec3::new(x_unit, slope, z_unit).normalize(); 81 | 82 | mesh.positions.push(Vec3::new(0.0, cone.height / 2.0, 0.0)); 83 | mesh.normals.push(normal); 84 | mesh.uvs.push(Vec2::new(0.5, 0.5)); 85 | } 86 | 87 | // Add bottom vertices 88 | for i in 0..=cone.segments { 89 | 90 | let theta = i as f32 * angle_step; 91 | let x_unit = f32::cos(theta); 92 | let z_unit = f32::sin(theta); 93 | 94 | let slope = cone.radius / cone.height; 95 | let normal = Vec3::new(x_unit, slope, z_unit).normalize(); 96 | 97 | let uv = Vec2::new( 98 | (z_unit * 0.5) + 0.5, 99 | (x_unit * 0.5) + 0.5, 100 | ); 101 | 102 | mesh.positions.push(Vec3::new( 103 | x_unit * cone.radius, 104 | -cone.height / 2.0, 105 | z_unit * cone.radius, 106 | )); 107 | mesh.normals.push(normal); 108 | mesh.uvs.push(uv); 109 | } 110 | 111 | // Add indices 112 | for i in 0..cone.segments { 113 | 114 | let top = base_index + i; 115 | let left = base_index + cone.segments + i; 116 | let right = left + 1; 117 | 118 | mesh.indices.push(right); 119 | mesh.indices.push(left); 120 | mesh.indices.push(top); 121 | } 122 | } 123 | 124 | impl From for Mesh { 125 | fn from(cone: Cone) -> Self { 126 | 127 | // Validate input parameters 128 | assert!(cone.height > 0.0, "Must have positive height"); 129 | assert!(cone.radius > 0.0, "Must have positive radius"); 130 | assert!(cone.segments > 2, "Must have at least 3 subdivisions to close the surface"); 131 | 132 | // code adapted from http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html 133 | // (source code at https://github.com/SEilers/Apparat) 134 | 135 | // bottom + body 136 | let n_vertices = (cone.segments + 2) + (cone.segments * 2 + 1); 137 | let n_triangles = cone.segments * 2; 138 | let n_indices = n_triangles * 3; 139 | 140 | let mut mesh = MeshData::new(n_vertices as usize, n_indices as usize); 141 | 142 | add_bottom(&mut mesh, &cone); 143 | add_body(&mut mesh, &cone); 144 | 145 | let mut m = Mesh::new(PrimitiveTopology::TriangleList); 146 | m.set_indices(Some(Indices::U32(mesh.indices))); 147 | m.insert_attribute(Mesh::ATTRIBUTE_POSITION, mesh.positions); 148 | m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, mesh.normals); 149 | m.insert_attribute(Mesh::ATTRIBUTE_UV_0, mesh.uvs); 150 | m 151 | } 152 | } -------------------------------------------------------------------------------- /src/polygon.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{Display, Formatter}; 3 | use bevy::math::{Rect, Vec2, Vec3}; 4 | use bevy::prelude::Mesh; 5 | use bevy::render::mesh::{Indices, PrimitiveTopology}; 6 | use triangulate::{ListFormat, TriangulationError, Vertex}; 7 | use triangulate::formats::IndexedListFormat; 8 | 9 | pub struct Polygon { 10 | /// Points on a path where the last and first point are connected to form a closed circle. 11 | /// Must not intersect. Must contain enough points. 12 | pub points: Vec, 13 | } 14 | 15 | impl Polygon { 16 | pub fn new_regular_ngon(radius: f32, n: usize) -> Polygon { 17 | let angle_step = 2.0 * std::f32::consts::PI / n as f32; 18 | let mut points = Vec::with_capacity(n); 19 | 20 | for i in 0..n { 21 | let theta = angle_step * i as f32; 22 | points.push(Vec2::new( 23 | radius * f32::cos(theta), 24 | radius * f32::sin(theta), 25 | )); 26 | } 27 | 28 | Polygon { points } 29 | } 30 | 31 | /// Creates a triangle where the points touch a circle of specified radius. 32 | pub fn new_triangle(radius: f32) -> Polygon { 33 | Self::new_regular_ngon(radius, 3) 34 | } 35 | 36 | /// Creates a pentagon where the points touch a circle of specified radius. 37 | pub fn new_pentagon(radius: f32) -> Polygon { 38 | Self::new_regular_ngon(radius, 5) 39 | } 40 | 41 | /// Creates a hexagon where the points touch a circle of specified radius. 42 | pub fn new_hexagon(radius: f32) -> Polygon { 43 | Self::new_regular_ngon(radius, 6) 44 | } 45 | 46 | /// Creates a octagon where the points touch a circle of specified radius. 47 | pub fn new_octagon(radius: f32) -> Polygon { 48 | Self::new_regular_ngon(radius, 8) 49 | } 50 | } 51 | 52 | fn bounding_rect_for_points<'a>(points: impl Iterator) -> Rect { 53 | let mut x_min = 0.0f32; 54 | let mut x_max = 0.0f32; 55 | let mut y_min = 0.0f32; 56 | let mut y_max = 0.0f32; 57 | 58 | for point in points { 59 | x_min = x_min.min(point.x); 60 | x_max = x_max.max(point.x); 61 | y_min = y_min.min(point.y); 62 | y_max = y_max.max(point.y); 63 | } 64 | 65 | Rect { 66 | min: Vec2::new(x_min, y_min), 67 | max: Vec2::new(x_max, y_max), 68 | } 69 | } 70 | 71 | // This is an ugly workaround for rust's orphan rule. Neither Vec2 nor the Vertex trait come from this crate. 72 | // So we need to implement a newtype and hope it gets optimized away (which it should). 73 | #[derive(Debug, Copy, Clone)] 74 | struct Vec2f(Vec2); 75 | 76 | impl Vertex for Vec2f { 77 | type Coordinate = f32; 78 | 79 | fn x(&self) -> Self::Coordinate { 80 | self.0.x 81 | } 82 | 83 | fn y(&self) -> Self::Coordinate { 84 | self.0.y 85 | } 86 | } 87 | 88 | /// The input must not be empty. 89 | /// No edge can cross any other edge, whether it is on the same polygon or not. 90 | /// Each vertex must be part of exactly two edges. Polygons cannot 'share' vertices with each other. 91 | /// Each vertex must be distinct - no vertex can have x and y coordinates that both compare equal to another vertex's. 92 | #[derive(Debug)] 93 | pub struct InvalidInput; 94 | 95 | impl Display for InvalidInput { 96 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 97 | write!(f, "Invalid polygon input") 98 | } 99 | } 100 | 101 | impl Error for InvalidInput { } 102 | 103 | impl From> for InvalidInput { 104 | fn from(value: TriangulationError) -> Self { 105 | match value { 106 | TriangulationError::TrapezoidationError(_) => panic!("Failed to triangulate: {}", value), 107 | TriangulationError::NoVertices => Self, 108 | TriangulationError::InternalError(_) => Self, 109 | TriangulationError::FanBuilder(_) => panic!("Failed to triangulate: {}", value), 110 | _ => panic!("Failed to triangulate: {}", value), 111 | } 112 | } 113 | } 114 | 115 | impl TryFrom for Mesh { 116 | 117 | type Error = InvalidInput; 118 | 119 | fn try_from(polygon: Polygon) -> Result { 120 | 121 | if polygon.points.len() < 3 { 122 | return Err(InvalidInput); 123 | } 124 | 125 | let mut positions: Vec<[f32; 3]> = Vec::with_capacity(polygon.points.len()); 126 | let mut normals: Vec<[f32; 3]> = Vec::with_capacity(polygon.points.len()); 127 | let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(polygon.points.len()); 128 | 129 | // The domain is needed for UV mapping. The domain tells us how to transform all points to optimally fit the 0-1 range. 130 | let domain = bounding_rect_for_points(polygon.points.iter()); 131 | 132 | // Add the vertices 133 | for v in &polygon.points { 134 | positions.push([v.x, 0.0, v.y]); 135 | normals.push(Vec3::Y.to_array()); 136 | 137 | // Transform the polygon domain to the 0-1 UV domain. 138 | let u = (v.x - domain.min.x) / (domain.max.x - domain.min.x); 139 | let v = (v.y - domain.min.y) / (domain.max.y - domain.min.y); 140 | uvs.push([u, v]); 141 | } 142 | 143 | // Triangulate to obtain the indices 144 | // This library is terrible to use. The heck is that initializer object. And this trait madness. 145 | let polygons = polygon 146 | .points 147 | .into_iter() 148 | .map(|v| Vec2f(v)) 149 | .collect::>(); 150 | let mut output = Vec::<[usize; 3]>::new(); 151 | let format = IndexedListFormat::new(&mut output).into_fan_format(); 152 | triangulate::Polygon::triangulate(&polygons, format)?; 153 | let indices = output.into_iter() 154 | .map(|[a, b, c]| [c, b, a]) 155 | .flatten() 156 | .map(|v| v as u32) 157 | .collect(); 158 | 159 | // Put the mesh together 160 | let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); 161 | mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); 162 | mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); 163 | mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); 164 | mesh.set_indices(Some(Indices::U32(indices))); 165 | Ok(mesh) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/torus.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::Vec3; 2 | use bevy::prelude::{Mesh, Vec2}; 3 | use bevy::render::mesh::{Indices, PrimitiveTopology}; 4 | use crate::MeshData; 5 | use crate::util::FlatTrapezeIndices; 6 | 7 | pub struct Torus { 8 | /// The radius of the ring. Measured from the mesh's origin to the center line of the tube. 9 | pub radius: f32, 10 | /// The width of the ring 11 | pub tube_radius: f32, 12 | /// The number of segments that make up the ring. 13 | pub radial_segments: usize, 14 | /// The number of segments that make up the tube. 15 | pub tube_segments: usize, 16 | /// Circumference in radians around the main axis. 2pi for a full torus. 17 | pub radial_circumference: f32, 18 | /// Circumference in radians of the individual ring segments. 2pi for a closed tube. 19 | pub tube_circumference: f32, 20 | /// The offset in radians of where on the circle the torus begins. Ignored if radial_circumference is 2pi. 21 | pub radial_offset: f32, 22 | /// The offset in radians of where the tube begins on its circle. Ignored if tube_circumference is 2pi. 23 | pub tube_offset: f32, 24 | } 25 | 26 | impl Default for Torus { 27 | fn default() -> Self { 28 | Self { 29 | radius: 0.8, 30 | tube_radius: 0.2, 31 | radial_segments: 64, 32 | tube_segments: 32, 33 | radial_circumference: std::f32::consts::TAU, 34 | tube_circumference: std::f32::consts::TAU, 35 | radial_offset: 0.0, 36 | tube_offset: 0.0, 37 | } 38 | } 39 | } 40 | 41 | impl From for Mesh { 42 | fn from(torus: Torus) -> Mesh { 43 | 44 | // Input parameter validation 45 | assert!(torus.radius > 0.0, "The radii of a torus must be positive"); 46 | assert!(torus.tube_radius > 0.0, "The radii of a torus must be positive"); 47 | assert!(torus.radial_segments >= 3, "Must have at least 3 radial segments"); 48 | assert!(torus.tube_segments >= 3, "3 Must have at least 3 tube segments"); 49 | assert!(torus.radial_circumference > 0.0, "Radial circumference must be positive"); 50 | assert!(torus.tube_circumference > 0.0, "Tube circumference must be positive"); 51 | assert!(torus.radial_circumference <= std::f32::consts::TAU, "Radial circumference must not exceed 2pi radians"); 52 | assert!(torus.tube_circumference <= std::f32::consts::TAU, "Tube circumference must not exceed 2pi radians"); 53 | if torus.radial_circumference < std::f32::consts::TAU { 54 | assert!(torus.radial_offset >= 0.0, "Radial offset must be between 0 and 2pi"); 55 | assert!(torus.radial_offset <= std::f32::consts::TAU, "Radial offset must be between 0 and 2pi"); 56 | } 57 | if torus.tube_radius < std::f32::consts::TAU { 58 | assert!(torus.tube_radius >= 0.0, "Tube offset must be between 0 and 2pi"); 59 | assert!(torus.tube_radius <= std::f32::consts::TAU, "Tube offset must be between 0 and 2pi"); 60 | } 61 | 62 | let num_vertices = (torus.radial_segments + 1) * (torus.tube_segments + 1); 63 | let mut mesh = MeshData { 64 | positions: Vec::with_capacity(num_vertices), 65 | normals: Vec::with_capacity(num_vertices), 66 | uvs: Vec::with_capacity(num_vertices), 67 | indices: Vec::with_capacity(torus.radial_segments * torus.tube_segments * 6), 68 | }; 69 | 70 | generate_torus_body(&mut mesh, &torus); 71 | 72 | let mut m = Mesh::new(PrimitiveTopology::TriangleList); 73 | m.insert_attribute(Mesh::ATTRIBUTE_POSITION, mesh.positions); 74 | m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, mesh.normals); 75 | m.insert_attribute(Mesh::ATTRIBUTE_UV_0, mesh.uvs); 76 | m.set_indices(Some(Indices::U32(mesh.indices))); 77 | m 78 | } 79 | } 80 | 81 | fn generate_torus_body(mesh: &mut MeshData, torus: &Torus) { 82 | 83 | // This code is based on http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html 84 | 85 | let angle_step_vertical = torus.tube_circumference / torus.tube_segments as f32; 86 | let angle_step_horizontal = torus.radial_circumference / torus.radial_segments as f32; 87 | 88 | // Add vertices ring by ring 89 | for horizontal_idx in 0..=torus.radial_segments { 90 | 91 | let theta_horizontal = angle_step_horizontal * horizontal_idx as f32 + torus.radial_offset; 92 | 93 | // The center of the vertical ring 94 | let ring_center = Vec3::new( 95 | torus.radius * f32::cos(theta_horizontal), 96 | 0.0, 97 | torus.radius * f32::sin(theta_horizontal) 98 | ); 99 | 100 | for vertical_idx in 0..=torus.tube_segments { 101 | 102 | let theta_vertical = angle_step_vertical * vertical_idx as f32 + torus.tube_offset; 103 | 104 | let position = Vec3::new( 105 | f32::cos(theta_horizontal) * (torus.radius + torus.tube_radius * f32::cos(theta_vertical)), 106 | f32::sin(theta_vertical) * torus.tube_radius, 107 | f32::sin(theta_horizontal) * (torus.radius + torus.tube_radius * f32::cos(theta_vertical)), 108 | ); 109 | 110 | // The normal points from the radius 0 torus to the actual point 111 | let normal = (position - ring_center).normalize(); 112 | mesh.positions.push(position); 113 | mesh.normals.push(normal); 114 | 115 | // Since the segments are basically a deformed grid, we can overlay that onto the UV space 116 | let u = 1.0 / torus.radial_segments as f32 * horizontal_idx as f32; 117 | let v = 1.0 / torus.tube_segments as f32 * vertical_idx as f32; 118 | mesh.uvs.push(Vec2::new(u, v)); 119 | } 120 | } 121 | 122 | // Add indices for each face 123 | for horizontal_idx in 0..torus.radial_segments { 124 | 125 | let ring0_base_idx = horizontal_idx * (torus.tube_segments + 1); 126 | let ring1_base_idx = (horizontal_idx + 1) * (torus.tube_segments + 1); 127 | 128 | for vertical_idx in 0..torus.tube_segments { 129 | let face = FlatTrapezeIndices { 130 | lower_left: (ring0_base_idx + vertical_idx) as u32, 131 | upper_left: (ring0_base_idx + vertical_idx + 1) as u32, 132 | lower_right: (ring1_base_idx + vertical_idx) as u32, 133 | upper_right: (ring1_base_idx + vertical_idx + 1) as u32, 134 | }; 135 | face.generate_triangles(&mut mesh.indices); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /src/cylinder.rs: -------------------------------------------------------------------------------- 1 | // This is based on a blog post found here: http://apparat-engine.blogspot.com/2013/04/procdural-meshes-cylinder.html. 2 | 3 | use bevy::math::Vec3; 4 | use bevy::prelude::Vec2; 5 | use bevy::render::mesh::{Indices, Mesh}; 6 | use bevy::render::render_resource::PrimitiveTopology; 7 | use crate::MeshData; 8 | use crate::util::FlatTrapezeIndices; 9 | 10 | pub struct Cylinder { 11 | pub height: f32, 12 | pub radius_bottom: f32, 13 | pub radius_top: f32, 14 | pub radial_segments: u32, 15 | pub height_segments: u32, 16 | } 17 | 18 | impl Default for Cylinder { 19 | fn default() -> Self { 20 | Self { 21 | height: 1.0, 22 | radius_bottom: 0.5, 23 | radius_top: 0.5, 24 | radial_segments: 32, 25 | height_segments: 1, 26 | } 27 | } 28 | } 29 | 30 | impl Cylinder { 31 | /// Create a cylinder where the top and bottom disc have the same radius. 32 | pub fn new_regular(height: f32, radius: f32, subdivisions: u32) -> Self { 33 | Self { 34 | height, 35 | radius_bottom: radius, 36 | radius_top: radius, 37 | radial_segments: subdivisions, 38 | height_segments: 1, 39 | } 40 | } 41 | } 42 | 43 | fn add_top(mesh: &mut MeshData, cylinder: &Cylinder) { 44 | 45 | let angle_step = std::f32::consts::TAU / cylinder.radial_segments as f32; 46 | let base_index = mesh.positions.len() as u32; 47 | 48 | // Center 49 | let center_pos = Vec3::new(0.0, cylinder.height / 2.0, 0.0); 50 | mesh.positions.push(center_pos); 51 | mesh.uvs.push(Vec2::new(0.5, 0.5)); 52 | mesh.normals.push(Vec3::Y); 53 | 54 | // Vertices 55 | for i in 0..=cylinder.radial_segments { 56 | 57 | let theta = i as f32 * angle_step; 58 | let x_unit = f32::cos(theta); 59 | let z_unit = f32::sin(theta); 60 | 61 | let pos = Vec3::new( 62 | cylinder.radius_top * x_unit, 63 | cylinder.height / 2.0, 64 | cylinder.radius_top * z_unit, 65 | ); 66 | let uv = Vec2::new( 67 | (z_unit * 0.5) + 0.5, 68 | (x_unit * 0.5) + 0.5, 69 | ); 70 | 71 | mesh.positions.push(pos); 72 | mesh.uvs.push(uv); 73 | mesh.normals.push(Vec3::Y) 74 | } 75 | 76 | // Indices 77 | for i in 0..cylinder.radial_segments { 78 | mesh.indices.push(base_index); 79 | mesh.indices.push(base_index + i + 2); 80 | mesh.indices.push(base_index + i + 1); 81 | } 82 | } 83 | 84 | fn add_bottom(mesh: &mut MeshData, cylinder: &Cylinder) { 85 | 86 | let angle_step = std::f32::consts::TAU / cylinder.radial_segments as f32; 87 | let base_index = mesh.positions.len() as u32; 88 | 89 | // Center 90 | let center_pos = Vec3::new(0.0, -cylinder.height / 2.0, 0.0); 91 | mesh.positions.push(center_pos); 92 | mesh.uvs.push(Vec2::new(0.5, 0.5)); 93 | mesh.normals.push(-Vec3::Y); 94 | 95 | // Vertices 96 | for i in 0..=cylinder.radial_segments { 97 | 98 | let theta = i as f32 * angle_step; 99 | let x_unit = f32::cos(theta); 100 | let z_unit = f32::sin(theta); 101 | 102 | let pos = Vec3::new( 103 | cylinder.radius_bottom * x_unit, 104 | -cylinder.height / 2.0, 105 | cylinder.radius_bottom * z_unit, 106 | ); 107 | let uv = Vec2::new( 108 | (z_unit * 0.5) + 0.5, 109 | (x_unit * -0.5) + 0.5, 110 | ); 111 | 112 | mesh.positions.push(pos); 113 | mesh.uvs.push(uv); 114 | mesh.normals.push(-Vec3::Y) 115 | } 116 | 117 | // Indices 118 | for i in 0..cylinder.radial_segments { 119 | mesh.indices.push(base_index + i + 1); 120 | mesh.indices.push(base_index + i + 2); 121 | mesh.indices.push(base_index); 122 | } 123 | } 124 | 125 | fn add_body(mesh: &mut MeshData, cylinder: &Cylinder) { 126 | 127 | let angle_step = std::f32::consts::TAU / cylinder.radial_segments as f32; 128 | let base_index = mesh.positions.len() as u32; 129 | 130 | // Vertices 131 | for i in 0..=cylinder.radial_segments { 132 | 133 | let theta = angle_step * i as f32; 134 | let x_unit = f32::cos(theta); 135 | let z_unit = f32::sin(theta); 136 | 137 | // Calculate normal of this segment, it's a straight line so all normals are the same 138 | let slope = (cylinder.radius_bottom - cylinder.radius_top) / cylinder.height; 139 | let normal = Vec3::new(x_unit, slope, z_unit).normalize(); 140 | 141 | for h in 0..=cylinder.height_segments { 142 | let height_percent = h as f32 / cylinder.height_segments as f32; 143 | let y = height_percent * cylinder.height - cylinder.height / 2.0; 144 | let radius = (1.0 - height_percent) * cylinder.radius_bottom + height_percent * cylinder.radius_top; 145 | 146 | let pos = Vec3::new(x_unit * radius, y, z_unit * radius); 147 | let uv = Vec2::new(i as f32 / cylinder.radial_segments as f32, height_percent); 148 | 149 | mesh.positions.push(pos); 150 | mesh.normals.push(normal); 151 | mesh.uvs.push(uv); 152 | } 153 | } 154 | 155 | // Indices 156 | for i in 0..cylinder.radial_segments { 157 | for h in 0..cylinder.height_segments { 158 | let segment_base = base_index + (i * (cylinder.height_segments + 1)) + h; 159 | let indices = FlatTrapezeIndices { 160 | lower_left: segment_base, 161 | upper_left: segment_base + 1, 162 | lower_right: segment_base + cylinder.height_segments + 1, 163 | upper_right: segment_base + cylinder.height_segments + 2, 164 | }; 165 | indices.generate_triangles(&mut mesh.indices); 166 | } 167 | } 168 | } 169 | 170 | impl From for Mesh { 171 | fn from(cylinder: Cylinder) -> Self { 172 | 173 | // Input parameter validation 174 | assert_ne!(cylinder.radius_top, 0.0, "Radius must not be 0. Use a cone instead."); 175 | assert_ne!(cylinder.radius_bottom, 0.0, "Radius must not be 0. Use a cone instead."); 176 | assert!(cylinder.radius_bottom > 0.0, "Must have positive radius."); 177 | assert!(cylinder.radius_top > 0.0, "Must have positive radius."); 178 | assert!(cylinder.radial_segments > 2, "Must have at least 3 subdivisions to close the surface."); 179 | assert!(cylinder.height_segments >= 1, "Must have at least one height segment."); 180 | assert!(cylinder.height > 0.0, "Must have positive height"); 181 | 182 | let num_vertices = (cylinder.radial_segments + 1) * (cylinder.height_segments + 3) + 2; 183 | // top&bottom + body 184 | let num_indices = cylinder.radial_segments * 3 * 2 + cylinder.radial_segments * cylinder.height_segments * 6; 185 | 186 | let mut mesh = MeshData::new(num_vertices as usize, num_indices as usize); 187 | 188 | add_top(&mut mesh, &cylinder); 189 | add_bottom(&mut mesh, &cylinder); 190 | add_body(&mut mesh, &cylinder); 191 | 192 | let mut m = Mesh::new(PrimitiveTopology::TriangleList); 193 | m.insert_attribute(Mesh::ATTRIBUTE_POSITION, mesh.positions); 194 | m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, mesh.normals); 195 | m.insert_attribute(Mesh::ATTRIBUTE_UV_0, mesh.uvs); 196 | m.set_indices(Some(Indices::U32(mesh.indices))); 197 | m 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/tube.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, Sub}; 2 | use bevy::prelude::{Mesh, Quat, Vec2, Vec3}; 3 | use bevy::render::mesh::{Indices, PrimitiveTopology}; 4 | use crate::MeshData; 5 | use crate::util::{Extent, FlatTrapezeIndices}; 6 | 7 | pub trait Curve { 8 | 9 | /// Evaluate the curve at some point along it. 10 | fn eval_at(&self, t: f32) -> Vec3; 11 | 12 | /// Calculate a unit tangent at a specific point on the curve. 13 | /// By default it will take two close points and use their difference to construct the tangent. 14 | fn tangent_at(&self, t: f32) -> Vec3 { 15 | const DELTA: f32 = 0.0001; 16 | 17 | let t0 = t - DELTA; 18 | let t1 = t + DELTA; 19 | let v0 = self.eval_at(t0); 20 | let v1 = self.eval_at(t1); 21 | 22 | (v1 - v0).normalize() 23 | } 24 | } 25 | 26 | /// Default curve implementation. It's a straight line up (y+). 27 | /// This is mainly used as a fallback and is thus not public. 28 | /// Users are expected to bring their own curve implementations. 29 | struct DefaultCurve; 30 | 31 | impl Curve for DefaultCurve { 32 | fn eval_at(&self, t: f32) -> Vec3 { 33 | assert!(t >= 0.0); 34 | assert!(t <= 1.0); 35 | Vec3::new(0.0, t, 0.0) 36 | } 37 | 38 | fn tangent_at(&self, _: f32) -> Vec3 { 39 | Vec3::new(0.0, 1.0, 0.0) 40 | } 41 | } 42 | 43 | pub struct Tube { 44 | pub radius: f32, 45 | pub curve: Box, 46 | pub length_segments: u32, 47 | pub radial_segments: u32, 48 | pub radial_circumference: f32, 49 | pub radial_offset: f32, 50 | } 51 | 52 | impl Default for Tube { 53 | fn default() -> Self { 54 | Tube { 55 | radius: 0.05, 56 | curve: Box::new(DefaultCurve), // straight line 57 | length_segments: 64, 58 | radial_segments: 64, 59 | radial_circumference: std::f32::consts::TAU, 60 | radial_offset: 0.0, 61 | } 62 | } 63 | } 64 | 65 | struct FrenetSerretFrame { 66 | origin: Vec3, 67 | tangent: Vec3, 68 | normal: Vec3, 69 | binormal: Vec3, 70 | } 71 | 72 | fn initial_normal(tangent: Vec3) -> Vec3 { 73 | 74 | // Select initial normal in the direction of the minimum component of the tangent 75 | let mut min = f32::MAX; 76 | let tx = tangent.x.abs(); 77 | let ty = tangent.y.abs(); 78 | let tz = tangent.z.abs(); 79 | 80 | let mut normal = Vec3::new(0.0, 0.0, 0.0); 81 | 82 | if tx <= min { 83 | min = tx; 84 | normal = Vec3::new(1.0, 0.0, 0.0); 85 | } 86 | if ty <= min { 87 | min = ty; 88 | normal = Vec3::new(0.0, 1.0, 0.0); 89 | } 90 | if tz <= min { 91 | normal = Vec3::new(0.0, 0.0, 1.0); 92 | } 93 | 94 | normal 95 | } 96 | 97 | fn initial_frame(curve: &dyn Curve) -> FrenetSerretFrame { 98 | 99 | let origin = curve.eval_at(0.0); 100 | let tangent = curve.tangent_at(0.0); 101 | let normal = initial_normal(tangent); 102 | let v = tangent.cross(tangent.cross(normal).normalize()); 103 | 104 | FrenetSerretFrame { 105 | origin, 106 | tangent, 107 | normal: v, 108 | binormal: tangent.cross(v), 109 | } 110 | } 111 | 112 | fn calculate_frames(curve: &dyn Curve, num_frames: u32) -> Vec { 113 | 114 | let mut out = Vec::with_capacity(num_frames as usize); 115 | let step = 1.0 / (num_frames - 1) as f32; 116 | 117 | // First frame is different 118 | out.push(initial_frame(curve)); 119 | 120 | // Calculate a smoothly shifting coordinate frame for each segment point 121 | for i in 1..num_frames { 122 | 123 | let t = step * i as f32; 124 | let prev_frame: &FrenetSerretFrame = out.get(i as usize - 1).unwrap(); // unwrap: i starts at 1 125 | 126 | let mut cur_frame = FrenetSerretFrame { 127 | origin: curve.eval_at(t), 128 | tangent: curve.tangent_at(t), 129 | normal: prev_frame.normal, 130 | binormal: prev_frame.binormal, 131 | }; 132 | 133 | let mut v = prev_frame.tangent.cross(cur_frame.tangent); 134 | if v.length() > f32::EPSILON { 135 | v = v.normalize(); 136 | let angle = prev_frame.tangent.dot(cur_frame.tangent); 137 | let angle = angle.clamp(-1.0, 1.0); 138 | let theta = f32::acos(angle); 139 | let rot = Quat::from_axis_angle(v, theta); 140 | cur_frame.normal = rot.mul_vec3(cur_frame.normal); 141 | } 142 | 143 | cur_frame.binormal = cur_frame.tangent.cross(cur_frame.normal); 144 | 145 | out.push(cur_frame); 146 | } 147 | 148 | // If the curve is closed, make the frames line up 149 | let start_end_distance = curve.eval_at(0.0).sub(curve.eval_at(1.0)).length(); 150 | if start_end_distance <= 2.0 * f32::EPSILON { 151 | 152 | let first_frame = out.get(0).unwrap(); // unwrap: We have >= 1 segment 153 | let last_frame = out.last().unwrap(); // unwrap: We have >= 1 segment 154 | 155 | // Post-process the frames 156 | let discrepancy_theta = { 157 | let t = first_frame.normal.dot(last_frame.normal) 158 | .clamp(-1.0, 1.0) 159 | .acos() / (num_frames - 1) as f32; 160 | if first_frame.tangent.dot(first_frame.normal.cross(last_frame.normal)) > 0.0 { 161 | -t 162 | } 163 | else { 164 | t 165 | } 166 | }; 167 | 168 | // Rotate each frame a little to make them line up 169 | for (idx, frame) in out.iter_mut().skip(1).enumerate() { 170 | let rot = Quat::from_axis_angle(frame.tangent, discrepancy_theta * idx as f32); 171 | frame.normal = rot.mul_vec3(frame.normal); 172 | frame.binormal = frame.tangent.cross(frame.normal); 173 | } 174 | } 175 | 176 | out 177 | } 178 | 179 | fn normalize_frames(frames: &mut [FrenetSerretFrame]) { 180 | let mut extent = Extent::new(); 181 | for frame in frames.iter() { 182 | extent.extend_to_include(frame.origin); 183 | } 184 | let center = extent.center(); 185 | let lengths = extent.lengths().to_array(); 186 | let scale = 1.0 / lengths.iter() 187 | .fold(f32::MIN, |a, b| f32::max(a, f32::abs(*b))); 188 | for frame in frames.iter_mut() { 189 | frame.origin -= center; 190 | frame.origin *= scale; 191 | } 192 | } 193 | 194 | fn add_tube_segment(mesh: &mut MeshData, frame: &FrenetSerretFrame, tube: &Tube, index: usize) { 195 | 196 | let angle_step = tube.radial_circumference / tube.radial_segments as f32; 197 | 198 | for i in 0..=tube.radial_segments { 199 | let theta = angle_step * i as f32 + tube.radial_offset; 200 | let sin = theta.sin(); 201 | let cos = -theta.cos(); 202 | 203 | let normal = Vec3::normalize(cos * frame.normal + sin * frame.binormal); 204 | let position = frame.origin + tube.radius * normal; 205 | let uv = Vec2::new( 206 | index as f32 / tube.length_segments as f32, 207 | i as f32 / tube.radial_segments as f32 208 | ); 209 | 210 | mesh.normals.push(normal); 211 | mesh.positions.push(position); 212 | mesh.uvs.push(uv); 213 | } 214 | } 215 | 216 | fn add_ribbon_segment(mesh: &mut MeshData, frame: &FrenetSerretFrame, tube: &Tube, index: usize) { 217 | 218 | let theta = tube.radial_offset + std::f32::consts::FRAC_PI_2; 219 | let sin = theta.sin(); 220 | let cos = -theta.cos(); 221 | let base = Vec3::normalize(cos * frame.normal + sin * frame.binormal); 222 | 223 | // Front 224 | let front_normal = frame.tangent.cross(base); 225 | mesh.normals.push(front_normal); 226 | mesh.normals.push(front_normal); 227 | mesh.positions.push(frame.origin + tube.radius * base); 228 | mesh.positions.push(frame.origin + tube.radius * -base); 229 | mesh.uvs.push(Vec2::new( 230 | index as f32 / tube.length_segments as f32, 231 | 0.0 232 | )); 233 | mesh.uvs.push(Vec2::new( 234 | index as f32 / tube.length_segments as f32, 235 | 1.0 236 | )); 237 | 238 | // Back 239 | if tube.radial_segments == 2 { 240 | mesh.normals.push(-front_normal); 241 | mesh.normals.push(-front_normal); 242 | mesh.positions.push(frame.origin + tube.radius * -base); 243 | mesh.positions.push(frame.origin + tube.radius * base); 244 | mesh.uvs.push(Vec2::new( 245 | index as f32 / tube.length_segments as f32, 246 | 0.0 247 | )); 248 | mesh.uvs.push(Vec2::new( 249 | index as f32 / tube.length_segments as f32, 250 | 1.0 251 | )); 252 | } 253 | } 254 | 255 | // Calculate the bounding box of this mesh and then shrink the mesh to fit into the unit box 256 | fn normalize_positions(positions: &mut [Vec3]) { 257 | 258 | let mut extent = Extent::new(); 259 | for point in positions.iter() { 260 | extent.extend_to_include(*point); 261 | } 262 | let center = extent.center(); 263 | let lengths = extent.lengths().to_array(); 264 | let scale = 1.0 / lengths.iter() 265 | .fold(f32::MIN, |a, b| f32::max(a, f32::abs(*b))); 266 | for point in positions.iter_mut() { 267 | *point -= center; 268 | *point *= scale; 269 | } 270 | } 271 | 272 | fn index_tube(mesh: &mut MeshData, tube: &Tube) { 273 | for j in 1..=tube.length_segments { 274 | for i in 1..=tube.radial_segments { 275 | 276 | let a = ( tube.radial_segments + 1 ) * ( j - 1 ) + ( i - 1 ); 277 | let b = ( tube.radial_segments + 1 ) * j + ( i - 1 ); 278 | let c = ( tube.radial_segments + 1 ) * j + i; 279 | let d = ( tube.radial_segments + 1 ) * ( j - 1 ) + i; 280 | 281 | // faces 282 | mesh.indices.push(a); 283 | mesh.indices.push(b); 284 | mesh.indices.push(d); 285 | mesh.indices.push(b); 286 | mesh.indices.push(c); 287 | mesh.indices.push(d); 288 | } 289 | } 290 | } 291 | 292 | fn index_ribbon(mesh: &mut MeshData, tube: &Tube) { 293 | for ls in 0..tube.length_segments { 294 | for rs in 0..tube.radial_segments { 295 | let indices = FlatTrapezeIndices { 296 | lower_left: 2 * tube.radial_segments * ls + 2 * rs, 297 | upper_left: 2 * tube.radial_segments * (ls + 1) + 2 * rs, 298 | lower_right: 2 * tube.radial_segments * ls + 2 * rs + 1, 299 | upper_right: 2 * tube.radial_segments * (ls + 1) + 2 * rs + 1, 300 | }; 301 | indices.generate_triangles(&mut mesh.indices); 302 | } 303 | } 304 | } 305 | 306 | // The implementation of this algorithm is based on three.js. 307 | // https://github.com/mrdoob/three.js 308 | fn add_tube(mesh: &mut MeshData, tube: &Tube) { 309 | 310 | let mut frames = calculate_frames(tube.curve.deref(), tube.length_segments + 1); 311 | normalize_frames(frames.as_mut_slice()); 312 | for (idx, frame) in frames.iter().enumerate() { 313 | if tube.radial_segments < 3 { 314 | add_ribbon_segment(mesh, frame, tube, idx); 315 | } 316 | else { 317 | add_tube_segment(mesh, frame, tube, idx); 318 | } 319 | } 320 | 321 | // Generate indices for the faces 322 | if tube.radial_segments < 3 { 323 | index_ribbon(mesh, tube); 324 | } 325 | else { 326 | index_tube(mesh, tube); 327 | } 328 | } 329 | 330 | fn make_line(tube: &Tube) -> Mesh { 331 | let mut m = Mesh::new(PrimitiveTopology::LineStrip); 332 | let mut positions = Vec::with_capacity(tube.length_segments as usize + 1); 333 | let step = 1.0 / tube.length_segments as f32; 334 | for i in 0..=tube.length_segments { 335 | let t = step * i as f32; 336 | let p = tube.curve.eval_at(t); 337 | positions.push(p); 338 | } 339 | normalize_positions(positions.as_mut_slice()); 340 | m.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); 341 | m 342 | } 343 | 344 | impl From for Mesh { 345 | fn from(tube: Tube) -> Self { 346 | 347 | assert!(tube.length_segments > 0, "Must have at least one length segment"); 348 | assert!(tube.radial_offset >= 0.0 && tube.radial_offset <= std::f32::consts::TAU, "Radial offset must be in [0, 2pi]"); 349 | assert!(tube.radial_circumference > 0.0 && tube.radial_circumference <= std::f32::consts::TAU, "Radial circumference must be in (0, 2pi]"); 350 | 351 | // Special case: Tube should be a line 352 | if tube.radius.abs() < f32::EPSILON || tube.radial_segments == 0 { 353 | return make_line(&tube); 354 | } 355 | 356 | let num_vertices = (tube.length_segments + 1) as usize * (tube.radial_segments + 1) as usize; 357 | let num_indices = tube.length_segments as usize * tube.radial_segments as usize * 6; 358 | let mut mesh = MeshData::new(num_vertices, num_indices); 359 | 360 | add_tube(&mut mesh, &tube); 361 | 362 | let mut m = Mesh::new(PrimitiveTopology::TriangleList); 363 | m.insert_attribute(Mesh::ATTRIBUTE_POSITION, mesh.positions); 364 | m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, mesh.normals); 365 | m.insert_attribute(Mesh::ATTRIBUTE_UV_0, mesh.uvs); 366 | m.set_indices(Some(Indices::U32(mesh.indices))); 367 | m 368 | } 369 | } -------------------------------------------------------------------------------- /examples/gallery.rs: -------------------------------------------------------------------------------- 1 | use bevy::app::App; 2 | use bevy::asset::{AssetServer, Assets}; 3 | use bevy::input::Input; 4 | use bevy::math::Vec3; 5 | use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; 6 | use bevy::pbr::{AmbientLight, DirectionalLight, NotShadowCaster, PbrBundle, StandardMaterial}; 7 | use bevy::prelude::*; 8 | use bevy::render::settings::{WgpuFeatures, WgpuSettings}; 9 | use bevy::text::{Text, TextAlignment, TextStyle}; 10 | use bevy::ui::{AlignSelf, PositionType, Style, Val}; 11 | use bevy::window::{CursorGrabMode, PrimaryWindow}; 12 | use bevy::DefaultPlugins; 13 | use bevy::render::RenderPlugin; 14 | use bevy_normal_material::prelude::{NormalMaterial, NormalMaterialPlugin}; 15 | use bevy_more_shapes::torus::Torus; 16 | use bevy_more_shapes::{Cone, Cylinder, Grid, Polygon}; 17 | use smooth_bevy_cameras::controllers::fps::{FpsCameraBundle, FpsCameraController, FpsCameraPlugin}; 18 | use bevy_more_shapes::tube::{Curve, Tube}; 19 | 20 | struct WaveFunction; 21 | 22 | impl Curve for WaveFunction { 23 | fn eval_at(&self, t: f32) -> Vec3 { 24 | Vec3::new( 25 | -f32::sin(t * std::f32::consts::PI * 2.0) * 0.2, 26 | t, 27 | f32::sin(t * std::f32::consts::PI * 2.0) * 0.2 28 | ) 29 | } 30 | } 31 | 32 | struct Knot { 33 | rotation_winds: u32, 34 | circle_winds: u32, 35 | } 36 | 37 | impl Curve for Knot { 38 | fn eval_at(&self, mut t: f32) -> Vec3 { 39 | 40 | t *= std::f32::consts::TAU * 2.0; 41 | let cu = f32::cos(t); 42 | let su = f32::sin(t); 43 | let quop = self.circle_winds as f32 / self.rotation_winds as f32 * t; 44 | let cs = f32::cos(quop); 45 | 46 | Vec3::new( 47 | (2.0 + cs) * 0.5 * cu, 48 | (2.0 + cs) * su * 0.5, 49 | f32::sin(quop) * 0.5, 50 | ) 51 | } 52 | } 53 | 54 | // Spawns the actual gallery of shapes. Spawns a row for each type in z+ direction. 55 | fn spawn_shapes( 56 | mut commands: Commands, 57 | mut meshes: ResMut>, 58 | mut materials: ResMut>, 59 | mut normal_materials: ResMut>, 60 | mut wireframe_config: ResMut, 61 | mut ambient_light: ResMut, 62 | asset_server: Res, 63 | ) { 64 | let checkerboard_texture = asset_server.load("textures/checkerboard_1024x1024.png"); 65 | 66 | // Start out without wireframes, but you can toggle them. 67 | wireframe_config.global = false; 68 | 69 | /* 70 | // Comparison: Builtin sphere 71 | let mut sphere = Icosphere::default(); 72 | sphere.radius = 0.5; 73 | commands.spawn(PbrBundle { 74 | mesh: meshes.add(Mesh::try_from(sphere).unwrap()), 75 | material: materials.add(StandardMaterial::from(Color::BISQUE)), 76 | transform: Transform::from_xyz(-2.0, 0.0, 5.0), 77 | ..Default::default() 78 | }); 79 | commands.spawn(PbrBundle { 80 | mesh: meshes.add(Mesh::try_from(sphere).unwrap()), 81 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 82 | transform: Transform::from_xyz(-2.0, 0.0, 7.0), 83 | ..Default::default() 84 | }); 85 | 86 | */ 87 | 88 | // Default cone 89 | commands.spawn(MaterialMeshBundle { 90 | mesh: meshes.add(Mesh::from(Cone::default())), 91 | material: normal_materials.add(NormalMaterial::default()), 92 | transform: Transform::from_xyz(0.0, 0.0, 5.0), 93 | ..Default::default() 94 | }); 95 | 96 | // Big cone 97 | commands.spawn(PbrBundle { 98 | mesh: meshes.add(Mesh::from(Cone { 99 | radius: 0.8, 100 | height: 2.0, 101 | segments: 32, 102 | })), 103 | material: materials.add(StandardMaterial::from(Color::YELLOW_GREEN)), 104 | transform: Transform::from_xyz(0.0, 0.0, 7.0), 105 | ..Default::default() 106 | }); 107 | 108 | // Small cone 109 | commands.spawn(PbrBundle { 110 | mesh: meshes.add(Mesh::from(Cone { 111 | radius: 0.8, 112 | height: 0.3, 113 | segments: 32, 114 | })), 115 | material: materials.add(StandardMaterial::from(Color::DARK_GRAY)), 116 | transform: Transform::from_xyz(0.0, 0.0, 9.0), 117 | ..Default::default() 118 | }); 119 | 120 | // Textured cone 121 | commands.spawn(PbrBundle { 122 | mesh: meshes.add(Mesh::from(Cone::default())), 123 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 124 | transform: Transform::from_xyz(0.0, 0.0, 11.0), 125 | ..Default::default() 126 | }); 127 | 128 | // Textured cylinder 129 | commands.spawn(PbrBundle { 130 | mesh: meshes.add(Mesh::from(Cylinder::default())), 131 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 132 | transform: Transform::from_xyz(2.0, 0.0, 13.0), 133 | ..Default::default() 134 | }); 135 | 136 | // Tiny cylinder 137 | commands.spawn(PbrBundle { 138 | mesh: meshes.add(Mesh::from(Cylinder { 139 | height: 1.0, 140 | radius_bottom: 0.5, 141 | radius_top: 0.5, 142 | radial_segments: 3, 143 | height_segments: 1, 144 | })), 145 | material: materials.add(StandardMaterial::from(Color::OLIVE)), 146 | transform: Transform::from_xyz(2.0, 0.0, 11.0), 147 | ..Default::default() 148 | }); 149 | 150 | // Default cylinder 151 | { 152 | let mut mat = StandardMaterial::from(Color::CRIMSON); 153 | mat.cull_mode = None; 154 | commands.spawn(PbrBundle { 155 | mesh: meshes.add(Mesh::from(Cylinder::default())), 156 | material: materials.add(mat), 157 | transform: Transform::from_xyz(2.0, 0.0, 5.0), 158 | ..Default::default() 159 | }); 160 | } 161 | 162 | // Taller regular cylinder 163 | commands.spawn(PbrBundle { 164 | mesh: meshes.add(Mesh::from(Cylinder::new_regular(2.2, 0.5, 16))), 165 | material: materials.add(StandardMaterial::from(Color::FUCHSIA)), 166 | transform: Transform::from_xyz(2.0, 0.0, 7.0), 167 | ..Default::default() 168 | }); 169 | 170 | // Irregular cylinder 171 | commands.spawn(MaterialMeshBundle { 172 | mesh: meshes.add(Mesh::from(Cylinder { 173 | height: 1.0, 174 | radius_bottom: 0.6, 175 | radius_top: 0.2, 176 | radial_segments: 64, 177 | height_segments: 1, 178 | })), 179 | material: normal_materials.add(NormalMaterial::default()), 180 | transform: Transform::from_xyz(2.0, 0.0, 9.0), 181 | ..Default::default() 182 | }); 183 | 184 | // Height segmented cylinder 185 | commands.spawn(PbrBundle { 186 | mesh: meshes.add(Mesh::from(Cylinder { 187 | height: 1.0, 188 | radius_bottom: 0.3, 189 | radius_top: 0.5, 190 | radial_segments: 32, 191 | height_segments: 5, 192 | })), 193 | material: materials.add(StandardMaterial::from(Color::SEA_GREEN)), 194 | transform: Transform::from_xyz(2.0, 0.0, 15.0), 195 | ..Default::default() 196 | }); 197 | 198 | // Single-segment grid 199 | commands.spawn(PbrBundle { 200 | mesh: meshes.add(Mesh::from(Grid::default())), 201 | material: materials.add(StandardMaterial::from(Color::SALMON)), 202 | transform: Transform::from_xyz(4.0, 0.0, 5.0), 203 | ..Default::default() 204 | }); 205 | 206 | // Multi-segment grid 207 | commands.spawn(PbrBundle { 208 | mesh: meshes.add(Mesh::from(Grid { 209 | width: 1.0, 210 | height: 0.6, 211 | width_segments: 10, 212 | height_segments: 6, 213 | })), 214 | material: materials.add(StandardMaterial::from(Color::TEAL)), 215 | transform: Transform::from_xyz(4.0, 0.0, 7.0), 216 | ..Default::default() 217 | }); 218 | 219 | // Single-segment grid textured 220 | commands.spawn(PbrBundle { 221 | mesh: meshes.add(Mesh::from(Grid::default())), 222 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 223 | transform: Transform::from_xyz(4.0, 0.0, 9.0), 224 | ..Default::default() 225 | }); 226 | 227 | // Multi-segment grid textured 228 | commands.spawn(PbrBundle { 229 | mesh: meshes.add(Mesh::from(Grid::new_square(1.0, 12))), 230 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 231 | transform: Transform::from_xyz(4.0, 0.0, 11.0), 232 | ..Default::default() 233 | }); 234 | 235 | // Triangle polygon 236 | commands.spawn(PbrBundle { 237 | mesh: meshes.add(Mesh::try_from(Polygon::new_triangle(0.7)).unwrap()), 238 | material: materials.add(StandardMaterial::from(Color::GREEN)), 239 | transform: Transform::from_xyz(6.0, 0.0, 5.0), 240 | ..Default::default() 241 | }); 242 | 243 | // Octagon polygon 244 | commands.spawn(PbrBundle { 245 | mesh: meshes.add(Mesh::try_from(Polygon::new_octagon(0.7)).unwrap()), 246 | material: materials.add(StandardMaterial::from(Color::SEA_GREEN)), 247 | transform: Transform::from_xyz(6.0, 0.0, 7.0), 248 | ..Default::default() 249 | }); 250 | 251 | // Many-cornered polygon 252 | commands.spawn(PbrBundle { 253 | mesh: meshes.add(Mesh::try_from(Polygon::new_regular_ngon(0.7, 32)).unwrap()), 254 | material: materials.add(StandardMaterial::from(Color::YELLOW)), 255 | transform: Transform::from_xyz(6.0, 0.0, 9.0), 256 | ..Default::default() 257 | }); 258 | 259 | // Star 260 | commands.spawn(PbrBundle { 261 | mesh: meshes.add(Mesh::try_from(Polygon { 262 | points: generate_star_shape(7, 0.7, 0.4), 263 | }).unwrap()), 264 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 265 | transform: Transform::from_xyz(6.0, 0.0, 11.0), 266 | ..Default::default() 267 | }); 268 | 269 | // Simple torus 270 | commands.spawn(PbrBundle { 271 | mesh: meshes.add(Mesh::from(Torus::default())), 272 | material: materials.add(StandardMaterial::from(Color::ALICE_BLUE)), 273 | transform: Transform::from_xyz(8.0, 0.0, 5.0), 274 | ..Default::default() 275 | }); 276 | 277 | // Low poly torus 278 | commands.spawn(PbrBundle { 279 | mesh: meshes.add(Mesh::from(Torus { 280 | radius: 0.8, 281 | tube_radius: 0.2, 282 | radial_segments: 8, 283 | tube_segments: 5, 284 | ..Default::default() 285 | })), 286 | material: materials.add(StandardMaterial::from(Color::PINK)), 287 | transform: Transform::from_xyz(8.0, 0.0, 7.0), 288 | ..Default::default() 289 | }); 290 | 291 | // Thick torus 292 | commands.spawn(PbrBundle { 293 | mesh: meshes.add(Mesh::from(Torus { 294 | radius: 0.5, 295 | tube_radius: 0.3, 296 | ..Default::default() 297 | })), 298 | material: materials.add(StandardMaterial::from(Color::NAVY)), 299 | transform: Transform::from_xyz(8.0, 0.0, 9.0), 300 | ..Default::default() 301 | }); 302 | 303 | // Textured torus 304 | commands.spawn(PbrBundle { 305 | mesh: meshes.add(Mesh::from(Torus::default())), 306 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 307 | transform: Transform::from_xyz(8.0, 0.0, 11.0), 308 | ..Default::default() 309 | }); 310 | 311 | // Half torus 312 | { 313 | let mut mat = StandardMaterial::from(Color::CRIMSON); 314 | mat.cull_mode = None; 315 | commands.spawn(PbrBundle { 316 | mesh: meshes.add(Mesh::from(Torus { 317 | radial_circumference: std::f32::consts::PI, 318 | tube_circumference: std::f32::consts::TAU, 319 | ..Default::default() 320 | })), 321 | material: materials.add(mat), 322 | transform: Transform::from_xyz(10.0, 0.0, 5.0), 323 | ..Default::default() 324 | }); 325 | } 326 | 327 | // Half torus (horizontal cut) 328 | { 329 | let mut mat = StandardMaterial::from(Color::ORANGE_RED); 330 | mat.cull_mode = None; 331 | let mut flipped_transform = Transform::from_xyz(10.0, 0.0, 7.0); 332 | flipped_transform.rotation = Quat::from_rotation_x(std::f32::consts::PI); 333 | commands.spawn(PbrBundle { 334 | mesh: meshes.add(Mesh::from(Torus { 335 | radial_circumference: std::f32::consts::TAU, 336 | tube_circumference: std::f32::consts::PI, 337 | tube_offset: std::f32::consts::PI * 1.5, 338 | ..Default::default() 339 | })), 340 | material: materials.add(mat), 341 | transform: flipped_transform, 342 | ..Default::default() 343 | }); 344 | } 345 | 346 | // 2/3 torus with texture 347 | { 348 | let mut mat = StandardMaterial::from(checkerboard_texture.clone()); 349 | mat.cull_mode = None; 350 | commands.spawn(PbrBundle { 351 | mesh: meshes.add(Mesh::from(Mesh::from(Torus { 352 | radial_circumference: std::f32::consts::PI * 4.0/3.0, 353 | tube_circumference: std::f32::consts::TAU, 354 | ..Default::default() 355 | }))), 356 | material: materials.add(mat), 357 | transform: Transform::from_xyz(10.0, 0.0, 9.0), 358 | ..Default::default() 359 | }); 360 | } 361 | 362 | // Simple tube 363 | { 364 | let mut mat = StandardMaterial::from(Color::WHITE); 365 | mat.cull_mode = None; 366 | 367 | commands.spawn(PbrBundle { 368 | mesh: meshes.add(Mesh::from(Tube { 369 | curve: Box::new(WaveFunction), 370 | ..Default::default() 371 | })), 372 | material: materials.add(mat), 373 | transform: Transform::from_xyz(12.0, 0.0, 5.0), 374 | ..Default::default() 375 | }); 376 | } 377 | 378 | // Knot 379 | commands.spawn(PbrBundle { 380 | mesh: meshes.add(Mesh::from(Tube { 381 | curve: Box::new(Knot { 382 | rotation_winds: 2, 383 | circle_winds: 3, 384 | }), 385 | radius: 0.1, 386 | length_segments: 128, 387 | ..Default::default() 388 | })), 389 | material: materials.add(StandardMaterial::from(checkerboard_texture.clone())), 390 | transform: Transform::from_xyz(12.0, 0.0, 7.0), 391 | ..Default::default() 392 | }); 393 | 394 | // Line tube 395 | { 396 | let mut mat = StandardMaterial::from(Color::WHITE); 397 | mat.cull_mode = None; 398 | 399 | commands.spawn(PbrBundle { 400 | mesh: meshes.add(Mesh::from(Tube { 401 | curve: Box::new(Knot { 402 | rotation_winds: 2, 403 | circle_winds: 3, 404 | }), 405 | radius: 0.0, 406 | length_segments: 128, 407 | ..Default::default() 408 | })), 409 | material: materials.add(mat), 410 | transform: Transform::from_xyz(12.0, 0.0, 9.0), 411 | ..Default::default() 412 | }); 413 | } 414 | 415 | // Flat wave 416 | { 417 | let mut mat = StandardMaterial::from(checkerboard_texture.clone()); 418 | mat.cull_mode = None; 419 | 420 | commands.spawn(PbrBundle { 421 | mesh: meshes.add(Mesh::from(Tube { 422 | radius: 0.2, 423 | radial_segments: 1, 424 | curve: Box::new(WaveFunction), 425 | ..Default::default() 426 | })), 427 | material: materials.add(mat), 428 | transform: Transform::from_xyz(12.0, 0.0, 11.0), 429 | ..Default::default() 430 | }); 431 | } 432 | 433 | // Sun 434 | commands.spawn(DirectionalLightBundle { 435 | directional_light: DirectionalLight { 436 | color: Color::WHITE, 437 | illuminance: 15000.0, 438 | shadows_enabled: true, 439 | ..Default::default() 440 | }, 441 | transform: Transform::from_xyz(100.0, 100.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), 442 | ..Default::default() 443 | }); 444 | 445 | // Ambient light 446 | ambient_light.brightness = 0.2; 447 | 448 | // Sky 449 | commands.spawn(( 450 | PbrBundle { 451 | mesh: meshes.add(Mesh::from(shape::Box::default())), 452 | material: materials.add(StandardMaterial { 453 | base_color: Color::hex("111111").unwrap(), 454 | unlit: true, 455 | cull_mode: None, 456 | ..default() 457 | }), 458 | transform: Transform::from_scale(Vec3::splat(50.0)), 459 | ..default() 460 | }, 461 | NotShadowCaster, 462 | )); 463 | } 464 | 465 | fn generate_star_shape(n: usize, radius_big: f32, radius_small: f32) -> Vec { 466 | let mut positions = Vec::new(); 467 | let angle_step = 2.0 * std::f32::consts::PI / (n * 2) as f32; 468 | for i in 0..2 * n { 469 | let theta = angle_step * i as f32; 470 | if i % 2 == 0 { 471 | positions.push(Vec2::new( 472 | radius_big * f32::cos(theta), 473 | radius_big * f32::sin(theta), 474 | )); 475 | } else { 476 | positions.push(Vec2::new( 477 | radius_small * f32::cos(theta), 478 | radius_small * f32::sin(theta), 479 | )); 480 | } 481 | } 482 | 483 | positions 484 | } 485 | 486 | // Spawn a UI layer with the controls and other useful info. 487 | fn spawn_info_text(mut commands: Commands, asset_server: Res) { 488 | 489 | // Show text that presents the controls 490 | commands.spawn(TextBundle { 491 | style: Style { 492 | align_self: AlignSelf::FlexEnd, 493 | position_type: PositionType::Absolute, 494 | position: UiRect { 495 | top: Val::Px(10.0), 496 | left: Val::Px(10.0), 497 | ..Default::default() 498 | }, 499 | ..Default::default() 500 | }, 501 | text: Text::from_section( 502 | "WASD + Mouse movement\nSpace Up, LShift Down\nESC toggle input grab\nX toggle wireframes", 503 | TextStyle { 504 | font: asset_server.load("fonts/FiraSans-Medium.ttf"), 505 | font_size: 15.0, 506 | color: Color::WHITE, 507 | }, 508 | ).with_alignment(TextAlignment::Left), 509 | ..Default::default() 510 | }); 511 | } 512 | 513 | // Spawn and configure the camera. 514 | fn spawn_camera(mut commands: Commands) { 515 | let mut controller = FpsCameraController::default(); 516 | controller.enabled = false; // we have a system that takes care of this, so disable it to prevent first-frame weirdness 517 | 518 | commands 519 | .spawn(Camera3dBundle::default()) 520 | .insert(FpsCameraBundle::new( 521 | controller, 522 | Vec3::new(0.0, 0.0, 0.0), 523 | Vec3::new(0.0, 0.0, 1.0), 524 | Vec3::new(0.0, 1.0, 0.0), 525 | )); 526 | } 527 | 528 | // Toggles global wireframe mode (all meshes) on a key press. 529 | fn toggle_wireframe_system( 530 | keys: Res>, 531 | mut wireframe_config: ResMut, 532 | ) { 533 | if keys.just_pressed(KeyCode::X) { 534 | wireframe_config.global = !wireframe_config.global; 535 | } 536 | } 537 | 538 | pub struct MouseLockPlugin; 539 | 540 | #[derive(Resource)] 541 | pub struct MouseLock { 542 | /// If the lock is engaged the input will be grabbed and the cursor hidden. 543 | pub lock: bool, 544 | /// The plugin comes with a default toggle system. If you implement your own logic when to lock and unlock, you need to override it. 545 | pub override_default_lock_system: bool, 546 | // Keep track of where the mouse was when it entered, so we can restore its position later. 547 | last_position: Option, 548 | // Keep track of what the last lock status was, so we can detect when we need to toggle. 549 | last_lock: bool, 550 | } 551 | 552 | impl MouseLock { 553 | pub fn new(initially_locked: bool, override_default_lock_system: bool) -> Self { 554 | Self { 555 | lock: initially_locked, 556 | override_default_lock_system, 557 | last_position: None, 558 | last_lock: false, 559 | } 560 | } 561 | 562 | pub fn grab_mode(&self) -> CursorGrabMode { 563 | if self.lock { 564 | CursorGrabMode::Locked 565 | } else { 566 | CursorGrabMode::None 567 | } 568 | } 569 | } 570 | 571 | impl Default for MouseLock { 572 | fn default() -> Self { 573 | MouseLock { 574 | lock: false, 575 | override_default_lock_system: false, 576 | last_position: None, 577 | last_lock: false, 578 | } 579 | } 580 | } 581 | 582 | // Determines the correct lock state based on inputs. ESC to drop focus, click on the window to regain it. 583 | fn automatic_lock_system( 584 | mut lock: ResMut, 585 | keys: Res>, 586 | mouse: Res>, 587 | ) { 588 | // Automatic locking overridden, do nothing. 589 | if lock.override_default_lock_system { 590 | return; 591 | } 592 | 593 | // Check for unlock 594 | if lock.last_lock { 595 | if keys.just_pressed(KeyCode::Escape) { 596 | lock.lock = false; 597 | } 598 | } else { 599 | // The current focus state is the last focus event. 600 | if mouse.just_pressed(MouseButton::Left) { 601 | lock.lock = true; 602 | } 603 | } 604 | } 605 | 606 | // Observed the MouseLock status and updates the actual window config according to the status. 607 | fn update_lock(mut lock: ResMut, mut primary_query: Query<&mut Window, With>) { 608 | 609 | // Change detected 610 | if lock.lock != lock.last_lock { 611 | 612 | let mut window = primary_query.get_single_mut().unwrap(); 613 | 614 | // Locking, save position 615 | if lock.lock { 616 | lock.last_position = window.cursor_position(); 617 | } 618 | 619 | // Set display modes 620 | window.cursor.grab_mode = lock.grab_mode(); 621 | window.cursor.visible = !lock.lock; 622 | 623 | // Unlocked, restore cursor position 624 | if !lock.lock { 625 | // Try to restore cursor position 626 | if let Some(pos) = lock.last_position { 627 | window.set_cursor_position(Some(pos)); 628 | } 629 | } 630 | 631 | // Update done 632 | lock.last_lock = lock.lock; 633 | } 634 | } 635 | 636 | impl Plugin for MouseLockPlugin { 637 | fn build(&self, app: &mut App) { 638 | app 639 | // Add default config 640 | .insert_resource(MouseLock::default()) 641 | .add_system(automatic_lock_system) 642 | .add_system(update_lock.in_base_set(CoreSet::PostUpdate)); 643 | } 644 | } 645 | 646 | fn lock_camera( 647 | mouse_lock: Res, 648 | mut camera_controllers: Query<&mut FpsCameraController>, 649 | ) { 650 | // When the cursor is locked, we want the camera to be active. Otherwise keep it still. 651 | camera_controllers.for_each_mut(|mut cam| cam.enabled = mouse_lock.lock); 652 | } 653 | 654 | fn main() { 655 | App::new() 656 | .insert_resource(Msaa::Sample4) 657 | .add_plugins(DefaultPlugins.set(RenderPlugin { 658 | wgpu_settings: WgpuSettings { 659 | // Wireframes require line mode 660 | features: WgpuFeatures::POLYGON_MODE_LINE, 661 | ..default() 662 | }, 663 | })) 664 | .add_plugin(smooth_bevy_cameras::LookTransformPlugin) 665 | .add_plugin(FpsCameraPlugin::default()) 666 | .add_plugin(WireframePlugin) 667 | .add_plugin(MouseLockPlugin) 668 | .add_plugin(NormalMaterialPlugin) 669 | .add_startup_system(spawn_camera) 670 | .add_startup_system(spawn_shapes) 671 | .add_startup_system(spawn_info_text) 672 | .add_system(toggle_wireframe_system) 673 | .add_system(lock_camera) 674 | .run(); 675 | } 676 | --------------------------------------------------------------------------------