Hi, Is there a tool/documentation for how to get from i3dm tiles to EXT_mesh_gpu_instancing glTF’s?
Maybe there is a CesiumJS sample with EXT_mesh_gpu_instancing ?
There is a short section in the migration guide (for the upcoming finalized 1.1 spec), but as far as I know, there is no tool for the conversion yet.
I just added a DRAFT PR at Add preliminary GPU instance metadata example by javagl · Pull Request #53 · CesiumGS/3d-tiles-samples · GitHub with a sample that uses EXT_structural_metadata
together with EXT_mesh_gpu_instancing
. But the API for accessing the required information at runtime is not yet finalized. The sandcastle in the sample accesses a private API, and should therefore be considered only as a basic preview.
Also, manually assigning IDs to the instances could/should be done with EXT_instance_features
. The current example just uses the instance ID to access the respective row of the property table.
The example therefore is only a very early draft state, but may be helpful for first experiments.
ok thanks, in the meantime I’ve got something basically working https://bertt.github.io/cesium_3dtiles_samples/samples/1.1/forest/
Does this model contain any metadata? I’m asking because this is the part where it becomes “difficult”, insofar that additonal extensions are required in order to model the capabilities of the Batch ID/Table of the I3DM.
There may be cases of “plain and simple” I3DM models. Specifically, the ones that only use position, normals (rotation/orientation), and scale. For these, it might be possible (with reasonable effort) to create some sort of converter from I3DM to glTF+EXT_mesh_gpu_instancing: The properties can largely be translated into the appropriate accessors - pretty much generically (even though one has to be careful to get the orientations right).
Trying to create a tool that can convert arbitrary I3DMs to glTF, including BatchTables or quantized positions/normals, could be more challenging. And this might not be worth the effort medium-term: I3DM is still supported, and the time might better be spent with making sure that the source pipeline (i.e. the tiler) outputs glTF instead of I3DM in the first place.
I’ve created this instanced glTF with some custom code (input: tree.glb and a list of positions/scales), plan is to add EXT_mesh_features/EXT_structural_metadata for the metadata. So skipping i3dm completely indeed.
now struggling to get the instanced trees in the glTF on the correct position in CesiumJS. It works more like b3dm then i3dm is my impression.
I have to admit that I’d probably have to go though a few scenarios (on pen-and-paper level) to wrap my head around the coordinate system, the glTF transforms, and the transformation order and the exact differences between them.
So on a high level: When you say “It works more like b3dm…”, does this refer to confusion of the transformation order? If you find something “suspicious” - i.e. something that is unexpected or apparently inconsistent with the spec - then feel free to drop a note here or in an issue.
Got a bit further, models are now on the correct position. Demo uses implicit tiling + 1 instanced tile.
https://bertt.github.io/cesium_3dtiles_samples/samples/1.1/trees/
But now there is a strange rotation going on
I’ve got basically a table with tree point locations (in epsg:4326) and a glTF model (tree.glb), getting it to work correctly in CesiumJS is quite puzzling…
I think I’ll have to add ENU vector in the instance rotation, will experiment next week.
Puzzling indeed.
I was curious and tried this out. The ENU is probably the main point, but when trying to apply the ENU, the different coordinate system conventions (y-up in glTF vs. z-up in 3D Tiles) have to be taken into account. I came up with something that worked for your example, but … I’ll frankly admit that this eventually was the result of some trial-and-error, after what I thought should be the right solution turned out to be wrong
But even though the code looks obscure, I’ll post it here: For a given cartesian position (created from the position of Amsterdam in this example), it computes the quaternion that has to be written into the glTF ROTATION
of the EXT_mesh_gpu_instancing
.
function computeQuaternion(cartesian) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(
cartesian, Cesium.Ellipsoid.WGS84);
let mat4 = Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
mat4 = Cesium.Matrix4.multiply(mat4, Cesium.Axis.Z_UP_TO_Y_UP, mat4);
mat4 = Cesium.Matrix4.multiply(mat4, enu, mat4);
mat4 = Cesium.Matrix4.multiply(mat4, Cesium.Axis.Y_UP_TO_Z_UP, mat4); // ???
mat4 = Cesium.Matrix4.multiply(mat4, Cesium.Axis.Y_UP_TO_Z_UP, mat4); // ???
const mat3 = Cesium.Matrix4.getMatrix3(mat4, new Cesium.Matrix3());
const quaternion = Cesium.Quaternion.fromRotationMatrix(mat3);
return quaternion;
}
const cartesian = Cesium.Cartesian3.fromDegrees(4.8988027, 52.3700643);
const quaternion = computeQuaternion(cartesian);
console.log('Quaternion for glTF: ' + quaternion);
Now, I could probably explain the Z_UP_TO_Y_UP
as being the inverse of the y-up-to-z-up transform that CesiumJS is doing at some point, but … could not explain the rotation about 180° around the x-axis there (done via the Y_UP_TO_Z_UP
rotations). Eventually, these rotations are just swizzling the matrix rows and changing the signs, and I guess one could find an explanation for that (and based on that, a more ‘geometrically sensible’ implementation), but … that’s where I’d need a bit more time and pen+paper.
Fow now, I was just happy to see the result …
Here’s the corresponding GLB:
0_0_0.glb (2.2 MB)
thanks looks like these formulas could be simplified indeed.
I’ve added random scales/rotations in the meantime:
https://bertt.github.io/cesium_3dtiles_samples/samples/1.1/trees_fixed/
There is one thought that is still bothering me a bit: I’m not entirely sure whether trying to write these orientations into the GLB itself is the best thing to do here.
When looking at the GLB in isolation (e.g. with https://sandbox.babylonjs.com/ , which supports instancing), then you see that this GLB is oriented as-if it was attached to the globe, at the desired position. So you could not use the same GLB on a different place on earth, without doing a further transform. This transform would have to take into account the ENU of its current position (inverted), and the ENU of the target position. (Even more room for confusion here…)
It might be better or more verstatile to use a GLB where all trees are upright (aligned with the up/y-axis), and then use the tile transform to put it at the right position on earth. This tile transform could then probably just be the ENU, as it is. (That’s a gut feeling, but I think it should be…)
To illustrate that (and apologies for the use of Paint here) :
How easily this could be accomplished may also depend on the source data. For example, if the GLB is covering a large area, then the positions and rotations of the EXT_mesh_gpu_instancing
object would basically reflect the curvature of earth. But it could still be placed at any point of the globe.
Maybe I’ll try that out with the original GLB at some point. For now, I just wanted to mention that this might be easier and more versatile in the long run…
I did some very basic experiment, referring to the last comment.
I did not have the “original”, “2D” positions of the trees. So as a workaround for the basic test, I just planted 50000 trees. These are using the detault “y-up” orientation, and a “dummy” translation.
In order to properly place them on the surface at the desired position, the tileset.transform
can just be set to be the ENU matrix, as computed with
const cartesian = Cesium.Cartesian3.fromDegrees(4.8988027, 52.3700643);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(
cartesian, Cesium.Ellipsoid.WGS84);
console.log("Tileset root node matrix:" + Cesium.Matrix4.toArray(enu));
for a given position on the globe.
When viewing this in Cesium, the result should be trees that are properly aligned with the up-direction of the respective position on the globe:
The advantages might be:
- The GLB file is one with a “default” orientation, and can be rendered in each viewer that supports instancing
- The trees might not need a
ROTATION
for the instances any more (unless they are really supposed to have individual rotations, rotations that depend on the terrain, or something like the “curvature of earth”). The file size may be reduced considerably (2.2 MB to 680KB, although I also omitted theSCALE
in this test) - The GLB can more easily be displayed at any position of the globe, by changing the
tileset.transform
to be the ENU for that position.
One could also just omit the transform, and place it programmatically at runtime, by setting the model matrix to be the ENU:
const viewer = new Cesium.Viewer("cesiumContainer");
const tileset = new Cesium.Cesium3DTileset({
url: "http://localhost:8003/CesiumTreeInstances/tileset_traffic_signs.json"
});
const cartesian = Cesium.Cartesian3.fromDegrees(4.8988027, 52.3700643);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(
cartesian, Cesium.Ellipsoid.WGS84);
tileset.modelMatrix = enu;
viewer.scene.primitives.add(tileset);
viewer.zoomTo(tileset);
Whether or not all this is applicable in your case, or relevant for you in any other way … I don’t know. But I wanted to try it out.
Here’s the tileset.json
and the GLB that I used for the experiment. (The bounding box in the JSON is utterly wrong, that’s just the one from the original model…):
{
"asset": {
"version": "1.1"
},
"geometricError": 1024.0,
"root": {
"boundingVolume": {
"box": [
0.0,
0.0,
0.0,
6813.594,
0.0,
0.0,
0.0,
4585.989,
0.0,
0.0,
0.0,
6077.698
]
},
"geometricError": 512.0,
"refine": "ADD",
"content": {
"uri": "content/{level}_{x}_{y}.glb"
},
"implicitTiling": {
"subdivisionScheme": "QUADTREE",
"subtreeLevels": 1,
"subtrees": {
"uri": "subtrees/{level}_{x}_{y}.subtree"
}
},
"transform": [
-0.08539610266329878,0.9963470809160427,0,0,-0.7890777436401776,-0.06763121536248962,0.6105590333459319,0,0.60832871060114,0.05213936189361366,0.7919707487020472,0,3888175.3441949254,333252.36478888814,5028049.702918064,1
]
}
}
0_0_0.glb (678.4 KB)