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

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…