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

Helper Macros

A small set of #[macro_export] macros that collapse the boilerplate that shows up constantly around asset lookups and drawing. They’re plain macro_rules!, exported at the crate root — pebble::or_return!, pebble::bind_mat!, etc. — no prelude needed.

or_return!

Systems return (), so the ? operator isn’t available the way it would be in a function returning Option/Result. or_return! collapses the let Some(x) = expr else { return }; pattern that fills the gap:

fn draw(materials: Read<Assets<Material>>) {
    let material = or_return!(materials.get(handle));
    // ...
}

Pass a second argument to return something other than ():

let material = or_return!(materials.get(handle), return None);

The other three macros are built on top of or_return!, so they inherit this same early-return-when-not-ready behavior — an asset that hasn’t finished uploading yet is a normal, common case (a couple frames on load), not a bug to unwrap() past.

bind_mat! / bind_comp!

The lookup-then-bind every draw call repeats: materials.get(handle), then set_pipeline + set_bind_group(0, ...):

fn draw(
    mut frame: Write<CurrentFrame>,
    materials: Read<Assets<Material>>,
) {
    let Some(mut active) = frame.active() else { return };
    let pass = PassBuilder::new().build();
    let mut render_pass = active.begin_pass(pass);

    bind_mat!(render_pass, materials, material_handle);
}

It evaluates to the looked-up &GPUMaterial, so grab it with let if you also need .update(name, data)/.update_value(name, &value) on it (e.g. a per-frame camera uniform):

let material = bind_mat!(render_pass, materials, material_handle);
material.update("camera", bytemuck::bytes_of(&camera_data));

bind_comp! is the same thing for a ComputePass + Compute instead of a RenderPass + Material.

There’s no combined “bind and draw” macro — a draw call sometimes needs more than one bind group (material at group 0, something else at group 1) before drawing, so bind_mat!/bind_comp! stay composable rather than folded into one rigid macro.

draw_mesh!

Sets the vertex/index buffers and calls draw_indexed for a mesh handle, defaulting the instance range to 0..1:

draw_mesh!(render_pass, meshes, mesh_handle);
draw_mesh!(render_pass, meshes, mesh_handle, 0..enemy_count); // instanced draw

Putting it together

The Recording a Render Pass example, using the full suite:

fn draw(
    mut frame: Write<CurrentFrame>,
    materials: Read<Assets<Material>>,
    meshes: Read<Assets<Mesh>>,
) {
    let Some(mut active) = frame.active() else { return }; // no frame this tick — skip

    let pass = PassBuilder::new()
        .with_target(ColorTargetBuilder::new().with_clear([0.1, 0.1, 0.1, 1.0]).build())
        .build();

    let mut render_pass = active.begin_pass(pass);
    bind_mat!(render_pass, materials, material_handle);
    draw_mesh!(render_pass, meshes, mesh_handle);
}

Five lines of lookups and wiring down to two, with the same not-ready-yet handling as the manual version.