Unable to load b3dm tileset to scene

I have a b3dm object, that is available by this link. I created this b3dm by 3d-tiles-tools. I also have a tileset.json with such contents:

{
             "asset": {
                "version": "1.0"
             },
            "geometricError": 0,
            "root": {
                "boundingVolume": {
                  "region": [43.970149521784, 56.260414816738106, 43.9702, 56.26042, 0, 10]
               },
              "geometricError": 0,
              "refine": "ADD",
              "content": {
                "uri": "model.b3dm"
              }
          }
   }

This tileset is available by this link

I try to add my model to the scene like so:

 const tileset = await Cesium.Cesium3DTileset.fromUrl("/files/digitaltwineditor/models/ee38fd54-4dbb-4dd7-b10e-270475bc5707/15c23b48-75da-4606-b7ae-1632baaa0d8f/tileset.json", {
      debugShowBoundingVolume: true,
      debugShowGeometricError: true,
      debugShowUrl: true,
      debugWireframe: true
    });
    viewer.scene.primitives.add(tileset);

But it does not work. I see no model rendered. I expect to see this model somewhere near these WGS84 coordinates 43.970149521784, 56.260414816738106. But there is just white screen.

The initial model was in glb format - it is just a simple tree. You can see this model attached to the topic.
849af735-3acc-4978-a70c-97580db543fa.glb (178.7 KB)

Finally, the whole reproducible example is running at this link

What is wrong with that and how can I fix it? Many thanks in advance!

I switched on OSM imagery and see that I’m somewhere in Ocean. It seems like region property is defined incorrectly. Probably it should be in EPSG:3857

The tileset.json contains

"boundingVolume": {
  "region": [43.970149521784, 56.260414816738106, 43.9702, 56.26042, 0, 10]
},

and you said

I expect to see this model somewhere near these WGS84 coordinates 43.970149521784, 56.260414816738106 .

It might be that there is a misunderstanding: The boundingVolume of a tile does not place the tile into that bounding volume. Instead, it is supposed to describe where the tile data actually is.

The bounding box of the given B3DM file is

min: (-6.922408011098346,-0.46230301526023787,-8.381912988737927)
max: (8.597974959256684,16.36762981635396,7.617428474129772)

meaning that this B3DM is placed “at the origin”, and this is totally unrelated to the bounding region that is given in the tileset.


Details of the answer now depend on the actual goal:

  • Should that B3DM be displayed at a certain position on the globe? Then you can specify a tile transform for the tile that refers to this B3DM
  • Do you want to place the tileset at a certain position on the globe? Then you only have to set the proper bounding volume in the tileset.json, and can set the location of the tileset at runtime.

I think that the second one is a bit more versatile, but depending on the application case, you might want to have the B3DM geolocated directly. However, if the goal is the second one, then you could use the following as your tileset.json file:

{
  "asset": {
    "version": "1.1"
  },
  "geometricError": 4096,
  "root": {
    "boundingVolume": {
      "box": [
        0.8377834740791688,
        0.38224225730407735,
        7.952663400546863,
        7.760191485177515,
        0,
        0,
        0,
        -7.9996707314338495,
        0,
        0,
        0,
        8.4149664158071
      ]
    },
    "geometricError": 512,
    "content": {
      "uri": "model.b3dm"
    },
    "refine": "ADD"
  }
}

Then you can create a tileset from that, and set it to the desired location, by setting its modelMatrix, as shown in this sandcastle:

const viewer = new Cesium.Viewer("cesiumContainer");

// Create the tileset in the viewer
const tileset = viewer.scene.primitives.add(
  await Cesium.Cesium3DTileset.fromUrl(
    "http://localhost:8003/tileset.json", {
    debugShowBoundingVolume: true,
  })
);

// Move the tileset to a certain position on the globe
const transform = Cesium.Transforms.eastNorthUpToFixedFrame(
  Cesium.Cartesian3.fromDegrees(43.970149521784, 56.260414816738106, 0)
);
tileset.modelMatrix = transform;

// Zoom to the tileset, with a small offset so that
// it is fully visible
const offset = new Cesium.HeadingPitchRange(
  Cesium.Math.toRadians(-22.5),
  Cesium.Math.toRadians(-22.5),
  60.0
);
viewer.zoomTo(tileset, offset);

The result will then be this:

Cesium Single B3DM


An aside:

The tileset.json that was shown above was created with the 3d-tiles-tools. There is a pull request in the 3d-tiles-tools that was merged yesterday, which contains a createTilesetJson function. This function is not yet part of the release, but if you check out this branch locally, then you can use this function to create a tileset.json file for your model.b3dm file like this:

npx ts-node ./src/main.ts createTilesetJson -i C:\data\model.b3dm -o C:\data\tileset.json

