GeometryInstance merge fails with "All attribute lists must have the same number of attributes" for specific rotation-translation combinations

Description

I’m experiencing a consistent error when merging multiple GeometryInstance objects into a single Primitive. The error only occurs with a very specific combination of rotation and translation values:

Error Message:
Reproducible Scenario

I’m building a 3D gizmo (translation widget) with arrows pointing in all six directions (±X, ±Y, ±Z). I reuse the same CylinderGeometry instances for all directions with different modelMatrix transformations.

Code

function calTransModelMatrix(axis, translate) {
  const modelMatrix = Matrix4.IDENTITY.clone();
  
  if (Cartesian3.equals(axis, Cartesian3.UNIT_X)) {
    // ✅ Works fine
    const rotation = Matrix3.fromRotationY(CesiumMath.toRadians(90));
    const translation = Cartesian3.fromElements(translate, 0, 0);
    Matrix4.setTranslation(modelMatrix, translation, modelMatrix);
    Matrix4.setRotation(modelMatrix, rotation, modelMatrix);
  }
  else if (Cartesian3.equals(axis, Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()))) {
    // ❌ FAILS with error
    const rotation = Matrix3.fromRotationY(CesiumMath.toRadians(-90));
    const translation = Cartesian3.fromElements(-translate, 0, 0);  // FAILS
    // const translation = Cartesian3.fromElements(-translate, 0.00001, 0);  // WORKS!
    Matrix4.setTranslation(modelMatrix, translation, modelMatrix);
    Matrix4.setRotation(modelMatrix, rotation, modelMatrix);
  }
  else if (Cartesian3.equals(axis, Cartesian3.negate(Cartesian3.UNIT_Y, new Cartesian3()))) {
    // ✅ Works fine
    const rotation = Matrix3.fromRotationX(CesiumMath.toRadians(90));
    const translation = Cartesian3.fromElements(0, -translate, 0);
    Matrix4.setTranslation(modelMatrix, translation, modelMatrix);
    Matrix4.setRotation(modelMatrix, rotation, modelMatrix);
  }
  else if (Cartesian3.equals(axis, Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()))) {
    // ✅ Works fine
    const rotation = Matrix3.fromRotationY(CesiumMath.toRadians(180));
    const translation = Cartesian3.fromElements(0, 0, -translate);
    Matrix4.setTranslation(modelMatrix, translation, modelMatrix);
    Matrix4.setRotation(modelMatrix, rotation, modelMatrix);
  }
  
  return modelMatrix;
}

// Create geometries (reused for multiple instances)
const arrowGeometry = new CylinderGeometry({
  length: 0.2,
  topRadius: 0,
  bottomRadius: 0.06,
});
const lineGeometry = new CylinderGeometry({
  length: 0.8,
  topRadius: 0.01,
  bottomRadius: 0.01,
});

// Create instances with different transformations
const xArrowNegModelMatrix = calTransModelMatrix(
  Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()),
  0.9  // translate value
);

const xArrowNegInstance = new GeometryInstance({
  id: 'xAxisNeg',
  geometry: arrowGeometry,
  modelMatrix: xArrowNegModelMatrix,
  attributes: {
    color: ColorGeometryInstanceAttribute.fromColor(new Color(1.0, 0.0, 0.0, 0.99)),
  },
});

// Similar instances for xLineNeg, xArrow, xLine...

// Merge into Primitive - THIS FAILS
const xTransPrimitive = new Primitive({
  geometryInstances: [xArrowInstance, xLineInstance, xArrowNegInstance, xLineNegInstance],
  appearance: new PerInstanceColorAppearance({
    flat: true,
    translucent: true,
  }),
  asynchronous: false,
});

The Strange Behavior

FAILS:

  • Rotation: Matrix3.fromRotationY(-90°)

  • Translation: (-0.9, 0, 0) :cross_mark:

WORKS:

  • Rotation: Matrix3.fromRotationY(-90°)

  • Translation: (-0.9, 0.00001, 0) :white_check_mark:

  • Translation: (-0.9, -0.00001, 0) :white_check_mark:

Also WORKS (other axes):

  • Rotation: Matrix3.fromRotationX(90°), Translation: (0, -0.9, 0) :white_check_mark:

  • Rotation: Matrix3.fromRotationY(180°), Translation: (0, 0, -0.9) :white_check_mark:

  • Rotation: Matrix3.fromRotationY(90°), Translation: (0.9, 0, 0) :white_check_mark:

Key Observations

  1. Only fails for negative X-axis with the specific combination of:

    • Y-axis rotation of -90 degrees

    • Translation with Y and Z components exactly 0

  2. Adding any tiny offset (even 1e-5) to the Y or Z component fixes the issue

  3. Other negative axes (Y, Z) work fine with zero components in their translations

  4. The error occurs during Primitive creation when merging multiple GeometryInstance objects that share the same base geometry

Questions

  1. Is this a known issue with geometry instance merging when certain transformation matrices are applied?

  2. Is there a better workaround than adding a tiny epsilon offset?

  3. Should I create separate geometry instances instead of reusing the same geometry?

Environment

  • CesiumJS Version: [1.135.0]

  • Browser: Chrome[142.0.7444.60]

  • OS: [win10]

I’ll start by saying, I’m not entirely sure why this happens - the answer likely lies deep within Cesium’s GeometryPipeline. If you can turn that code snippet into a sandcastle demo, I can take some time later to step through some code and see if I can find an answer.

If I had to guess, though, I’d say that Cesium might be combining geometry where vertices perfectly overlap and perhaps this means one particular instance ends up having more normals than vertices or something. It does sound like what you’re doing should work (i.e. I’d call this a bug), but it’s also pretty low level and not necessarily a common workflow.

Is there a reason you want to do it this way? Is performance of the utmost importance here? (That is, are you drawing thousands of these or something? If you’re just drawing one for the active gizmo, I wouldn’t sweat the performance too much. I’d just make multiple primitives or geometry instances). You could also just make a 3D model of the entire gizmo! (The other workarounds you suggested also seem perfectly reasonable to me).

Best,
Matt