bevyengine/bevy · error · GenerateTangentsError

mesh not suitable for tangent generation

Error message

mesh not suitable for tangent generation

What it means

GenerateTangentsError::MikktspaceError (crates/bevy_mesh/src/mikktspace.rs:73) wraps bevy_mikktspace::GenerateTangentSpaceError: the underlying MikkTSpace implementation ran over the mesh and gave up, meaning the geometry itself is unsuitable for tangent generation. Typical culprits are degenerate (zero-area) triangles, NaN/inf components in positions or UVs, or UVs collapsed so that no valid tangent frame exists.

Source

Thrown at crates/bevy_mesh/src/mikktspace.rs:73

        vert: usize,
    ) {
        let idx = self.index(face, vert);
        self.tangents[idx] = tangent_space.unwrap_or_default().tangent_encoded();
    }
}

#[derive(Error, Debug)]
/// Failed to generate tangents for the mesh.
pub enum GenerateTangentsError {
    #[error("cannot generate tangents for {0:?}")]
    UnsupportedTopology(PrimitiveTopology),
    #[error("missing indices")]
    MissingIndices,
    #[error("missing vertex attributes '{0}'")]
    MissingVertexAttribute(&'static str),
    #[error("the '{0}' vertex attribute should have {1:?} format")]
    InvalidVertexAttributeFormat(&'static str, VertexFormat),
    #[error("mesh not suitable for tangent generation")]
    MikktspaceError(#[from] bevy_mikktspace::GenerateTangentSpaceError),
    #[error("Mesh access error: {0}")]
    MeshAccessError(#[from] MeshAccessError),
}

pub(crate) fn generate_tangents_for_mesh(
    mesh: &Mesh,
) -> Result<Vec<[f32; 4]>, GenerateTangentsError> {
    match mesh.primitive_topology() {
        PrimitiveTopology::TriangleList => {}
        other => return Err(GenerateTangentsError::UnsupportedTopology(other)),
    };

    let positions = mesh.try_attribute_option(Mesh::ATTRIBUTE_POSITION)?.ok_or(
        GenerateTangentsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name),
    )?;
    let VertexAttributeValues::Float32x3(positions) = positions else {
        return Err(GenerateTangentsError::InvalidVertexAttributeFormat(

View on GitHub (pinned to 396ca72708)

Solutions

  1. Sanitize the mesh: reject or drop triangles where two vertex indices coincide or positions are equal (zero area)
  2. Check positions and UVs for non-finite values and clamp/repair them before compute_tangents()
  3. Give UVs a non-degenerate mapping (identical UVs on every vertex cannot define a tangent frame)

Example fix

// before: degenerate soup passes format checks but fails inside mikktspace
mesh.compute_tangents(); // Err(MikktspaceError(GenerateTangentSpaceError))

// after: drop degenerate triangles first
let Indices::U32(idx) = mesh.indices().unwrap() else { unreachable!() };
let kept: Vec<[u32; 3]> = idx.as_chunks().0.iter().copied()
    .filter(|&[a, b, c]| a != b && b != c && a != c)
    .collect();
mesh.insert_indices(Indices::U32(kept.into_iter().flatten().collect()));
mesh.compute_tangents().unwrap();
Defensive patterns

Strategy: validation

Validate before calling

fn geometry_sane_for_tangents(mesh: &Mesh) -> bool {
    let ok_f32 = |x: &f32| x.is_finite();
    let positions_ok = mesh
        .attribute(Mesh::ATTRIBUTE_POSITION)
        .and_then(|v| v.as_float3())
        .is_some_and(|p| p.iter().flatten().all(ok_f32));
    let uvs_ok = mesh
        .attribute(Mesh::ATTRIBUTE_UV_0)
        .and_then(|v| v.as_float2())
        .is_some_and(|u| u.iter().flatten().all(ok_f32));
    let no_degenerate = mesh.indices().is_some_and(|idx| match idx {
        Indices::U16(v) => v.as_chunks().0.iter().all(|&[a, b, c]| a != b && b != c && a != c),
        Indices::U32(v) => v.as_chunks().0.iter().all(|&[a, b, c]| a != b && b != c && a != c),
    });
    positions_ok && uvs_ok && no_degenerate
}

Try / catch

match mesh.compute_tangents() {
    Ok(()) => {}
    Err(GenerateTangentsError::MikktspaceError(e)) => {
        bevy::log::error!("geometry unsuitable for tangents: {e}; check degenerate triangles/UVs");
        // fall back: skip tangents or sanitize mesh and retry once
    }
    Err(e) => bevy::log::error!("{e}"),
}

Prevention

When it happens

Trigger: Calling mesh.compute_tangents() after topology, indices, and attribute presence/format checks have all passed, but the data contains degenerate triangles (repeated vertex indices, coincident positions), NaNs from bad math, or fully-collapsed UVs (all UVs identical).

Common situations: Procedural meshes with accidental duplicate vertices producing zero-area slivers; heightfield geometry with poles or seams where many triangles collapse to a point; imported assets with uninitialized UV channels set to (0,0) everywhere; division by zero upstream producing NaN positions.

Related errors


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