From 0b8433939d6ad8e9fdf47066225a1fcfb7c491b0 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Fri, 8 Jan 2021 20:39:33 +0100 Subject: [PATCH] bevy_render: add torus and capsule shape (#1223) * bevy_render: add torus shape * bevy_render: add capsule shape * bevy_render: reorganize shape module * bevy_render: add more docs --- crates/bevy_render/src/mesh/shape/capsule.rs | 379 ++++++++++++++++++ .../bevy_render/src/mesh/shape/icosphere.rs | 106 +++++ .../src/mesh/{shape.rs => shape/mod.rs} | 78 +--- crates/bevy_render/src/mesh/shape/torus.rs | 94 +++++ 4 files changed, 585 insertions(+), 72 deletions(-) create mode 100644 crates/bevy_render/src/mesh/shape/capsule.rs create mode 100644 crates/bevy_render/src/mesh/shape/icosphere.rs rename crates/bevy_render/src/mesh/{shape.rs => shape/mod.rs} (77%) create mode 100644 crates/bevy_render/src/mesh/shape/torus.rs diff --git a/crates/bevy_render/src/mesh/shape/capsule.rs b/crates/bevy_render/src/mesh/shape/capsule.rs new file mode 100644 index 00000000000000..f2cbf866bb4cf8 --- /dev/null +++ b/crates/bevy_render/src/mesh/shape/capsule.rs @@ -0,0 +1,379 @@ +use crate::{ + mesh::{Indices, Mesh}, + pipeline::PrimitiveTopology, +}; +use bevy_math::{Vec2, Vec3}; + +/// A cylinder with hemispheres at the top and bottom +pub struct Capsule { + /// Radius on the xz plane. + pub radius: f32, + /// Number of sections in cylinder between hemispheres. + pub rings: usize, + /// Height of the middle cylinder on the y axis, excluding the hemispheres. + pub depth: f32, + /// Number of latitudes, distributed by inclination. Must be even. + pub latitudes: usize, + /// Number of longitudes, or meridians, distributed by azimuth. + pub longitudes: usize, + /// Manner in which UV coordinates are distributed vertically. + pub uv_profile: CapsuleUvProfile, +} +impl Default for Capsule { + fn default() -> Self { + Capsule { + radius: 0.5, + rings: 0, + depth: 1.0, + latitudes: 16, + longitudes: 32, + uv_profile: CapsuleUvProfile::Aspect, + } + } +} + +#[derive(Clone, Copy)] +/// Manner in which UV coordinates are distributed vertically. +pub enum CapsuleUvProfile { + /// UV space is distributed by how much of the capsule consists of the hemispheres. + 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, +} + +impl Default for CapsuleUvProfile { + fn default() -> Self { + CapsuleUvProfile::Aspect + } +} + +impl From for Mesh { + #[allow(clippy::clippy::needless_range_loop)] + fn from(capsule: Capsule) -> Self { + // code adapted from https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db + + let Capsule { + radius, + rings, + depth, + latitudes, + longitudes, + uv_profile, + } = 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 half_depth = depth * 0.5; + let summit = half_depth + 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::default(); vert_len]; + let mut vts: Vec = vec![Vec2::default(); vert_len]; + let mut vns: Vec = vec![Vec3::default(); 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 / (depth + 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::default(); longitudes]; + let mut rho_theta_cartesian: Vec = vec![Vec2::default(); 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::new(0.0, 1.0, 0.0); + + // 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 in 0..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_depth, -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_depth, -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_depth - 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_depth - 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 in 0..lonsp1 { + let j_mod = j % longitudes; + + let s_texture = s_texture_cache[j]; + 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_depth - depth * fac; + + for j in 0..lonsp1 { + let j_mod = j % longitudes; + let tc = theta_cartesian[j_mod]; + let rtc = rho_theta_cartesian[j_mod]; + let s_texture = s_texture_cache[j]; + + 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); + + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); + mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, vs); + mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, vns); + mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, vts); + mesh.set_indices(Some(Indices::U32(tris))); + mesh + } +} diff --git a/crates/bevy_render/src/mesh/shape/icosphere.rs b/crates/bevy_render/src/mesh/shape/icosphere.rs new file mode 100644 index 00000000000000..d4356259ca474e --- /dev/null +++ b/crates/bevy_render/src/mesh/shape/icosphere.rs @@ -0,0 +1,106 @@ +use hexasphere::shapes::IcoSphere; + +use crate::{ + mesh::{Indices, Mesh}, + pipeline::PrimitiveTopology, +}; + +/// A sphere made from a subdivided Icosahedron. +#[derive(Debug)] +pub struct Icosphere { + /// The radius of the sphere. + pub radius: f32, + /// The number of subdivisions applied. + pub subdivisions: usize, +} + +impl Default for Icosphere { + fn default() -> Self { + Self { + radius: 1.0, + subdivisions: 5, + } + } +} + +impl From for Mesh { + fn from(sphere: Icosphere) -> Self { + if sphere.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 = sphere.subdivisions + 1; + let number_of_resulting_points = temp * temp * 10 + 2; + + panic!( + "Cannot create an icosphere of {} subdivisions due to there being too many vertices being generated: {}. (Limited to 65535 vertices or 79 subdivisions)", + sphere.subdivisions, + number_of_resulting_points + ); + } + let generated = IcoSphere::new(sphere.subdivisions, |point| { + let inclination = point.z.acos(); + let azumith = point.y.atan2(point.x); + + let norm_inclination = 1.0 - (inclination / std::f32::consts::PI); + let norm_azumith = (azumith / std::f32::consts::PI) * 0.5; + + [norm_inclination, norm_azumith] + }); + + let raw_points = generated.raw_points(); + + let points = raw_points + .iter() + .map(|&p| (p * 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); + + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); + mesh.set_indices(Some(indices)); + mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, points); + mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + mesh + } +} diff --git a/crates/bevy_render/src/mesh/shape.rs b/crates/bevy_render/src/mesh/shape/mod.rs similarity index 77% rename from crates/bevy_render/src/mesh/shape.rs rename to crates/bevy_render/src/mesh/shape/mod.rs index 72e28938d8276c..5c7213c18dbf4b 100644 --- a/crates/bevy_render/src/mesh/shape.rs +++ b/crates/bevy_render/src/mesh/shape/mod.rs @@ -1,7 +1,6 @@ use super::{Indices, Mesh}; use crate::pipeline::PrimitiveTopology; use bevy_math::*; -use hexasphere::shapes::IcoSphere; pub struct Cube { pub size: f32, @@ -252,75 +251,10 @@ impl From for Mesh { } } -/// A sphere made from a subdivided Icosahedron. -#[derive(Debug)] -pub struct Icosphere { - /// The radius of the sphere. - pub radius: f32, - /// The number of subdivisions applied. - pub subdivisions: usize, -} - -impl Default for Icosphere { - fn default() -> Self { - Self { - radius: 1.0, - subdivisions: 5, - } - } -} - -impl From for Mesh { - fn from(sphere: Icosphere) -> Self { - if sphere.subdivisions >= 80 { - // https://oeis.org/A005901 - let subdivisions = sphere.subdivisions + 1; - let number_of_resulting_points = (subdivisions * subdivisions * 10) + 2; - - panic!( - "Cannot create an icosphere of {} subdivisions due to there being too many vertices being generated: {}. (Limited to 65535 vertices or 79 subdivisions)", - sphere.subdivisions, - number_of_resulting_points - ); - } - let generated = IcoSphere::new(sphere.subdivisions, |point| { - let inclination = point.z.acos(); - let azumith = point.y.atan2(point.x); - - let norm_inclination = 1.0 - (inclination / std::f32::consts::PI); - let norm_azumith = (azumith / std::f32::consts::PI) * 0.5; - - [norm_inclination, norm_azumith] - }); - - let raw_points = generated.raw_points(); - - let points = raw_points - .iter() - .map(|&p| (p * sphere.radius).into()) - .collect::>(); +mod capsule; +mod icosphere; +mod torus; - 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); - - let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); - mesh.set_indices(Some(indices)); - mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, points); - mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, normals); - mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, uvs); - mesh - } -} +pub use capsule::{Capsule, CapsuleUvProfile}; +pub use icosphere::Icosphere; +pub use torus::Torus; diff --git a/crates/bevy_render/src/mesh/shape/torus.rs b/crates/bevy_render/src/mesh/shape/torus.rs new file mode 100644 index 00000000000000..830e9738a56a4d --- /dev/null +++ b/crates/bevy_render/src/mesh/shape/torus.rs @@ -0,0 +1,94 @@ +use crate::{ + mesh::{Indices, Mesh}, + pipeline::PrimitiveTopology, +}; +use bevy_math::Vec3; + +/// A torus (donut) shape. +#[derive(Debug)] +pub struct Torus { + pub radius: f32, + pub ring_radius: f32, + pub subdivisions_segments: usize, + pub subdivisions_sides: usize, +} + +impl Default for Torus { + fn default() -> Self { + Torus { + radius: 1.0, + ring_radius: 0.5, + subdivisions_segments: 32, + subdivisions_sides: 24, + } + } +} + +impl From for Mesh { + fn from(torus: Torus) -> Self { + // code adapted from http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html + // (source code at https://github.com/SEilers/Apparat) + + let n_vertices = (torus.subdivisions_segments + 1) * (torus.subdivisions_sides + 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 / torus.subdivisions_segments as f32; + let side_stride = 2.0 * std::f32::consts::PI / torus.subdivisions_sides as f32; + + for segment in 0..=torus.subdivisions_segments { + let theta = segment_stride * segment as f32; + let segment_pos = Vec3::new(theta.cos(), 0.0, theta.sin() * torus.radius); + + for side in 0..=torus.subdivisions_sides { + let phi = side_stride * side as f32; + + let x = theta.cos() * (torus.radius + torus.ring_radius * phi.cos()); + let z = theta.sin() * (torus.radius + torus.ring_radius * phi.cos()); + let y = torus.ring_radius * phi.sin(); + + let normal = segment_pos.cross(Vec3::unit_y()).normalize(); + + positions.push([x, y, z]); + normals.push(normal.into()); + uvs.push([ + segment as f32 / torus.subdivisions_segments as f32, + side as f32 / torus.subdivisions_sides as f32, + ]); + } + } + + let n_faces = (torus.subdivisions_segments) * (torus.subdivisions_sides); + 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 = torus.subdivisions_sides + 1; + for segment in 0..torus.subdivisions_segments { + for side in 0..torus.subdivisions_sides { + 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); + } + } + + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); + mesh.set_indices(Some(Indices::U32(indices))); + mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, positions); + mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + mesh + } +}