bevyengine/bevy · error · MeshMergeError

Incompatible vertex attribute types

Error message

Incompatible vertex attribute types: {} and {}

What it means

MeshMergeError::IncompatibleVertexAttributes (crates/bevy_mesh/src/mesh.rs:3025), returned by Mesh::merge. When both meshes carry an attribute with the same id, merge extends self's buffer with other's; that only works when the VertexAttributeValues variants match exactly (Float32x3 with Float32x3, etc.). A type mismatch is rejected because concatenating different byte layouts would corrupt the buffer.

Solutions

  1. Convert the mismatched attribute in one mesh so both use the same VertexAttributeValues variant, then merge again
  2. Remove the attribute you do not need from one mesh with mesh.remove_attribute(id) before merging
  3. Regenerate one mesh through the same builder/pipeline as the other so attribute formats line up by construction

Example fix

// before: same attribute id, different formats
let mut a = plane_mesh(); // POSITION: Float32x3
let mut b = imported_mesh(); // POSITION: Float32x2
a.merge(&b); // Err(IncompatibleVertexAttributes { self_attribute: POSITION(Float32x3), other: Float32x2 })

// after: convert b's positions to Float32x3 first
let fixed: Vec<[f32; 3]> = b.attribute(Mesh::ATTRIBUTE_POSITION).unwrap().as_float2().unwrap().iter().map(|p| [p[0], p[1], 0.0]).collect();
b.insert_attribute(Mesh::ATTRIBUTE_POSITION, fixed);
a.merge(&b).unwrap();
Defensive patterns

Strategy: try-catch

Validate before calling

use std::mem::discriminant;
use bevy_mesh::MeshVertexAttributeId;

// true when every attribute shared by both meshes uses the same values variant
fn attributes_compatible(a: &Mesh, b: &Mesh) -> bool {
    a.attributes().all(|(attr, va)| {
        b.attribute(attr.id).map_or(true, |vb| {
            discriminant(va) == discriminant(vb)
        })
    })
}

Try / catch

match a.merge(&b) {
    Ok(()) => {}
    Err(MeshMergeError::IncompatibleVertexAttributes { self_attribute, other_attribute }) => {
        bevy::log::error!(
            "cannot merge: {} vs {:?}", self_attribute.name, other_attribute.map(|a| a.name)
        );
        // convert or remove the offending attribute, then retry once
    }
    Err(e) => bevy::log::error!("merge failed: {e}"),
}

Prevention

When it happens

Trigger: Calling mesh_a.merge(&mesh_b) where an attribute with the same MeshVertexAttributeId exists in both but with different formats — e.g. ATTRIBUTE_POSITION as Float32x3 in one and Float32x2 in the other, or joint indices as Uint16x4 vs Uint32x4. Checked per attribute during the extend loop (mesh.rs:2182).

Common situations: Merging geometry from different sources (a Bevy primitive plus an imported glTF mesh); one side passed through compression/packing (Snorm8x4 normals) while the other kept plain Float32x3; custom attributes registered under the same id with inconsistent formats across code versions.

Related errors


AI-assisted analysis of bevyengine/bevy@8d743eb7dc (2026-08-20). Data as JSON: /api/errors/73268ad964b9978f. Report an issue: GitHub.

Appendix: source

Thrown at crates/bevy_mesh/src/mesh.rs:3105

        }
    }

    /// Create a [`Mesh`] from a [`SerializedMesh`]. See the documentation for [`SerializedMesh`] for caveats.
    ///
    /// Use [`MeshDeserializer`] if you need to pass extra options to the deserialization process, such as specifying custom vertex attributes.
    pub fn into_mesh(self) -> Mesh {
        MeshDeserializer::default().deserialize(self)
    }
}

/// Use to specify extra options when deserializing a [`SerializedMesh`] into a [`Mesh`].
#[cfg(feature = "serialize")]
pub struct MeshDeserializer {
    custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
}

#[cfg(feature = "serialize")]
impl Default for MeshDeserializer {
    fn default() -> Self {
        // Written like this so that the compiler can validate that we use all the built-in attributes.
        // If you just added a new attribute and got a compile error, please add it to this list :)
        const BUILTINS: [MeshVertexAttribute; Mesh::FIRST_AVAILABLE_CUSTOM_ATTRIBUTE as usize] = [
            Mesh::ATTRIBUTE_POSITION,
            Mesh::ATTRIBUTE_NORMAL,
            Mesh::ATTRIBUTE_UV_0,
            Mesh::ATTRIBUTE_UV_1,
            Mesh::ATTRIBUTE_TANGENT,
            Mesh::ATTRIBUTE_COLOR,
            Mesh::ATTRIBUTE_JOINT_WEIGHT,
            Mesh::ATTRIBUTE_JOINT_INDEX,
        ];
        Self {
            custom_vertex_attributes: BUILTINS
                .into_iter()
                .map(|attribute| (attribute.name.into(), attribute))
                .collect(),

View on GitHub (pinned to 8d743eb7dc)