@Marco13 Thank you, Marco for such a detailed answer! I’m relatively new to all these concepts and have some extra question. You say, that the given B3DM have some specific bounding box and provide some values for min and max. The question is - how can I read myself these values? Is there any reader for this format? And the second question refers to the main goal of the topic. I guess, my main goal is not to position the model at runtime, since I do not expect cesium to be the sole client of my data. I mean to say, that I need the position to be hard coded inside tileset.json file. So, is it possible to implement through tile transform? Are there any examples with model instances positioned to some known coordinates in WGS84? Many-many thanks in advance!

@Marco13. I updated my example. I know use transorm function and it seems to work exactly like it should. At the same time the result looks very strange now.

It does not look like a tree in your screen. What may be wrong? Thanks!

You mentioned earlier

The initial model was in glb format - it is just a simple tree.

and you created a B3DM file from that. It may be worth mentioning that when you use 3D Tiles 1.1 (instead of 1.0), then you can also use .GLB files directly. So you could create the corresponding tileset.json like this:

{
  "asset": {
    "version": "1.1"          // When you use "1.1" here....
  },
...
    "content": {
      "uri": "model.glb"      // ... then you can use the GLB file here
    },
...
  }
}

Beyond that, there are different tools for dealing with B3DM data, but I’m not aware of any tool that can directly compute the bounding box of a B3DM. One … solution (although a bit cumbersome) is

  • Extract the .GLB file from the B3DM, e.g. with the 3d-tiles-tools b3dmToGlb command
    • (You don’t have to do this, because you already have the GLB file!)
  • Drop the GLB file into https://gltf.report/
  • Select the “Metadata” tab

This will show be bounding box, as in the lower right of this screenshot:

When you have this bounding box, with its (min,max) points, then you still have to convert this into the boundingVolume.box representation of 3D Tiles. There, you need the (center,halfAxes) representation (because it can not only represent an Axis-Aligned bounding box, but also an Oriented bounding box). The conversion from the (min,max) representation into the representation that is used for 3D Tiles is… simple, but … currently, also has to be done manually. The computation is shown, for example, in this code snippet.

All this is indeed a bit cumbersome. I think that some of this could be simplified. One idea would be to add this information as part of the output of the analyze command of the 3D Tiles Tools.

I opened Extend `analyze` command to include bounding volume information · Issue #59 · CesiumGS/3d-tiles-tools · GitHub for this. This could be a low-hanging fruit, be implemented with reasonable effort, and might be useful for various sorts of analysis and debugging.


I guess, my main goal is not to position the model at runtime […] I mean to say, that I need the position to be hard coded inside tileset.json file.

… it seems to work exactly like it should. At the same time the result looks very strange now.

This looks like … the transform matrix is wrong.

One way of determining the right matrix could be:

  • Create the transform with the eastNorthUpToFixedFrame function (for a given position)
  • Print this as a flat array to the console

For example, the output of this

const transform = Cesium.Transforms.eastNorthUpToFixedFrame(
  Cesium.Cartesian3.fromDegrees(43.970149521784, 56.260414816738106, 0)
);
console.log(Cesium.Matrix4.pack(transform, new Array(16), 0));

will be

-0.6942835079850745,0.7197016121559956,0,0,-0.5984826914710163,-0.5773457436868751,0.5554190852466787,0,0.399736011074243,0.3856183109069252,0.831570586146325,0,2555492.883786797,2465239.113014277,5280601.728445846,1

And you can just copy+paste this into the transform array of the root tile.

The final tileset.json should then be

{
  "asset": {
    "version": "1.1"
  },
  "geometricError": 4096,
  "root": {
    "transform": [
        -0.6942835079850745,0.7197016121559956,0,0,
        -0.5984826914710163,-0.5773457436868751,0.5554190852466787,0,
        0.399736011074243,0.3856183109069252,0.831570586146325,0,
        2555492.883786797,2465239.113014277,5280601.728445846,1
    ],
    "boundingVolume": {
      "box": [
        0.8377834740791688,
        0.38224225730407735,
        7.952663400546863,
        7.760191485177515,
        0,
        0,
        0,
        -7.9996707314338495,
        0,
        0,
        0,
        8.4149664158071
      ]
    },
    "geometricError": 512,
    "content": {
      "uri": "model.b3dm"
    },
    "refine": "ADD"
  }
}

This can be loaded with this sandcastle:

const viewer = new Cesium.Viewer("cesiumContainer");

// Create the tileset in the viewer
const tileset = viewer.scene.primitives.add(
  await Cesium.Cesium3DTileset.fromUrl(
    "http://localhost:8003/tileset.json", {
    debugShowBoundingVolume: true,
  })
);

// Zoom to the tileset, with a small offset so that
// it is fully visible
const offset = new Cesium.HeadingPitchRange(
  Cesium.Math.toRadians(-22.5),
  Cesium.Math.toRadians(-22.5),
  60.0
);
viewer.zoomTo(tileset, offset);

And it will display the tree at the same location as before, but without setting the modelMatrix at runtime.

1 Like

Thank you, sir! You really saved my day!