Select features from 3D tileset by ID

Hello!

Please excuse me beforehand for asking a stupid question, I am new to 3D tiles.

I want to use an existing tileset, not for display, but for extracting data. Specifically, I want to extract the textures from a selection of features (buildings). I have a set of feature IDs that I know are present in the tileset. The tileset I want to query uses version 1.0 of the 3D tileset specification.

So, my question: Is there a way to select features (glTF assets) in a 3D tileset by ID (preferably in Javascript)?

Thanks in advance,
Frans

There is no “out of the box” functionality for that. But until now, it sounds like something that could probably be implemented with a few lines of custom code, maybe using the 3d-tiles-tools and glTF-Transform for some convenience functionality.

Technically, it is not clear what you mean by “feature IDs that … are present in the tileset”. There are different concepts of “IDs” in different contexts, and these IDs could have different representations - so it might be necessary to add some technical details here…

Thank you Marco13. I will try to explain what I mean by feature IDs. I access the tileset through QGIS. When I identify a feature (building), I am returned the URL of a b3dm file. I think b3dm file represents a 3D tile. When I open the file, I see JSON structures. The first one from the top has key “id” and an array of ID strings as value. From that, I assumed ids to be always present. But perhaps it would be better to speak of feature properties?

I read this in the 3D tiles 1.0 spec: “Per-model properties, such as IDs, enable individual models to be identified and updated at runtime, e.g., show/hide, highlight color, etc. Properties may be used, for example, to query a web service to access metadata, such as passing a building’s ID to get its address. Or a property might be referenced on the fly for changing a model’s appearance, e.g., changing highlight color based on a property value.
So perhaps the correct phrasing for my question is: is it possible to select features by per-model property value?

I see, this referred directly to the IDs for the batch table. So what you are seeing there might correspond to the example from the ‘Batch Table’ section of the specificaiton, namely something like

{
    "id" : ["unique id", "another unique id"],
    "displayName" : ["Building name", "Another building name"],
    "yearBuilt" : [1999, 2015],
    "address" : [{"street" : "Main Street", "houseNumber" : "1"}, {"street" : "Main Street", "houseNumber" : "2"}]
}

The id in this example (that seems to also be in your data) is not always present. The creation tools are free to insert whatever information they want to include there. (In this case, there are two objects, and instead of using the id property, one could just use “their index”, 0 and 1…)


However, there are options for accessing the values (the IDs in your case) from this batch table. But… another open question is what you want to do with these IDs. You said

I want to extract the textures from a selection of features

Is there a way to select features (glTF assets) in a 3D tileset by ID

Which is not entirely clear for me: The IDs are assigned to parts of the geometry data. So there may be one glTF asset that contains the data of objects (like buildings) with the IDs "id0" and "id1" at the same time.

(Again: There are some building blocks that could help here - I just want to make sure to point into the right direction…)

Hi Marco13. yes, you are correct. I see IDs in the batch table, and indeed they match other properties by index.

Thank you for explaining the IDs in the batch table need not always be there. My case is not a general case; I want to extract models from a particular tileset that happens to have IDs in the batch table. I also know that the IDs identify buildings, because they are building IDs from a 2D building dataset. I hope that means that there is one glTF asset for each building.

For further background: In the 2D building dataset, it is possible to select building footprints within an irregular geometry. I don’t think that is possible with a 3D tileset. So I would like to use the set of building IDs from the 2D query to select buildings from the 3D tileset. A next step would be to extract the textures from each building model to do a colour analysis.

OK, then from my current understanding, you have a bunch of B3DM files, and they have associated IDs, and you want to do something with the glTF (payload) of these B3DM files based on the ID. There are several assumptions here, about the presence and structure of these IDs, but maybe that can be sorted out later.


However, there are some pointers that could be helpful for accomplishing your goal here.


The implementation example section in the specification shows a basic, low-level approach for reading that data from a batch table. It also links to the implementation of all that in CesiumJS.

Note: The example there handles the case where the data is stored as a Binary Body Reference. Skipping some details: The batch table can contain something like id: [ "id0", "id1" ] directly in the JSON. But these ID strings could also be stored in binary form. Depending on which assumptions you can make here, the implementation may be trivial and just use the JSON. Or it may be a bit more complex, and have to access the binary data…


Another option for accessing this data could be the 3d-tiles-tools. The 3d-tiles-tools currently do NOT offer a public API. Everything there may change arbitrarily in future versions. (Significant changes are unlikely - but the point is: There are no stability guarantees. It is only veryion 0.4.2).

I’m going out on a limb here, in terms of what you might need or try to accomplish. But the following is implemented against the cloned state of the 3d-tiles-tools, using internal API, but it shows how to access the data from a B3DM more or less conveniently:

import fs from "fs";
import { TileFormats } from "./src/tilesets";
import { B3dmFeatureTable } from "./src/structure";
import { BatchTable } from "./src/structure";
import { BatchTablePropertyTableModels } from "./src/tools";
import { PropertyTableModel } from "./src/metadata";
import { NodeIO } from "@gltf-transform/core";

