Convert a b3dm 3D Tile (1.0) to a glTF with coordinates expressed in any projected CRS (EPSG:code)

Hi,

given a b3dm Cesium 3D Tile having coordinates respecting the “7.3 Bounding Volume” section of the Cesium 3D Tiles 1.0 specification:

An array of six numbers that define a bounding geographic region in EPSG:4979 coordinates with the order [west, south, east, north, minimum height, maximum height]. Longitudes and latitudes are in radians, and heights are in meters above (or below) the WGS84 ellipsoid.

How could I convert it to a glTF file with coordinates expression in any projected CRS knowing it’s EPSG:code?

E.g.:
https://3d.geo.admin.ch/ch.swisstopo.swissbuildings3d.3d/v1/20240501/7/54/21.b3dm (https://3d.geo.admin.ch/ch.swisstopo.swissbuildings3d.3d/v1/tileset.json)
→ to a glTF expressed with coordinates in EPSG:2056

so that I can load it as glTF 2.0 asset in a desktop 3D software working with Cartesian coordinates (for example Blender)?

Because when directly loading the glTF 2.0 “as is”, the shape is rotated according to its position on the globe, which I don’t want because I’m working on a local scale, in a local Cartesian coordinate system.

Thanks.

If the core of the question is about the transform itself, I’d have to check some technical aspects, i.e. how exactly that transformation is done. But I assume that the change can basically be described by a 4x4 matrix, and that the question boils down to

“How to modify the glTF data in a B3DM?”
(where the “modification” is an arbitrary change in the orientation)

Is this correct?

Yes that’s correct, I updated the main question to be clearer. Hopefully.

Starting with some context: There are different options for how “something (i.e. the geometry data in a glTF) can be placed on the globe within a tileset”. Some of them do not make much sense in practice, but in theory, the options are…

  • Using a tile.transform in the tileset JSON that refers to the GLB
  • Using a node.matrix/translation/rotation/scale within the glTF
  • Directly storing the global coordinates in the POSITION attributes of the glTF (not recommended)

(I think that using the tile.transform is the most versatile, as I tried to illustrate in another thread. And it has exactly the advantage that you referred to: In other tools (like Blender), the glTF data is then shown in its “canonical” form, using the “standard glTF coordinates”)

There’s an additional caveat, namely the RTC_CENTER. In the B3DM that you linked to, this is indeed given as

  "RTC_CENTER" : [
    4373246.00084388,
    596545.5803726078,
    4588852.468037587
  ]

There is no tile.transform in the tileset JSON, so I assume that the question is really mainly about the orientation (rotation) of the model.


There are some options for “post-processing” such files. The basic workflow would be:

  • Extract the GLB data from the B3DM
  • Modify the GLB data as desired
  • Create a new B3DM from the modified GLB data

The 3d-tiles-tools do offer some of this functionality (e.g. b3dmToGlb to extract the GLB from a B3DM). But when doing each step manually at the command line, then the Batch- and Feature table information would be lost in that process…

One option to retain this information would be a custom script, based on the 3d-tiles-tools. With a disclaimer: The 3D Tiles Tools do not officially provide a public, stable API for that. They are only intended as a command-line application. But based on the current main state of the repository (this commit), one could use the following snippet:

import fs from "fs";

import { Document } from "@gltf-transform/core";
import { mat4 } from "@gltf-transform/core";

import { TileFormats } from "./src/tilesets";
import { GltfTransform } from "./src/tools";

// Perform a "dummy" modification on the given glTF-Transform
// document: Just change the matrix of the root nodes of all
// scenes in the document
function changeRootTransform(document: Document) {
  const documentRoot = document.getRoot();
  const scenes = documentRoot.listScenes();
  for (const scene of scenes) {
    const nodes = scene.listChildren();
    for (const node of nodes) {
      const oldMatrix = node.getMatrix();

      // Dummy modification: Change the translation of the matrix
      const newMatrix = oldMatrix.slice() as mat4;
      newMatrix[12] = 10.0;
      newMatrix[13] = 20.0;
      newMatrix[14] = 30.0;

      console.log("oldMatrix ", oldMatrix);
      console.log("newMatrix ", newMatrix);
      node.setMatrix(newMatrix);
    }
  }
}

async function postProcessB3dmData(inputB3dmData: Buffer) {
  // Extract the GLB data from the given B3DM data,
  const inputTileData = TileFormats.readTileData(inputB3dmData);
  const inputGlbData = TileFormats.extractGlbPayload(inputTileData);

  // Create a glTF-Transform 'Document' for the GLB
  const io = await GltfTransform.getIO();
  const document = await io.readBinary(inputGlbData);

  // Modify the document
  await document.transform(changeRootTransform);

  // Create the GLB data for the modified document
  const outputGlbData = await io.writeBinary(document);

  // Create a new B3DM data with the modified GLB data,
  // but KEEP the original feature- and batch table data!
  const outputTileData = TileFormats.createB3dmTileDataFromGlb(
    Buffer.from(outputGlbData),
    inputTileData.featureTable.json,
    inputTileData.featureTable.binary,
    inputTileData.batchTable.json,
    inputTileData.batchTable.binary
  );
  const outputB3dmData = TileFormats.createTileDataBuffer(outputTileData);
  return outputB3dmData;
}

async function postProcessB3dm(inputFileName: string, outputFileName: string) {
  const inputB3dmData = fs.readFileSync(inputFileName);
  const outputB3dmData = await postProcessB3dmData(inputB3dmData);
  fs.writeFileSync(outputFileName, outputB3dmData);
}

postProcessB3dm("21.b3dm", "21-modified.b3dm");

So the process would be to check out the repository, run npm install/npm run build, store the given snippet e.g. as PostProcessB3dm.ts in the root directory of the project, and then run it with
npx ts-node ./PostProcessB3dm.ts
to modify the B3DM file (named at the bottom).

There are some inlined comments that describe the basic steps. The main change will be at the place that is currently described as “Dummy modification”: Instead of just arbitrarily changing the matrix, it would be necessary to change that matrix so that it describes the intended change of the coordinate system.

I’m hazy about what that entails. I could imagine that it could be possible to derive the matrix that is necessary to “undo the rotation” from the region that is stored in the tileset - probably the inverse of the ENU matrix at its center. But for questions that are specific to the projections themself, someone else might have to chime in…