Ellipsoid Rendering Distortion and Origin Offset

I’m new to Cesium-Native and currently working on an OpenGL + Cesium-Native project to render 3DTiles and Ellipsoid. While the 3DTiles render correctly, the Ellipsoid’s meshes are distorted and the origin is not centered on the sphere. I’m rendering a unit sphere, not in the WGS84 Ellipsoid, so I don’t think I should encounter numerical issues. I’ve borrowed a lot of code from Cesium-Unity, but I still can’t figure out where the bug is. Below are the relevant code snippets I think might be related. I’d really appreciate it if someone with experience could take a look. Thank you!

void *PrepareRendererResources::prepareInMainThread(Cesium3DTilesSelection::Tile &tile, void *pLoadThreadResult_) {
    // SPDLOG_INFO("---------prepare in main---------");
    const Cesium3DTilesSelection::TileContent &content = tile.getContent();
    const Cesium3DTilesSelection::TileRenderContent *pRenderContent = content.getRenderContent();
    if (!pRenderContent) {
        return nullptr;
    }

    std::unique_ptr<LoadThreadResult> pLoadThreadResult(static_cast<LoadThreadResult *>(pLoadThreadResult_));
    std::vector<std::shared_ptr<Mesh>> meshes = pLoadThreadResult->meshes;
    const std::vector<CesiumPrimitiveInfo> &primitiveInfos = pLoadThreadResult->primitiveInfos;

    const CesiumGltf::Model &model = pRenderContent->getModel();
    glm::dmat4 tileTransform = tile.getTransform();
    tileTransform = CesiumGltfContent::GltfUtilities::applyRtcCenter(model, tileTransform);
    CesiumGltfContent::GltfUtilities::applyGltfUpAxisTransform(model, tileTransform); 

    int32_t meshSize = meshes.size();
    if (!meshSize) {
        return nullptr;
    }

    std::string name = "glTF";
    auto urlIt = model.extras.find("Cesium3DTiles_TileUrl");
    if (urlIt != model.extras.end()) {
        name = urlIt->second.getStringOrDefault("glTF");
    }

    size_t meshIndex = 0;

    GameComponent* tilesetComponentPtr = pTileset;
    CesiumGeoreference* georeferencePtr = pTileset->getGeoreference();
    model.forEachPrimitiveInScene(
        -1,
        [tilesetComponentPtr, georeferencePtr, &meshes, &primitiveInfos, &meshIndex, &tile, &tileTransform](const CesiumGltf::Model &gltf,
                                                      const CesiumGltf::Node &node,
                                                      const CesiumGltf::Mesh &mesh,
                                                      const CesiumGltf::MeshPrimitive &primitive,
                                                      const glm::dmat4 &transform) {
        auto positionAccessorIt = primitive.attributes.find("POSITION");
        if (positionAccessorIt == primitive.attributes.end()) {
            return;
        }

        int32_t positionAccessorID = positionAccessorIt->second;
        CesiumGltf::AccessorView<glm::vec3> positionView(gltf, positionAccessorID);
        if (positionView.status() != CesiumGltf::AccessorViewStatus::Valid) {
            return;
        }

        const CesiumPrimitiveInfo &primitiveInfo = primitiveInfos[meshIndex];
        glm::dmat4 modelToEcef = tileTransform * transform;

        auto meshInstancePtr = meshes[meshIndex];
        meshInstancePtr->setParent(tilesetComponentPtr);

        CesiumGlobeAnchor* anchor= new CesiumGlobeAnchor(georeferencePtr);
        meshInstancePtr->addGlobeAnchor(anchor);
        anchor->setDetectTransformChanges(false);
        anchor->setAdjustOrientationForGlobeWhenMoving(false);
        anchor->updateLocalToGlobeFixedMatrixAndEcef(modelToEcef);
        anchor->setParent(meshInstancePtr.get());

        const CesiumGltf::Material *pMaterial = CesiumGltf::Model::getSafe(&gltf.materials, primitive.material);
        if (pMaterial) {
            setGltfMaterialParameterValues(gltf, primitiveInfo, pMaterial, meshes[meshIndex++]);
        }
    });

    return new CesiumGltfObject{std::move(meshes), std::move(pLoadThreadResult->primitiveInfos)};
}



CesiumGeospatial::LocalHorizontalCoordinateSystem CesiumGeoreference::createCoordinateSystem() {
    double scaleToMeters = 1.0 / scale;
    if (originPlacement == CesiumGeoreferenceOriginPlacement::TrueOrigin) {
        glm::dmat4 localToEcef(
                               glm::dvec4(scaleToMeters, 0.0, 0.0, 0.0),
                               glm::dvec4(0.0, scaleToMeters, 0.0, 0.0),
                               glm::dvec4(0.0, 0.0, scaleToMeters, 0.0),
                               glm::dvec4(0.0, 0.0, 0.0, 1.0));
        return CesiumGeospatial::LocalHorizontalCoordinateSystem(localToEcef);
    }
    if (originAuthority == CesiumGeoreferenceOriginAuthority::EarthCenteredEarthFixed) {
        return CesiumGeospatial::LocalHorizontalCoordinateSystem(glm::dvec3(ecefX, ecefY, ecefZ),
                                                                 CesiumGeospatial::LocalDirection::East,
                                                                 CesiumGeospatial::LocalDirection::Up,
                                                                 CesiumGeospatial::LocalDirection::South,
                                                                 scaleToMeters,
                                                                 ellipsoid->getNativeEllipsoid());
    }
    return CesiumGeospatial::LocalHorizontalCoordinateSystem(glm::dvec3(longitude, latitude, height),
                                                             CesiumGeospatial::LocalDirection::East,
                                                             CesiumGeospatial::LocalDirection::Up,
                                                             CesiumGeospatial::LocalDirection::South,
                                                             scaleToMeters,
                                                             ellipsoid->getNativeEllipsoid());
}


