The transforms of a glTF are automatically multiplied by a matrix that corrects their axes to a Z-up, X-forward frame of reference. Model.fromGltf assumes the input model is Y-up, Z-forward in accordance with the glTF 2.0 spec. However, node.originalMatrix should be the original transform as specified in the glTF, so something sounds wrong.
Can you give us an example of the incorrect outputs, preferably as a Sandcastle?
Here is an updated (and summed up) version of my take on this issue:
The sandcastle test below loads two GLTF1.0 and GLTF2.0 models showing their native export axis and a small aircraft (as a node) that should be pointing to a heading 0 and is positioned 100 forward, 50 up. Camera points north by default.
Both models were created with 3DS Max (which works with a Z up, Y forward convention)
GLTF1.0 is exported using Open Collada exporter and converted using the COLLADA2GLTF utility.
GLTF2.0 is exported with the 3DSMax Babylon plugin.
I am unsure why I need to use the Y up, X forward options to orient the models properly when loading them with Cesium.Model.fromGltf.
I confirm model nodes originalMatrix contains the matrix as written in the file during export and is not transformed in any way: this makes it inconsistent from 1.0 to 2.0 as shown in the example.
I need a way to retrieve node positions, relative to the model, consistent across GLTF versions.
it is confusing to have the two versions showing properly oriented models (visually) but returning different position for their nodes
would it make sense for originalMatrix to be transformed as well, even though it would not be what was exported to GLTF?
Or, could there be a third “transformedOriginalMatrix” node property, for convenience, that would reflect the visual transform applied to the model? (perhaps originalMatrix could be renamed into “exportedMatrix” and originalMatrix be transformed?) .
looking at both model matrices, I see that they are identical despite their “visual” orientation being transformed. How does Cesium rotate GLTF2.0 models internally?
We rotate the glTF conventions to z-up since CesiumJS predominately uses earth-centered, earth-fixed (ECEF) coordinates. This is a z-up coordinate system (and similarly, local east-north-up coordinates are also z-up). So the upAxis and forwardAxis are describing the model’s coordinate system before converting to ECEF.
The code here is very closely tied to the glTF 2.0 convention of y-up, z-forward, as described in the spec, which matches what you said above for glTF 2.0.
I’ll admit I’m not an expert on the glTF 1.0 spec, my knowledge is mostly about 2.0. But looking it up in the spec, I see that even glTF 1.0 uses a y-up coordinate system, though it doesn’t specify which way is forward(!!). However, in your original message you mention expecting it to be Z-up, y-forward for glTF 1.0, is this from the 3DS Max convention you mentioned?
For geospatial data (in particular 3D Tiles) sometimes we have data sets that use z-up coordinate systems even within the glTF tile contents. In such cases, sometimes specifying z-up is necessary so the tileset is right-side-up and not on its side.
I need to spend a bit of time looking closer at your Sandcastle (thanks for providing that!) and models to understand the glTF 1.0 vs 2.0 difference you’re seeing before I can comment on the rest of your questions about node transforms.
Thank you @ptrgags for the explanations: this clears things up a bit further.
However, the code in getAxisCorrectionMatrix does not explain why I explicitly have to specify X as forwardAxis in order to have my model properly aligned.
I am not 100% sure about GLTF1.0 orientation either. What I am sure about is that in order to translate nodes I used to do:
let transform = Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(x, y, z));
node.matrix = Cesium.Matrix4.multiply(node.matrix, transform, node.matrix);
This would move nodes exactly as in 3DSMax (Z-up, Y-forward).
My biggest problem now, as explained earlier, is the discrepancy in node matrices relative to the model between the two GLTF versions (and why they would not be re-oriented when different up and forward axes are specified). I would really appreciate your insight on this.
I inspected the data a little, and it looks like the 2 different export paths create the node transforms in two different ways.
I looked at the POSITION attribute min/max and the node transforms. It looks like the raw POSITION data is rotated 90 degrees between the two models. It seems the glTF 1.0 has an extra node with a Y_UP_Transform that makes up for this difference.
[-5.3, 5.3] (10.6m, “wings”)
[-5.3, 5.3] (10.6m, “wings”)
[-4.8, 1.8] (6.6m, “forward/fuselage”)
[-1.6, 1.3] (2.9m, “up”)
[-1.5, 1.3] (2.8m, “up”)
[-3.1, 3.6] (6.6m, “forward/fuselage”)
node transformations in glTF
(plane) scale by 11.8894, then translate by (0, 100, 50). (Y_UP_Transform) then rotate +z → +y (and +y → -z)
(plane) scale by 11.8894, then translate by (0, 50, -100). No axis transform
I’m not sure where the Y_UP_Transform came from (I’m guessing COLLADA2GLTF?), but I understand why this is there. It’s very similar to a case in 3D Tiles where the raw positions are in z-up data, but you need to rotate +z -> +y to be a valid glTF which is y-up. Then the axis correction (in CesiumJS) cancels it out and you get z-up again. See this section of the 3D Tiles spec, especially the implementation note which talks about adding a matrix to the glTF.
Given the structure of your data, you might be able to get away with just calling getNode("Y_UP_Transform").originalMatrix and multiplying that on the left of the originalMatrix. like so. Or find some other way of accounting for that additional matrix for the glTF 1.0 models. Of course, if you can figure out a way to make the data export more consistently between glTF 1.0/2.0, that would be even better.
As for the x-forward vs y-forward difference, I’m not 100% certain (there’s a lot of rotations going on here), but I think it’s due to the geometry pointing along a y-forward rather than z-forward direction. Let’s check this by examining the glTF 1.0 through it’s long chain of rotations. I’ll be using the upAxis = Y, forwardAxis = X case.
First, let’s remember that CesiumJS is in ECEF. the origin is the center of earth. +x points towards (0, 0) on the globe, +y points towards the Indian ocean, +z points towards the north pole.
POSITION (glTF): plane is pointing along the y-axis (towards Indian Ocean)
after Y_UP_Transform (z-up to y-up) (glTF): plane is rotated to point along the z-axis (towards the North Pole)
after axis correction (y-up to z-up) (CesiumJS): This cancels out the previous one. plane is now pointing along the y-axis (towards Indian Ocean).
After ENU transform (CesiumJS): This translates/orients the mesh so +x points to a local east, +y points to a local north, and +z is up (perpendicular to ellipsoid). We were pointing along the y-axis, so now it points north, as we want.
Thank you again, @ptrgags, for you time investigating this problem.
Please note that not specifying X as forward axis makes both GLTF1.0 and GLTF2.0 to be rotated in the same way: this is not a “GLTF1.0 only” situation.
Also, my real problem, as described before, is to be able to retrieve nodes position, relative to Cesium native orientation, in a way that is consistent between GLTF1.0 and GLTF2.0. When I say “Cesium native orientation”, I mean the final orientation my model ends up in after whatever transforms and rotations being applied to make it show properly.
Put differently: why, when I find a configuration/transform that displays the model the way I want it, nodes matrices are not reflecting this configuration/transform and always return the same position.
Again, as stated in my previous comment:
I understand that nodes’ originalMatrix contains the matrix as written in the file during export and is not transformed in any way
would it make sense for originalMatrix to be transformed according to up and forward axes configuration, even though it would not be what was exported to GLTF?
Or, could there be a third “transformedOriginalMatrix” node property, for convenience, that would reflect the visual transform applied to the model? (perhaps originalMatrix could be renamed into “exportedMatrix” and originalMatrix be transformed?).
looking at both model matrices, I see that they are identical despite their “visual” orientation being transformed. How does Cesium rotate GLTF2.0 models visually without updating their matrices? Is this because of an extra rotated node that wraps the origina model? (the “Y_UP_Transform” you mentioned)
Thinking a bit more about what you’re trying to do, It sounds like the problem is you’re missing some of the matrix transforms that are applied after the node transforms. The node transform in getNode().originalMatrix is intended to only be a local transformation. The other relevant matrices (like the axis correction matrix and model matrix exist in memory, they’ve just been private API.
Here are some additional (private API) matrices. Try these and see if any of them are helpful to your use case. If they are, maybe we can think about whether it’s worth adding some to the API (though likely read-only access)
model.sceneGraph.axisCorrectionMatrix and other matrices. See ModelSceneGraph.js. This will give you the axis transform and model matrix.
Also note that the runtime nodes have originalTransform and transform (the latter of which gets updated over time when you change it).
As for why the models look similar but not quite the same, it’s what you said there, the Y_UP_Transform is the big difference in your input data. But after you apply that node transform, any matrix you chain on afterwards (axis correction, the ENU model matrix, etc) will be the same for both.
Interesting discussion here.
From my own experience, transformations (and especially rotations) can become pretty messy and tangled up if one does not follow very strict convention & notation rules. I typically follow the ones laid out by the robotic operating system, see:
I tried several variations to apply model transform to node and:
Neither model.sceneGraph axisCorrectionMatrix or computedModelMatrix appear to change from GLTF1.0 to GLTF2.0 or when modifying the up and forward axis while loading the models through fromGltf. (you can verify this in the updated sandcastle below). Is this a bug or did I miss something?
However, what seems to be a workable solution is to multiply nodes’ originalMatrix with their ._runtimeNode.transformToRoot. This will return a consistent position for both my re-oriented GLTF1.0 and GLTF2.0 models. (also verifiable in the example below)
How future proof is this private API? Can I safely rely on it or is there a better way to implement this?
In any case, I believe this can (should) be made more user friendly as I am sure I won’t be the only one bumping into this problem.
@tkazik That’s an interesting point. In the case of CesiumJS, we do have certain conventions for geospatial coordinate systems in different contexts (Cartesian ECEF, Cartographic (WGS84 lat/lon). However, documenting this better may be helpful. Some of these conventions are documented in the 3D Tiles specification, though I don’t think we have this documented for CesiumJS as a whole. Would you find it useful to have a page about coordinate systems somewhere in the cesium/Documentation at main · CesiumGS/cesium · GitHub folder of the repo? Or would somewhere else be easier to find?
@Xavier_Tassin in regards to that matrix, that part of the code is not likely to change anytime soon. However, it would be better if such a matrix (or some matrix derived from it) was exposed as a property of the public ModelNode. I can open an issue for this, but I’d like to know what interface would be more user-friendly first. Would you find any of these options useful?
Expose the separate matrices and let the user determine if/when to combine them. e.g. getNode().transformToRoot * getNode().originalMatrix
Make a different matrix that combines these two, for example getNode().computedModelMatrix == transformToRoot * originalMatrix
Would you have an idea for a different interface for getting the matrix that you’d find more user friendly?
Thank you, @ptrgags, for the continued discussion.
I do not have any real preference for the interface as long as it is public, documented and future proof.
A single property seems to be intuitive and readable, so I would go for that. I would perhaps call it computedNodeMatrix instead of computedModelMatrix so not to be mistaken with the root model matrix.
Which brings me back to how computedModelMatrix is created. Is there any explanation why it remains the same despite the various up and forward configurations I tried?