9eead719b0
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>
257 lines
9.2 KiB
Rust
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,
|
|
}
|