Files
Oxide/engine/src/render/mesh.rs
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

257 lines
9.2 KiB
Rust

//! 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::<Vertex>() 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<Vertex>,
/// Triangle indices into [`vertices`](Self::vertices), three per triangle.
pub indices: Vec<u32>,
}
impl Mesh {
/// Creates a mesh from raw vertex and index data.
pub fn new(vertices: Vec<Vertex>, indices: Vec<u32>) -> 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,
}