bevyengine/bevy · error · SkinnedMeshBoundsError

The mesh does not contain any joints that are skinned to ver

Error message

The mesh does not contain any joints that are skinned to vertices

What it means

SkinnedMeshBoundsError::NoSkinnedJoints is returned by SkinnedMeshBounds::from_mesh, which computes per-joint AABBs so skinned meshes can be frustum-culled tightly. from_mesh iterates every vertex influence with a non-zero joint weight and adds that vertex's position to the influencing joint's AABB accumulator. The error means at least one weighted influence existed, yet no joint ever received a valid point: every accumulator finished empty. In practice the POSITION attribute does not cover the vertices that carry the weights (too short or empty), or all weighted positions are NaN (f32::min/max silently drop NaN, leaving empty AABBs).

Source

Thrown at crates/bevy_mesh/src/skinning.rs:109

    // Model-space AABBs that enclose the vertices skinned to a joint. Some
    // joints may not be skinned to any vertices, so not every joint has an
    // AABB.
    //
    // `aabb_index_to_joint_index` maps from an `aabbs` index to a joint index,
    // which corresponds to `Mesh::ATTRIBUTE_JOINT_INDEX` and `SkinnedMesh::joints`.
    //
    // These arrays could be a single `Vec<(JointAabb, JointIndex)>`, but that
    // would waste two bytes due to alignment.
    //
    // TODO: If https://github.com/bevyengine/bevy/issues/11570 is fixed, `Vec<_>`
    // can be changed to `Box<[_]>`.
    pub aabbs: Vec<JointAabb>,
    pub aabb_index_to_joint_index: Vec<JointIndex>,
}

#[derive(Copy, Clone, PartialEq, Debug, Error)]
pub enum SkinnedMeshBoundsError {
    #[error("The mesh does not contain any joints that are skinned to vertices")]
    NoSkinnedJoints,
    #[error(transparent)]
    MeshAttributeError(#[from] MeshAttributeError),
}

impl SkinnedMeshBounds {
    /// Create a `SkinnedMeshBounds` from a [`Mesh`].
    ///
    /// The mesh is expected to have position, joint index and joint weight
    /// attributes. If any are missing then a [`MeshAttributeError`] is returned.
    pub fn from_mesh(mesh: &Mesh) -> Result<SkinnedMeshBounds, SkinnedMeshBoundsError> {
        let vertex_positions = expect_attribute_float32x3(mesh, Mesh::ATTRIBUTE_POSITION)?;
        let vertex_influences = InfluenceIterator::new(mesh)?;

        // Find the maximum joint index.
        let Some(max_joint_index) = vertex_influences
            .clone()
            .map(|i| i.joint_index.0 as usize)

View on GitHub (pinned to 396ca72708)

Solutions

  1. Make the POSITION attribute cover all skinned vertices: vertex count of POSITION must equal the count of JOINT_INDEX/JOINT_WEIGHT and be non-zero
  2. Ensure at least one vertex with a non-zero weight has a finite (non-NaN) position; fix whatever produced NaN positions (bad inverse bind matrices, broken animation math)
  3. Sanity-check the mesh before deriving bounds: compare attribute lengths and scan positions for NaN
  4. If you cannot fix the asset, treat the error as a signal and fall back to mesh.compute_aabb() for culling

Example fix

// before: weights for 100 vertices but only 10 positions
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);          // 10 verts
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, joint_indices);   // 100 verts
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, joint_weights);  // 100 verts
SkinnedMeshBounds::from_mesh(&mesh)?; // Err(NoSkinnedJoints)

// after: keep all attribute arrays the same length
assert_eq!(positions.len(), joint_indices.len());
assert_eq!(positions.len(), joint_weights.len());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, joint_indices);
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, joint_weights);
SkinnedMeshBounds::from_mesh(&mesh)?; // Ok(..)
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify skin data actually binds joints to existing vertices before deriving bounds
fn skinning_is_sane(mesh: &Mesh) -> bool {
    let Some(VertexAttributeValues::Float32x3(pos)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else { return false };
    let Some(VertexAttributeValues::Uint16x4(idx)) = mesh.attribute(Mesh::ATTRIBUTE_JOINT_INDEX) else { return false };
    let Some(VertexAttributeValues::Float32x4(wgt)) = mesh.attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT) else { return false };
    let n = idx.len().min(wgt.len());
    (0..n).any(|v| {
        pos.get(v).is_some_and(|p| p.iter().all(|c| c.is_finite()))
            && (0..4).any(|i| wgt[v][i] > 0.0)
    })
}

Try / catch

match SkinnedMeshBounds::from_mesh(&mesh) {
    Ok(bounds) => { /* insert for tight culling */ }
    Err(SkinnedMeshBoundsError::NoSkinnedJoints) => {
        warn!("mesh '{}' has weights but no bound joints; falling back to static AABB", name);
        let aabb = mesh.compute_aabb(); // fallback path
    }
    Err(SkinnedMeshBoundsError::MeshAttributeError(e)) => { /* fix attributes */ }
}

Prevention

When it happens

Trigger: Calling SkinnedMeshBounds::from_mesh(&mesh) (or a system that derives skinned-mesh bounds) on a mesh whose JOINT_INDEX/JOINT_WEIGHT arrays contain entries with weight > 0, but where vertex_positions.get(vertex_index) is None for all of them: POSITION has fewer vertices than the joint arrays (or is empty), or every weighted vertex position is NaN. Note: all-zero weights or an empty vertex list returns Ok(default) instead, not this error.

Common situations: Hand-authored or procedurally generated skinned meshes with mismatched attribute lengths (skin data exported for more vertices than positions); glTF assets whose position stream is truncated; degenerate positions produced by bad bind-pose math; test meshes with placeholder joint weights but no real geometry.

Related errors


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