bevyengine/bevy · critical

The base mesh did not have vertex positions

Error message

The base mesh did not have vertex positions

What it means

Panic thrown while bevy_mesh's Extrusion primitive builds its mesh. After constructing the front and back caps, the extrusion reads the base shape's vertex positions as VertexAttributeValues::Float32x3 to generate the mantel (side walls). If the mesh produced by the base shape builder has no Mesh::ATTRIBUTE_POSITION attribute, or stores it in a non-Float32x3 format, this panic fires. All built-in 2D primitives supply positions, so hitting it almost always means a custom or modified base shape builder that omits them.

Source

Thrown at crates/bevy_mesh/src/primitives/extrusion.rs:243

                    _ => {
                        panic!("Meshes used with Extrusions must have a primitive topology of `PrimitiveTopology::TriangleList`");
                    }
                };
            }
            back_face
        };

        // An extrusion of depth 0 does not need a mantel
        if self.half_depth == 0. {
            front_face.merge(&back_face).unwrap();
            return front_face;
        }

        let mantel = {
            let Some(VertexAttributeValues::Float32x3(cap_verts)) =
                front_face.attribute(Mesh::ATTRIBUTE_POSITION)
            else {
                panic!("The base mesh did not have vertex positions");
            };

            debug_assert!(self.segments > 0);

            let layers = self.segments + 1;
            let layer_depth_delta = self.half_depth * 2.0 / self.segments as f32;

            let perimeter = self.base_builder.perimeter();
            let (vert_count, index_count) =
                perimeter
                    .iter()
                    .fold((0, 0), |(verts, indices), perimeter| {
                        (
                            verts + layers * perimeter.vertices_per_layer() as usize,
                            indices + self.segments * perimeter.indices_per_segment(),
                        )
                    });
            let mut positions = Vec::with_capacity(vert_count);

View on GitHub (pinned to 396ca72708)

Solutions

  1. In the custom base shape's mesh construction, insert positions as Float32x3: mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions) where positions is Vec<[f32; 3]>
  2. If positions were converted to another format (e.g. half-float or snorm), re-insert them as VertexAttributeValues::Float32x3 before building the Extrusion
  3. Debug-print the base mesh's attributes to confirm what is missing: build the base shape's mesh alone and inspect mesh.attribute(Mesh::ATTRIBUTE_POSITION)
  4. While developing the custom shape, substitute a built-in primitive (Rectangle, Circle, RegularPolygon) as the base to verify the rest of the pipeline

Example fix

// before: custom base shape builds a mesh without positions
impl Primitive2d for MyShape { /* ... */ }
fn build_mesh() -> Mesh {
    let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, default());
    mesh.insert_indices(indices);
    // positions never inserted -> panics inside Extrusion
    mesh
}

// after: always insert Float32x3 positions
fn build_mesh() -> Mesh {
    let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, default());
    mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); // Vec<[f32; 3]>
    mesh.insert_indices(indices);
    mesh
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the base shape's mesh before extruding it (e.g. in a test)
fn base_has_positions(shape: &impl Primitive2d + Extrudable) -> bool {
    let mesh = primitives_extrusion_build_base(shape); // whatever builds the cap mesh
    matches!(
        mesh.attribute(Mesh::ATTRIBUTE_POSITION),
        Some(VertexAttributeValues::Float32x3(_))
    )
}

Type guard

fn has_float32x3_positions(mesh: &Mesh) -> bool {
    matches!(
        mesh.attribute(Mesh::ATTRIBUTE_POSITION),
        Some(VertexAttributeValues::Float32x3(_))
    )
}

Prevention

When it happens

Trigger: Calling .mesh() on an Extrusion (or spawning it as a Mesh) with a non-zero depth, where the base shape's built mesh lacks Mesh::ATTRIBUTE_POSITION or has positions in a format other than Float32x3. A depth of 0 returns early (front face merged with back face) before this check, so only non-zero depths reach the panic.

Common situations: Implementing a custom Primitive2d shape whose mesh construction forgets mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); converting or compressing positions to another VertexFormat before extruding; test meshes stripped of attributes.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/b1deeebbaf06cbd9. Report an issue: GitHub.