void Mesh::draw(Shader& shader, glm::dmat4& modelMatrix) {
   ...
    glm::dmat4 mtx = transform * modelMatrix; // transform equals modelToEcef of  `anchor-
    shader.setMat4("model", glm::mat4(mtx)); >updateLocalToGlobeFixedMatrixAndEcef(modelToEcef);`
   ...
}

Hi @DavidXu2025, welcome to the community!

Based on only a very quick look, the first thing that jumps out at me is that this line:

CesiumGltfContent::GltfUtilities::applyGltfUpAxisTransform(model, tileTransform);

Should be:

tileTransform = GltfUtilities::applyGltfUpAxisTransform(model, tileTransform);

As it is in Cesium for Unity. Without that assignment, the line does nothing, and the glTF up axis is not being accounted for.

Thank you for your help! I’ve been debugging and found that the gltfUpAxisValue is 3, and the applyGltfUpAxisTransform function simply returns the rootTransform parameter instead of creating a new one.

 glm::dmat4x4 GltfUtilities::applyGltfUpAxisTransform(
    const CesiumGltf::Model& model,
    const glm::dmat4x4& rootTransform) {
  auto gltfUpAxisIt = model.extras.find("gltfUpAxis");
  if (gltfUpAxisIt == model.extras.end()) {
    // The default up-axis of glTF is the Y-axis, and no other
    // up-axis was specified. Transform the Y-axis to the Z-axis,
    // to match the 3D Tiles specification
    return rootTransform * CesiumGeometry::Transforms::Y_UP_TO_Z_UP;
  }
  const CesiumUtility::JsonValue& gltfUpAxis = gltfUpAxisIt->second;
  int gltfUpAxisValue = static_cast<int>(gltfUpAxis.getSafeNumberOrDefault(1));
  if (gltfUpAxisValue == static_cast<int>(CesiumGeometry::Axis::X)) {
    return rootTransform * CesiumGeometry::Transforms::X_UP_TO_Z_UP;
  } else if (gltfUpAxisValue == static_cast<int>(CesiumGeometry::Axis::Y)) {
    return rootTransform * CesiumGeometry::Transforms::Y_UP_TO_Z_UP;
  } else if (gltfUpAxisValue == static_cast<int>(CesiumGeometry::Axis::Z)) {
    // No transform required
  }
  return rootTransform;  // just return from here.
}

3D Tiles uses a Z-up right-hand frame, while glTF uses a Y-up right-hand frame. In True Origin mode, I noticed that Cesium-Unity switches the Y and Z axes to create a left-hand Y-up frame from the 3D Tiles Z-up right-hand frame

// cesium-unity
LocalHorizontalCoordinateSystem createCoordinateSystem(
    const DotNet::CesiumForUnity::CesiumGeoreference& georeference) {
  double scale = 1.0 / georeference.scale();
  if (georeference.originPlacement() == 
      DotNet::CesiumForUnity::CesiumGeoreferenceOriginPlacement::TrueOrigin) {
    // In True Origin mode, we want a coordinate system that:
    // 1. Is at the origin,
    // 2. Inverts Y to create a left-handed coordinate system,
    // 3. Converts from Z-up to Y-up, and
    // 4. Uses the georeference's scale
    glm::dmat4 localToEcef(
      glm::dvec4(scale, 0.0, 0.0, 0.0),
      glm::dvec4(0.0, 0.0, scale, 0.0),
      glm::dvec4(0.0, scale, 0.0, 0.0),
      glm::dvec4(0.0, 0.0, 0.0, 1.0));
    return LocalHorizontalCoordinateSystem(localToEcef);
  }
  ...
}

So, I did the same and reversed the Z-axis to create an OpenGL right-hand Y-up axis from the 3D Tiles coordinate system.

// cesium-opengl
glm::dmat4 localToEcef(
  glm::dvec4(scaleToMeters, 0.0, 0.0, 0.0),  
  glm::dvec4(0.0, 0.0, scaleToMeters, 0.0),
  glm::dvec4(0.0, -scaleToMeters, 0.0, 0.0), 
  glm::dvec4(0.0, 0.0, 0.0, 1.0)
);

Using the localToEcef from the createCoordinateSystem function in True Origin mode and the applyGltfUpAxisTransform function from preparedInMain , I thought it should work. However, there’s still an origin offset and mesh distortion. So I continued debugging and found that replacing applyGltfUpAxisTransform with tileTransform = tileTransform * CesiumGeometry::Transforms::Y_UP_TO_Z_UP; eliminate the origin offset and mesh distortion, the tile textures are still misplaced. There may still be something I’m missing.

I truly appreciate the support from the Cesium team! I hope more Cesium-native tips and tricks can be shared within the community.

Finally, issues fixed!
The origin of TMS (Tile Map Service) tiles is typically at the top-left corner, while the origin of UV coordinates in OpenGL is at the bottom-left corner, resulting in the Y-axis pointing in opposite directions. Need to apply a transformation of 1.0 - v to the V coordinate when sampling the texture.

Glad to hear you got it working @DavidXu2025!