Meshes and Vertices
The built-in vertex layout
Vertex is a fixed layout: position, texture coordinates, normal, and tangent — everything a lit, textured mesh needs. A shader that only reads position still needs a buffer with all four fields populated, since the buffer’s layout is fixed regardless of what any one shader chooses to read:
use pebble::wgpu::mesh::Vertex;
fn triangle_vertices() -> Vec<Vertex> {
let uv = glam::Vec2::ZERO;
let normal = glam::Vec3::Z;
let tangent = glam::Vec4::new(1.0, 0.0, 0.0, 1.0);
vec![
Vertex::new(glam::Vec3::new(0.0, 0.6, 0.0), uv, normal, tangent),
Vertex::new(glam::Vec3::new(-0.6, -0.6, 0.0), uv, normal, tangent),
Vertex::new(glam::Vec3::new(0.6, -0.6, 0.0), uv, normal, tangent),
]
}
Vertex::layout()/InstanceVertex::layout() return an opaque VertexBufferLayout, used in a material’s vertex_layouts — see Materials. @location(0) on a shader’s vertex input has to line up with the position this layout puts things at.
Building a mesh
Mesh — a vertex list plus indices. Fields are private; the only way to construct one is MeshBuilder:
use pebble::wgpu::mesh::{MeshBuilder, Vertex};
let mesh = MeshBuilder::new(
vec![
Vertex::new(glam::Vec3::new(0.0, 0.6, 0.0), glam::Vec2::ZERO, glam::Vec3::Z, glam::Vec4::new(1.0, 0.0, 0.0, 1.0)),
// ...
],
vec![0, 1, 2],
).build_asset("triangle", &mut meshes);
.build_asset returns a Handle<Mesh> — spawning an entity with it (and a material instance handle) as components is how a render system finds them again (see Materials). Assets<Mesh>::get(handle) returns Option<&GPUMesh> (vertex_buffer/index_buffer/index_count), uploaded automatically through the same asset pipeline as every other GPU resource.
A custom vertex struct
Vertex/InstanceVertex cover the common case, but nothing stops a custom vertex type — build its VertexBufferLayout by hand from opaque VertexAttribute/VertexFormat values (no raw wgpu::VertexAttribute/vertex_attr_array! needed):
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct ParticleVertex {
position: glam::Vec3,
size: f32,
}
fn particle_layout() -> VertexBufferLayout {
VertexBufferLayout {
array_stride: std::mem::size_of::<ParticleVertex>() as u64,
step_mode: VertexStepMode::Instance,
attributes: vec![
VertexAttribute { format: VertexFormat::Float32x3, offset: 0, shader_location: 0 },
VertexAttribute { format: VertexFormat::Float32, offset: 12, shader_location: 1 },
],
}
}
offset is the byte offset of that field within the struct (matching its #[repr(C)] layout); step_mode: VertexStepMode::Instance advances the buffer per-instance instead of per-vertex — InstanceVertex::layout() uses the same setting for its model-matrix columns.
SkinnedVertex is a second built-in vertex layout, alongside Vertex/InstanceVertex — the same idea (position/UV/normal/tangent) plus joint indices/weights for skeletal animation.