// Only for information: Print the contents of the given table
function printPropertyTableModel(propertyTableModel: PropertyTableModel) {
  // Print the "column names" of the table (i.e. the property names)
  const propertyNames = propertyTableModel.getPropertyNames();
  console.log("propertyNames: ", propertyNames);

  // Print all rows of the table:
  const numRows = propertyTableModel.getCount();
  for (let r = 0; r < numRows; r++) {
    console.log("Row " + r);

    // Each row is an "entity" that has one property
    // stored in each column
    const entity = propertyTableModel.getMetadataEntityModel(r);
    for (const propertyName of propertyNames) {
      const value = entity.getPropertyValue(propertyName);
      console.log("  " + propertyName + ": " + value);
    }
  }
}

// Print information about the textures that are contained
// in the given GLB data (using glTF-Transform)
async function printTextures(glbData: Buffer) {
  const io = new NodeIO();
  const document = await io.readBinary(glbData);
  const root = document.getRoot();
  const textures = root.listTextures();
  console.log("There are " + textures.length + " textures");
  for (let i = 0; i < textures.length; i++) {
    const texture = textures[i];
    const imageData = texture.getImage();
    console.log("  texture " + i + " image data: ", imageData);
  }
}

async function process(fileName: string) {
  // Read the B3DM data from the file, and create
  // a "TileData" object
  const b3dmBuffer = fs.readFileSync(fileName);
  const tileData = TileFormats.readTileData(b3dmBuffer);

  // Obtain the batch- and feature table information from the tile data
  const batchTable = tileData.batchTable.json as BatchTable;
  const batchTableBinary = tileData.batchTable.binary;
  const featureTable = tileData.featureTable.json as B3dmFeatureTable;
  const numRows = featureTable.BATCH_LENGTH;

  // A lot of magic is happening here: Create a "table" object from
  // the batch table, that allows accessing the rows (objects)
  // and columns (properties)
  const propertyTableModel = BatchTablePropertyTableModels.create(
    batchTable,
    batchTableBinary,
    {},
    numRows
  );
  printPropertyTableModel(propertyTableModel);

  // Check, for example, row 3 of the table, and see whether
  // it contains the expected ID. If it does, print information
  // about the textures that are contained in the GLB data.
  const row = 3;
  const expectedId = 3;
  const entity = propertyTableModel.getMetadataEntityModel(row);
  const actualId = entity.getPropertyValue("id");

  if (actualId === expectedId) {
    console.log("Found expected ID in row " + row);
    await printTextures(tileData.payload);
  }
}

process("batchedWithBatchTable.b3dm");

The crucial part there is the PropertyTableModel interface. This is described in this image, but since it is an internal API, it is not documented beyond that.

For a given B3DM file, this will print something like

propertyNames:  [ 'id', 'Longitude', 'Latitude', 'Height', 'info', 'rooms' ]
Row 0
  id: 0
  Longitude: -1.31968
  Latitude: 0.698874
  Height: 6.155801922082901
  info: [object Object]
  rooms: room0_a,room0_b,room0_c
Row 1
  id: 1
  Longitude: -1.3196832683949145
  Latitude: 0.6988615321420496
  Height: 13.410263679921627
  info: [object Object]
  rooms: room1_a,room1_b,room1_c
...

It then checks a certain row to see whether it has the expected ID. If it does, it prints information about the textures that are contained in the glTF data (using glTF-Transform)

Thanks a lot for the thorough and hopeful response! It will take me a while to process and test this, but I will get back to you.

Finally I have found an opportunity to look into the matter further.
The main assumption is true: I have a bunch of B3DM files, in fact all B3DM files that are in the tileset. From each B3DM file, I want to extract all glTF building models and their associated ID. With that, it should be possible to make selections by ID.

From looking at the B3DM files, I can tell the IDs are included in non-binary JSON. So, if I understand correctly, a trivial implementation of CesiumJS is within reach. Unfortunately, I do not understand yet how to access the batch table. In the implementation example there is a batchTableJSON object. But I don’t understand how to get that object.
And suppose I do have an array of ID’s from the batch table, how could CesiumJS be used to get the matching glTF data?

The code in the specification is intended as an example (in a non-normative section) of how one could decode the data, when it has the structure that is shown there. The link into the CesiumJS source code leads to the Cesium3DTileBatchTable, which is an actual, complete implementation example, so to speak. But this is used deeply inside of CesiumJS. There is no “easy” way to ~“run this code on some given input data”. The batchTableJSON from the specification corresponds to the batchTableJson that is read in Geometry3DTileContent, but again, in a context where it may not be immediately obvious where the input data is coming from.

If you want to use CesiumJS for that, then the Cesium3DTileBatchTableSpec may be an entry point, showing how to use this class. But if you want to manually process individual files and extract certain information, then it might be easier to use the 3d-tiles-tools for that.

And suppose I do have an array of ID’s from the batch table, how could CesiumJS be used to get the matching glTF data?

Note that I wrote above:

So there may be one glTF asset that contains the data of objects (like buildings) with the IDs "id0" and "id1" at the same time.

There’s still the question of how to figure out which ID belongs to which part of the glTF, and even when you know that, it may be difficult to “extract” this part of the glTF (!) and store it as an individual file. Generally, you can access the _BATCHID attribute from the glTF and determine which batch each vertex belongs to. But when there are multiple IDs in one attribute, you’d have to do some lower-level geometry processing. (Maybe this is not the case in your data - we don’t know that yet…)