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
- Convert the mismatched attribute in one mesh so both use the same VertexAttributeValues variant, then merge again
- Remove the attribute you do not need from one mesh with mesh.remove_attribute(id) before merging
- 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
- Merged meshes should come from the same builder/pipeline so formats match by construction
- Compare VertexAttributeValues discriminants for shared attribute ids before merging
- Apply vertex compression only after all merges are done
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
- `Mesh::ATTRIBUTE_POSITION` vertex attributes should be of…
- the ' ' vertex attribute should have format
- Incompatible primitive topologies
- Mismatched vertex attribute values
- missing vertex attributes
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)