Hi, with 3D Tiles 1.0 we created composite tiles (CMPT) of a set of instanced tiles (I3DM) - for example per tree species 1 I3DM is used (so each species has a different GLB model).
Now when using 3D Tiles 1.1 (creating GLB’s) it’s no longer possible to create a composite tile containing GLB’s. Workaround is to create a GLB per species but it gives too many layers. Is there an alternative option?
@bertt Hi, Bert! Can you, please, share a chunk of your tileset.json to see how you build multiple instances of trees? I’m also trying to implement the same task and it would be really helpful to see the real world example. Thanks in advance!
Hi, for a sample of I3dm’s/cmpt see https://bertt.github.io/trees/lyon/1.0/
Looks cool! Thank you, Bert!
I’ll probably have to take a closer look at the data that you linked to, to better understand the goal and the issue (i.e. what exactly is contained in each CMPT, and what is contained in each I3DM).
The “Migration guide” in the spec gives some guidance about how the “legacy” tile formats can be emulated with glTF. But the devil is in the detail, and it can be difficult to decide on the ‘right’ way of structuring the data. Parts of a solution could be:
- There is the
cmptToGlb
command of the 3d-tiles tools. This command “recursively” walks through the CMPT (which may contain other CMPTs, and B3DMs as ‘leaf nodes’). It will write out all GLB files that it finds on this way. One aspect that might be “limiting” here: It does not only dissect the “top-level” CMPT itself. So when there’s a CMPT that contains an I3DM and a B3DM, then there is no existing function to extract this I3DM and B3DM (but only the GLBs that they contain). It would be easy to build such a function within the tools, if necessary. - There’s the
convertI3dmToGlb
command that tries to convert an I3DM into a GLB that use theEXT_mesh_gpu_instancing
(+ metadata extensions for batch tables, if necessary). It tries to do this conversion, but there are corner cases where this is not practically possible.
From the description, it sounds like it could be possible to…
- create a GLB for a single tree type/species
- augment that with
EXT_mesh_gpu_instancing
to create all desired instances of this tree type - if necessary: combine multiple such GLBs (with multiple incarnations of the
EXT_mesh_gpu_instancing
extension, one for each tree type mesh) to have one small GLB with many different tree types
But I’m not sure whether this was exactly the goal, or something that prevents people from using this approach.
the data I have is like:
-
1 I3dm with deciduous trees (deciduous tree glb model + positions/rotations/scales)
-
1 I3dm with coniferous trees (coniferous tree glb model + positions/rotations/scales)
This is for 2 species, but there could be many species (like hundreds).
In 1.0 I could combine them in a CMPT so in the client there is only one layer for trees.
So in 1.1 is there a way to combine ‘multiple such GLBs (with multiple incarnations of the EXT_mesh_gpu_instancing
extension, one for each tree type mesh) to have one small GLB with many different tree types’? Is there a tool or something?
Converting one I3DM to GLB (with the EXT_mesh_gpu_instancing
extension should be possible with the convertI3dmToGlb
command from the 3D Tiles tool. As I said, there are corner cases where this conversion is not always possible, but the case that you described sounds straightforward and should work out-of-the-box. (If not, any issues can be discussed here or in an issue in the repo).
Combining multiple GLBs, with the goal of “emulating” the structure of a CMPT that contained multiple I3DMs, is a slightly different story. There is no fully automated tool that can universally “merge” arbitrary GLBs (at least not without some user interaction to say what the merged result should be).
However, you might want to try out the glTF-Transform CLI ‘merge’ command. The input GLBs in this case are be ‘structurally simple’: They contain just a few nodes+mehes (no animations, scenes, cameras…). So iff glTF-Transform is just merging all nodes+meshes, then the result could already be what you’re looking for.
(I have not really used/examined that ‘merge’ CLI command. There might be issues when the glTF contains extensions that are not supported by glTF-Transform directly. But if it does not produce the desired result, then it would probably be easy to write a “custom merge operation”, with a few lines of code - we can explore some options here if necessary)
ok thinking about it, I doubt it will work because ‘gltf-transform merge’ will create a ‘merged’ model (of several tree species), but there is no way to display them separate again? Maybe there is an option to use glTF scenes? (although I don’t know how Cesium handles multiple scenes).
but there is no way to display them separate again?
At which point (and how) should this happen? Do you want to toggle something like “Shaw all deciduous trees” at runtime? Or is this only about having a single file with all deciduous trees (that could be shown in a standalone viewer or so)?
In both cases, I woudn’t immediately have an idea about how to accomplish that easily. (How was that done exactly with the CMPT/I3DM approach?)
Maybe others could have good ideas or approaches here…
Hi, I’ve created a minimal sample with 1.0 (I3dm/cmpt): there is 1 cmpt file, it contains 3 I3dms (each I3dm has a unique GLB model with 1 instance), in the Cesium client there is only 1 layer (like a ‘trees’ layer).
https://bertt.github.io/cesium_issues/composite_gpu_instances/
But I think the conclusion is that something like this it not possible in 1.1 with EXT_mesh_gpu_instancing GLB’s ?
note: something strange is happening to the tree, it appears and disappears on map navigation… any idea? must investigate it more.
note: something strange is happening to the tree, it appears and disappears on map navigation… any idea?
I also saw this, and purely from the observed behavior, it looks like a bug, but indeed might have to be investigated further.
Regarding the actual question:
I gave this a try, and created GLBs from the I3DMs. Combining them with glTF-Transform merge
does not appear to create the expected GLB. (For me, it only shows the tree).
I’ll have to read more about the current, actual implementation of merge
in glTF-Transform, but think that it might be possible to implement some “manual, custom ‘merge’ operation” - in doubt, by drilling down to the mesh primitives and textures and plainly “copying” the data into a target document.
But before investing time for that:
Iff it was possible to create such a single GLB that contains the three geometries and their respective instancing information: Would that be what you’re trying to accomplish?
For the disappearing tree I’ve created another issue see CMPT with I3DM's: features disappearing on map navigation
yes the question is: is its possible to create 1 GLB containing 3 glb models with their EXT_mesh_gpu_instancing info (like a composite GLB)?
It might be possible. But if it’s possible, it’s not trivial, and some necessary steps should be easier than they are now.
There is currently no tool for extracting the I3DMs from that CMPT. Adding this should not be much effort, and I just opened Offer function to just extract elements of a composite tile · Issue #78 · CesiumGS/3d-tiles-tools · GitHub for that.
Converting a single I3DM to a GLB is possible with convertI3dmToGlb
.
Merging the resulting GLBs is tricky. The glTF-Transform merge
function could do the bulk of the work here, with caveats:
- It will, by default, create a single GLB with multiple
scenes
(one scene for each input). There is no “right” or “wrong” or “good” or “bad” about that. It’s just the way it is (even though it’s not what we want here). Post-processing the GLB to merge all scenes into one is relatively easy. - The command-line version of glTF-Transform will not know the
EXT_mesh_features
orEXT_structural_metadata
orEXT_instance_features
extensions. This could be worked around by using the API of glTF-Transform and theNodeIO
object that is returned by theGltfTransform.getIO()
function of the3d-tiles-tools
, which comes preconfigured with all required extensions - The built-in
document.merge
function of glTF-Transform is not aware of the metadata extensions - so the result may contain ~“invalid metadata definitions”, roughly speaking
The last one is the most difficult to solve.
The goal of “merging metadata definitions” raises some tricky questions. Of course, one can shove all property tables into one array and call it a day. But merging the schema is not necessarily straightforward. There may be multiple classes that the property tables refer to, and a “good” merge operation might have the goal to identify whether two classes are equal - but even then, one could make the point that they are, in fact, different classes from different schemas (with different schema.id
values), and should be kept separate.
Even iff it is possible to merge the metadata: Transporting the information about the metadata extensions through the built-in glTF-Transform merge
process might be impossible. For example:
- there may be one GLB with a node that refers to
propertyTable:0
- there may be another GLB with a node that refers to
propertyTable:0
in this other GLB - merging the property tables could be done, and result in an array of two property tables
- the glTF-Transform
merge
function will do “anything” with the nodes, but it cannot update the property table that the node refers to - i.e. the second node would have to refer topropertyTable:1
after the merge…
I don’t see an easy solution for that right now.
One could try to “emulate” what the glTF-Transform merge
function is currently doing, and inserting that special treatment for the extension in the right place. But the merge
function, as it is currently implemented, uses a private API of glTF-Transform (see the current implementation of merge
). Maybe it’s possible to emulate that with the public API, but it’s hard to estimate the time/effort (and possible drawbacks or limitiations) for that.
Another approach could be to basically read the data of all I3DMs, and write them into a single target glTF directly, without the detour of glTF-Transform merge
. But that may require some restructuring, and I also cannot make promises about a timeline for that right now.
To summarize the state that I’m currently at:
I created a ProcessForum27475.ts
that contains drafts for the required functionalities. I’ll dump it here, for the case that someone wants to have a look. Some of the functions here will likely become part of the tools, e.g. for Offer function to just extract elements of a composite tile · Issue #78 · CesiumGS/3d-tiles-tools · GitHub .
This script currently does generate a single 0_0_0.glb
for the original 0_0_0.cmpt
. But it does not generate sensible metadata: For now, I inserted a hacky removeInstanceFeatures
method that brutally removes all EXT_instance_features
definitions from the nodes. The resulting GLB can be shown in common viewers and in CesiumJS, but the metadata will be lost. I’ll try to find a solution for that, but cannot make promises about that right now…
import path from "path";
import fs from "fs";
import { Document } from "@gltf-transform/core";
import { unpartition } from "@gltf-transform/functions";
import { prune } from "@gltf-transform/functions";
import { TileFormats } from "./src/tileFormats/TileFormats";
import { ContentDataTypeRegistry } from "./src/contentTypes/ContentDataTypeRegistry";
import { ContentDataTypes } from "./src/contentTypes/ContentDataTypes";
import { TileFormatsMigration } from "./src/migration/TileFormatsMigration";
import { GltfTransform } from "./src/contentProcessing/GltfTransform";
function splitCmptInternal(
tileDataBuffer: Buffer,
resultBuffers: Buffer[],
recursive: boolean
) {
const isComposite = TileFormats.isComposite(tileDataBuffer);
if (!isComposite) {
resultBuffers.push(tileDataBuffer);
} else {
const compositeTileData = TileFormats.readCompositeTileData(tileDataBuffer);
for (const innerTileDataBuffer of compositeTileData.innerTileBuffers) {
if (recursive) {
splitCmptInternal(innerTileDataBuffer, resultBuffers, recursive);
} else {
resultBuffers.push(innerTileDataBuffer);
}
}
}
}
function splitCmpt(tileDataBuffer: Buffer, recursive: boolean) {
const resultBuffers: Buffer[] = [];
splitCmptInternal(tileDataBuffer, resultBuffers, recursive);
return resultBuffers;
}
async function convertI3dmToGlb(directoryName: string, i3dmBuffer: Buffer) {
// Prepare the resolver for external GLBs in I3DM
const externalGlbResolver = async (
uri: string
): Promise<Buffer | undefined> => {
const externalGlbUri = path.resolve(directoryName, uri);
return fs.readFileSync(externalGlbUri);
};
const outputBuffer = await TileFormatsMigration.convertI3dmToGlb(
i3dmBuffer,
externalGlbResolver
);
return outputBuffer;
}
async function combineScenes(document: Document) {
const root = document.getRoot();
const scenes = root.listScenes();
if (scenes.length > 0) {
const combinedScene = scenes[0];
root.setDefaultScene(combinedScene);
for (let s = 1; s < scenes.length; s++) {
const otherScene = scenes[s];
const children = otherScene.listChildren();
for (const child of children) {
combinedScene.addChild(child);
otherScene.removeChild(child);
}
otherScene.dispose();
}
}
await document.transform(prune());
}
function removeInstanceFeatures(document: Document) {
const root = document.getRoot();
const nodes = root.listNodes();
for (const node of nodes) {
node.setExtension("EXT_instance_features", null);
}
}
async function customMerge(inputBuffers: Buffer[]): Promise<Document> {
// Create one document from each buffer and merge them
const io = await GltfTransform.getIO();
const mergedDocument = new Document();
for (const inputBuffer of inputBuffers) {
const inputDocument = await io.readBinary(inputBuffer);
mergedDocument.merge(inputDocument);
}
removeInstanceFeatures(mergedDocument);
// Combine all scenes into one
await combineScenes(mergedDocument);
return mergedDocument;
}
async function run(inputFileName: string, outputFileName: string) {
// Extract the buffers of the CMPT
const inputBuffer = fs.readFileSync(inputFileName);
const recursive = false;
const buffersFromCmpt = splitCmpt(inputBuffer, recursive);
// Collect all I3DM buffers
const i3dmBuffers: Buffer[] = [];
for (let i = 0; i < buffersFromCmpt.length; i++) {
const bufferFromCmpt = buffersFromCmpt[i];
const type = await ContentDataTypeRegistry.findType("", bufferFromCmpt);
if (type !== ContentDataTypes.CONTENT_TYPE_I3DM) {
console.log("Ignoring CMPT element with type " + type);
} else {
i3dmBuffers.push(bufferFromCmpt);
}
}
// Convert all I3DM buffers into GLB
const glbBuffers: Buffer[] = [];
const directoryName = path.dirname(inputFileName);
for (const i3dmBuffer of i3dmBuffers) {
const glbBuffer = await convertI3dmToGlb(directoryName, i3dmBuffer);
glbBuffers.push(glbBuffer);
}
if (glbBuffers.length === 0) {
console.log("No GLB buffers found");
return;
}
// Create a merged document and write the result
const resultDocument = await customMerge(glbBuffers);
await resultDocument.transform(unpartition());
console.log("Writing " + outputFileName);
const io = await GltfTransform.getIO();
const outputGlb = await io.writeBinary(resultDocument);
fs.writeFileSync(outputFileName, outputGlb);
}
run(
"C:/original/content/0_0_0.cmpt",
"C:/modified/content/0_0_0.glb"
);