Placing multiple models using geographic coordinates

Hello,

I’m not quite sure if this is thread should be in the 3D Tiles or CesiumJS category, please move it if you think CesiumJS is more fitting.

In our project we currently have a hard time to place multiple glTF models with geographic coordinates.

Everything is fine when we have tileset with only one model, since we then use a transform which represents both the tileset’s and the buildings center. But we can not figure out the right way to place a model relative to a tileset’s center, which would also be the foundation to place multiple models within one tileset.

We create a campus map where we have a main tileset in which we import one tileset for each building.

The idea is to place the main tileset at the campus’ center. For each building we have geographic coordinates, but we can not establish a workflow to transform the geographic world coordinates of a building to the local coordinate system relative to the campus center.

We calculate the transformation matrix of the campus’ center using Cesium.Cartesian3.fromDegrees() and then Cesium.Transforms.eastNorthUpToFixedFrame().

Our inital idea was that something like


Cesium.Cartesian3.substract(Cesium.Cartesian3.fromDegrees(campusCenter), Cesium.Cartesian3.fromDegrees(building))

should give us the translation for the building.

Only the resulting y is a roughly accurate value though. But in the transformation matrix of the building this value would be the x value.

I created a Sandcastle using a test tileset from us. In the sandcastle’s tileset the first child is the building we want to place on the right spot. It is the building the viewer zooms to. The second child has no transform and thus exists right now only to show where the center of the tileset is.

You can see the geographic coordinates of the campus center and the building in the sandcastle, it also logs the result of Cesium.Cartesian3.subtract.

The first child is roughly in the right area right now, but only because we manually created a transformation matrix (can be seen in the main tileset) by trial and error. But how can we do this accurately using calculations? Also for some buildings we may need to take a custom heading into account when calculating their transformation matrix.

Since the app is part of a research project, we are unable to use CesiumIon. So we can not use any recommendations related to this or other paid services.
I’m pretty sure we are missing something fundamental here, as this seems to be such an ordinary use case even without using CesiumIon.

I’ll gladly answer your questions if something is unclear.

Thanks in advance for your help!

I’m pretty sure we are missing something fundamental here, as this seems to be such an ordinary use case even without using CesiumIon.

Well, yes, but actually, no. Coordinate systems can be complicated, and therefore, the disclaimer: Everything that I say here should be taken with a grain of salt. But I’ll try…


You have the given cartographic coordinates of the campus:

const cartographic = Cesium.Cartographic.fromDegrees(
  6.928297092437934,
  50.92807545113617,
  0
);

These are the latitude/longitude/height (given in degrees here) that describe the position on the globe.


Then you can compute the cartesian coordinates from them:

const cartesian = Cesium.Cartographic.toCartesian(cartographic);

(or using Cartesian3.fromDegrees like you did in the Sandcastle)

These are the coordinates relative to the center of the earth (Earth-Centered Earth-Fixed (ECEF) coordinates)


From that, you can compute the “East-North-Up -to- Fixed Frame” matrix, with

const matrix = Cesium.Transforms.eastNorthUpToFixedFrame(cartesian);

This matrix can roughly be imagined as the matrix that transforms an object from the center of the earth to the respective position, including the proper rotation for that position (some details omitted … and computing that is actually somewhat tricky…)

In your case, this matrix is

-0.12062712460954786,0.9926978879842713,0,0,
-0.7706863170115904,-0.09364951363581787,0.6302954619596158,0,
0.6256929738933811,0.07603072923063509,0.7763553507467535,0,
3998831.079016911,485915.70577487565,4928505.253901741,1

(in column-major form here).

You can see that this matrix does involve a rotation. And this matrix is set as the modelMatrix of the tileset in your case. Without that, the tileset itself is located at “the center of the earth”, but this matrix causes it to end up where the Universität Köln actually is.


Now, you want to place the Geographie-Südbau-Building 303 relative to that, based on the cartographic position of that building.

It looks like you tried to accomplish this by

  • computing the difference (campus - building) of the cartesian coordinates
  • using that as the translation of the tile transform (i.e. as the last column, with the remaining matrix being the identity matrix)

And you mentioned the ‘trial and error’, which apparently consisted of swizzling and adjusting these entries…

The problem:

