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.

1 Like

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.

1 Like

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.

1 Like

Glad to hear you got it working @DavidXu2025!