//! Mesh data: CPU-side [`Mesh`] geometry, its GPU upload ([`GpuMesh`]), and //! built-in primitive builders. //! //! A [`Vertex`] carries position, normal, and UV — the minimal set the Stage 4 //! forward renderer needs for lit, textured-ready geometry. Meshes are built on //! the CPU (procedurally or, later, from a GLTF import) and uploaded once into a //! [`GpuMesh`] for drawing. use bytemuck::{Pod, Zeroable}; use wgpu::util::DeviceExt; use crate::math::{Aabb, Vec2, Vec3}; /// A single mesh vertex: position, normal, and texture coordinate. /// /// `repr(C)` + [`Pod`] so a `&[Vertex]` can be uploaded straight into a GPU /// vertex buffer with no per-field marshalling. #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)] pub struct Vertex { /// Object-space position. pub position: [f32; 3], /// Object-space normal (expected unit length for correct lighting). pub normal: [f32; 3], /// Texture coordinate. pub uv: [f32; 2], } impl Vertex { /// Builds a vertex from math types. pub fn new(position: Vec3, normal: Vec3, uv: Vec2) -> Self { Self { position: position.to_array(), normal: normal.to_array(), uv: uv.to_array(), } } /// The `wgpu` vertex buffer layout matching this struct's fields /// (`@location(0)` position, `@location(1)` normal, `@location(2)` uv). pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &wgpu::vertex_attr_array![ 0 => Float32x3, // position 1 => Float32x3, // normal 2 => Float32x2, // uv ], }; } /// CPU-side mesh geometry: an indexed triangle list. /// /// Indices are `u32` (32-bit), so meshes are not limited to 65k vertices. #[derive(Debug, Clone, Default)] pub struct Mesh { /// Vertex data. pub vertices: Vec, /// Triangle indices into [`vertices`](Self::vertices), three per triangle. pub indices: Vec, } impl Mesh { /// Creates a mesh from raw vertex and index data. pub fn new(vertices: Vec, indices: Vec) -> Self { Self { vertices, indices } } /// Number of triangles (index count / 3). pub fn triangle_count(&self) -> usize { self.indices.len() / 3 } /// The axis-aligned bounds of the mesh in object space /// ([`Aabb::EMPTY`](crate::math::Aabb) for an empty mesh). pub fn bounds(&self) -> Aabb { Aabb::from_points(self.vertices.iter().map(|v| Vec3::from_array(v.position))) } /// Uploads the mesh into GPU vertex/index buffers for drawing. pub fn upload(&self, device: &wgpu::Device, label: &str) -> GpuMesh { let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{label}.vertices")), contents: bytemuck::cast_slice(&self.vertices), usage: wgpu::BufferUsages::VERTEX, }); let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{label}.indices")), contents: bytemuck::cast_slice(&self.indices), usage: wgpu::BufferUsages::INDEX, }); GpuMesh { vertex_buffer, index_buffer, index_count: self.indices.len() as u32, } } /// A unit cube centered at the origin (side length 1), with per-face normals /// and UVs (so each face is flat-shaded correctly). pub fn cube() -> Self { Self::box_mesh(Vec3::splat(1.0)) } /// An axis-aligned box of the given `size` (full extents), centered at the /// origin, with per-face normals and UVs. pub fn box_mesh(size: Vec3) -> Self { let h = size * 0.5; // (normal, then the four corners CCW seen from outside) let faces: [(Vec3, [Vec3; 4]); 6] = [ // +X ( Vec3::X, [ Vec3::new(h.x, -h.y, h.z), Vec3::new(h.x, -h.y, -h.z), Vec3::new(h.x, h.y, -h.z), Vec3::new(h.x, h.y, h.z), ], ), // -X ( Vec3::NEG_X, [ Vec3::new(-h.x, -h.y, -h.z), Vec3::new(-h.x, -h.y, h.z), Vec3::new(-h.x, h.y, h.z), Vec3::new(-h.x, h.y, -h.z), ], ), // +Y ( Vec3::Y, [ Vec3::new(-h.x, h.y, h.z), Vec3::new(h.x, h.y, h.z), Vec3::new(h.x, h.y, -h.z), Vec3::new(-h.x, h.y, -h.z), ], ), // -Y ( Vec3::NEG_Y, [ Vec3::new(-h.x, -h.y, -h.z), Vec3::new(h.x, -h.y, -h.z), Vec3::new(h.x, -h.y, h.z), Vec3::new(-h.x, -h.y, h.z), ], ), // +Z ( Vec3::Z, [ Vec3::new(-h.x, -h.y, h.z), Vec3::new(h.x, -h.y, h.z), Vec3::new(h.x, h.y, h.z), Vec3::new(-h.x, h.y, h.z), ], ), // -Z ( Vec3::NEG_Z, [ Vec3::new(h.x, -h.y, -h.z), Vec3::new(-h.x, -h.y, -h.z), Vec3::new(-h.x, h.y, -h.z), Vec3::new(h.x, h.y, -h.z), ], ), ]; let uvs = [ Vec2::new(0.0, 1.0), Vec2::new(1.0, 1.0), Vec2::new(1.0, 0.0), Vec2::new(0.0, 0.0), ]; let mut vertices = Vec::with_capacity(24); let mut indices = Vec::with_capacity(36); for (normal, corners) in faces { let base = vertices.len() as u32; for (corner, uv) in corners.iter().zip(uvs.iter()) { vertices.push(Vertex::new(*corner, normal, *uv)); } indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]); } Self::new(vertices, indices) } /// A flat plane of `size` units on the XZ axes, centered at the origin, /// facing `+Y`. Useful as a ground reference. pub fn plane(size: f32) -> Self { let h = size * 0.5; let n = Vec3::Y; let vertices = vec![ Vertex::new(Vec3::new(-h, 0.0, h), n, Vec2::new(0.0, 1.0)), Vertex::new(Vec3::new(h, 0.0, h), n, Vec2::new(1.0, 1.0)), Vertex::new(Vec3::new(h, 0.0, -h), n, Vec2::new(1.0, 0.0)), Vertex::new(Vec3::new(-h, 0.0, -h), n, Vec2::new(0.0, 0.0)), ]; Self::new(vertices, vec![0, 1, 2, 0, 2, 3]) } /// A UV sphere of `radius` with `sectors` longitudinal and `stacks` /// latitudinal divisions. Normals are the (normalized) positions. pub fn uv_sphere(radius: f32, sectors: u32, stacks: u32) -> Self { use std::f32::consts::PI; let sectors = sectors.max(3); let stacks = stacks.max(2); let mut vertices = Vec::new(); for i in 0..=stacks { // From +Y pole (phi=0) to -Y pole (phi=PI). let phi = PI * i as f32 / stacks as f32; let (sin_phi, cos_phi) = phi.sin_cos(); for j in 0..=sectors { let theta = 2.0 * PI * j as f32 / sectors as f32; let (sin_theta, cos_theta) = theta.sin_cos(); let dir = Vec3::new(sin_phi * cos_theta, cos_phi, sin_phi * sin_theta); let uv = Vec2::new(j as f32 / sectors as f32, i as f32 / stacks as f32); vertices.push(Vertex::new(dir * radius, dir, uv)); } } let mut indices = Vec::new(); let row = sectors + 1; for i in 0..stacks { for j in 0..sectors { let a = i * row + j; let b = a + row; // Two triangles per quad; skip degenerate ones at the poles. // Vertex order is `a → a+1 → b` and `a+1 → b+1 → b`, which // winds the quad CCW when seen from *outside* the sphere — // the wgpu front-face convention. The previous ordering // (`a, b, a+1` / `a+1, b, b+1`) wound them CW from outside, // which made back-face culling eat the sphere's surface and // showed intersecting opaque meshes through it. if i != 0 { indices.extend_from_slice(&[a, a + 1, b]); } if i != stacks - 1 { indices.extend_from_slice(&[a + 1, b + 1, b]); } } } Self::new(vertices, indices) } } /// A mesh uploaded to the GPU: vertex and index buffers ready to draw. pub struct GpuMesh { /// Vertex buffer, laid out per [`Vertex::LAYOUT`]. pub vertex_buffer: wgpu::Buffer, /// `u32` index buffer. pub index_buffer: wgpu::Buffer, /// Number of indices to draw. pub index_count: u32, }