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>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
//! 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,
|
||||
}
|
||||
Reference in New Issue
Block a user