Skip to main content

pebble/wgpu/
mesh.rs

1use crate::{
2    assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3    wgpu::{
4        backend::WGPUBackend,
5        buffer::Buffer,
6        buffers::BufferBuilder,
7        flags::BufferUsages,
8        vertex_format::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode},
9    },
10};
11
12/// Standard per-vertex data: position, UV, normal, and tangent (`w` is the
13/// bitangent handedness sign, ±1 — cross `normal` with `tangent.xyz` and
14/// scale by `tangent.w` to get the bitangent).
15///
16/// Occupies vertex buffer locations 0–3 — [`InstanceVertex::layout`]
17/// deliberately starts at location 4 to leave room for this, so pairing
18/// them in the same pipeline doesn't collide. Adding a 5th attribute here
19/// would need a matching shift there.
20#[repr(C)]
21#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
22pub struct Vertex {
23    pub position: glam::Vec3,
24    pub tex_coords: glam::Vec2,
25    pub normal: glam::Vec3,
26    pub tangent: glam::Vec4,
27}
28
29impl Vertex {
30    pub fn new(
31        position: glam::Vec3,
32        tex_coords: glam::Vec2,
33        normal: glam::Vec3,
34        tangent: glam::Vec4,
35    ) -> Self {
36        Self {
37            position,
38            tex_coords,
39            normal,
40            tangent,
41        }
42    }
43
44    pub fn layout() -> VertexBufferLayout {
45        VertexBufferLayout {
46            array_stride: std::mem::size_of::<Vertex>() as u64,
47            step_mode: VertexStepMode::Vertex,
48            attributes: vec![
49                VertexAttribute { format: VertexFormat::Float32x3, offset: 0, shader_location: 0 }, // position
50                VertexAttribute { format: VertexFormat::Float32x2, offset: 12, shader_location: 1 }, // tex_coords
51                VertexAttribute { format: VertexFormat::Float32x3, offset: 20, shader_location: 2 }, // normal
52                VertexAttribute { format: VertexFormat::Float32x4, offset: 32, shader_location: 3 }, // tangent
53            ],
54        }
55    }
56}
57
58/// Per-instance vertex data. Carries a model matrix as four `Vec4` columns,
59/// passed as vertex attributes with `step_mode: Instance`.
60#[repr(C)]
61#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
62pub struct InstanceVertex {
63    pub model: glam::Mat4,
64}
65
66impl InstanceVertex {
67    pub fn new(model: glam::Mat4) -> Self {
68        Self { model }
69    }
70
71    pub fn layout() -> VertexBufferLayout {
72        VertexBufferLayout {
73            array_stride: std::mem::size_of::<InstanceVertex>() as u64,
74            step_mode: VertexStepMode::Instance,
75            attributes: vec![
76                VertexAttribute { format: VertexFormat::Float32x4, offset: 0, shader_location: 4 }, // model col 0
77                VertexAttribute { format: VertexFormat::Float32x4, offset: 16, shader_location: 5 }, // model col 1
78                VertexAttribute { format: VertexFormat::Float32x4, offset: 32, shader_location: 6 }, // model col 2
79                VertexAttribute { format: VertexFormat::Float32x4, offset: 48, shader_location: 7 }, // model col 3
80            ],
81        }
82    }
83}
84
85/// Source data for [`GPUMesh`]: a plain vertex/index list, uploaded as-is.
86/// Fields are private — the only way to construct one is [`MeshBuilder`]:
87/// `MeshBuilder::new(vertices, indices).build()`.
88pub struct Mesh {
89    vertices: Vec<Vertex>,
90    indices: Vec<u32>,
91}
92
93/// Builds a [`Mesh`] from a vertex/index list. Start from
94/// [`new`](Self::new), then finish with
95/// [`build`](Self::build)/[`build_asset`](Self::build_asset).
96pub struct MeshBuilder {
97    vertices: Vec<Vertex>,
98    indices: Vec<u32>,
99}
100
101impl MeshBuilder {
102    pub fn new(vertices: Vec<Vertex>, indices: Vec<u32>) -> Self {
103        Self { vertices, indices }
104    }
105
106    /// Logs a WARN for an empty vertex/index list — nothing would draw, and
107    /// it's a far more likely sign of a forgotten argument than an
108    /// intentionally invisible mesh.
109    fn validate(&self) {
110        if self.vertices.is_empty() {
111            tracing::warn!("MeshBuilder::new(): no vertices — did you forget to pass them?");
112        }
113        if self.indices.is_empty() {
114            tracing::warn!("MeshBuilder::new(): no indices — did you forget to pass them?");
115        }
116    }
117
118    /// Consume the builder and return the finished [`Mesh`] value.
119    pub fn build(self) -> Mesh {
120        self.validate();
121        Mesh { vertices: self.vertices, indices: self.indices }
122    }
123
124    /// Consume the builder, insert into `assets` under `name`, and return
125    /// the resulting [`Handle<Mesh>`].
126    pub fn build_asset(self, name: &str, assets: &mut Assets<Mesh>) -> Handle<Mesh> {
127        let mesh = self.build();
128        assets.insert(name, mesh)
129    }
130
131    /// Load the first static mesh from a glTF file.
132    pub fn from_file(path: &str) -> Result<Self, super::gltf_loader::ModelLoadError> {
133        let model = super::gltf_loader::load_gltf(path)?;
134        let (_, mesh) = model.static_meshes.into_iter().next()
135            .ok_or_else(|| super::gltf_loader::ModelLoadError::MissingData("no static mesh in file".to_string()))?;
136        Ok(Self { vertices: mesh.vertices, indices: mesh.indices })
137    }
138}
139
140/// A mesh uploaded to the GPU. `index_buffer`/`index_count` must stay in
141/// sync if you ever mutate one after construction — there's no invariant
142/// check, so a mismatched pair silently draws garbage or the wrong index
143/// range.
144pub struct GPUMesh {
145    pub vertex_buffer: Buffer,
146    pub index_buffer: Buffer,
147    pub index_count: u32,
148}
149
150impl AssetSource for Mesh {
151    type Processed = GPUMesh;
152}
153
154impl Asset<WGPUBackend> for Mesh {
155    type Deps<'a> = ();
156
157    fn upload<'a>(&self, backend: &WGPUBackend, _deps: &()) -> Option<GPUMesh> {
158        let vertex_buffer = BufferBuilder::with_data(bytemuck::cast_slice(self.vertices.as_slice()))
159            .with_label("Mesh Vertex Buffer")
160            .with_usage(BufferUsages::VERTEX)
161            .build(backend);
162        let index_buffer = BufferBuilder::with_data(bytemuck::cast_slice(&self.indices))
163            .with_label("Mesh Index Buffer")
164            .with_usage(BufferUsages::INDEX)
165            .build(backend);
166        Some(GPUMesh {
167            vertex_buffer,
168            index_buffer,
169            index_count: self.indices.len() as u32,
170        })
171    }
172}
173
174crate::wgpu::plugin_macros::asset_plugin! {
175    /// Registers the [`Mesh`] asset pipeline. Included by
176    /// [`WGPUPlugin`](super::backend::WGPUPlugin); add directly only if you're
177    /// assembling the `wgpu` module's plugins by hand.
178    MeshPlugin, Mesh
179}