Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.