This transform is applied on top of the transform of the tileset itself. So whatever you’re inserting there as the transform of the tile, it will be post-multiplied to the modelMatrix that you set for the tileset.

(The relevant section in the specification is 3d-tiles/specification at main · CesiumGS/3d-tiles · GitHub )

But how can we do this accurately using calculations?

I think (with the usual disclaimers) that a generic and pragmatic way could be:

  • Compute the ENU matrix of the campus center, campusMatrix
  • Compute the ENU matrix of the building, buildingMatrix
  • Compute the node transform as transform = inverse(campusMatrix) * buildingMatrix

This is the matrix that has to be put into the transform of the tile that contains the building.


One advantage here is that this will take the curvature of earth and the orientation of the center node into account. If you only used the difference in cartesian coordinates and used this as a plain translation, then it would not account for the orientation (rotation) that is implied by the “root” transform (i.e. the campus/modelMatrix). An attempt to draw something was made…:

Well, roughly like that… :roll_eyes:

The curvature may not be sooo important, but the orientation definitely is. That’s why you had to “swizzle” the entries of the translation component to make it “roughly correct”.


However, here’s a sandcastle that combines a few of these things. It contains a few utility functions, e.g. for computing the tile.transform, and prints it to the console for the specific building that you gave in the example:

(Adjust the tileset URL accordingly)

The output will be

buildingNodeTransform
0.9999999873427992,0.00012352201856777323,-0.0001002831598706233,0,
-0.0001235201691789367,0.9999999922012257,0.000018447653096775873,0,
0.0001002854377798873,-0.000018435265870364592,0.9999999948014859,0,
640.9285668455414,-117.50585927954307,-0.03322102688252926,1.0000000000000002

which can just be copy-and-pasted into the transform of the respective node of the tileset JSON, yielding this:

tileset-main.json:

{
  "asset": {
    "version": "1.1"
  },
  "root": {
    "children": [
      {
        "contents": [
          {
            "uri": "tileset-303.json"
          }
        ],
        "boundingVolume": {
          "box": [
            0.032474517822265625,
            0.10713672637939453,
            0,
            2500.923995971679688,
            0,
            0,
            0,
            2500.70527458190918,
            0,
            0,
            0,
            200
          ]
        },
        "refine": "ADD",
        "transform": [
          0.9999999873427992,0.00012352201856777323,-0.0001002831598706233,0,
          -0.0001235201691789367,0.9999999922012257,0.000018447653096775873,0,
          0.0001002854377798873,-0.000018435265870364592,0.9999999948014859,0,
          640.9285668455414,-117.50585927954307,-0.03322102688252926,1.0000000000000002
        ]
      },
      {
        "contents": [
          {
            "uri": "tileset-test.json"
          }
        ],
        "boundingVolume": {
          "box": [
            0.032474517822265625,
            0.10713672637939453,
            0,
            2500.923995971679688,
            0,
            0,
            0,
            2500.70527458190918,
            0,
            0,
            0,
            200
          ]
        },
        "refine": "ADD"
      }

    ],
    "boundingVolume": {
      "box": [
        0.032474517822265625,
        0.10713672637939453,
        0,
        2500.923995971679688,
        0,
        0,
        0,
        2500.70527458190918,
        0,
        0,
        0,
        200
      ]
    },
    "geometricError": 2000
  }
}

Now… this appears to be a pretty “manual” process. And it is, in your case. How much “manual” work is involved here also depends on which tool you are using to create the main tileset JSON to begin with.

For Cesium ion, there is the “Location Editor”, described at Set Location for Data Uploaded to Cesium ion – Cesium . But when you’re not using ion, then all these computations will have to be done “somewhere else”.

Some convenience functionality might be added in the 3d-tiles-tools. For example, I just opened Document and extend the functionality of `createTilesetJson` · Issue #104 · CesiumGS/3d-tiles-tools · GitHub to keep track of a possible functionality to place given glTF assets at a certain position.

But in your case, when you have many glTF assets, and different cartographic positions for each of them, then you’ll probably have to do some computations manually.

Maybe the snippets from the Sandcastle can help, and maybe this (somewhat elaborate) answer helps sorting out the non-trivial cases where a certain tile.transform does not only have to take into account the root transform, but also the transforms of all its parent tiles…