From 9a443bf6c8304362b5b4c4a02e4124b98545f429 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 19 Apr 2024 17:56:45 +0200 Subject: [PATCH] Try to upgrade CMPT to GLB when target version is 1.1 --- src/tools/contentProcessing/GltfTransform.ts | 58 +++++++++++++ src/tools/migration/TileFormatsMigration.ts | 21 +++++ .../migration/TileFormatsMigrationCmpt.ts | 83 +++++++++++++++++++ .../tilesetProcessing/TilesetUpgrader.ts | 61 +++++++++++++- .../upgrade/TilesetUpgradeOptions.ts | 3 + 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/tools/migration/TileFormatsMigrationCmpt.ts diff --git a/src/tools/contentProcessing/GltfTransform.ts b/src/tools/contentProcessing/GltfTransform.ts index 1487df3a..1dbc2e44 100644 --- a/src/tools/contentProcessing/GltfTransform.ts +++ b/src/tools/contentProcessing/GltfTransform.ts @@ -3,9 +3,14 @@ import draco3d from "draco3d"; import { MeshoptDecoder } from "meshoptimizer"; import { MeshoptEncoder } from "meshoptimizer"; +import { Document } from "@gltf-transform/core"; +import { Logger } from "@gltf-transform/core"; import { Transform } from "@gltf-transform/core"; import { NodeIO } from "@gltf-transform/core"; +import { prune } from "@gltf-transform/functions"; +import { unpartition } from "@gltf-transform/functions"; + import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { EXTStructuralMetadata } from "../../gltf-extensions"; @@ -79,4 +84,57 @@ export class GltfTransform { const outputGlb = await io.writeBinary(document); return Buffer.from(outputGlb); } + + /** + * Combine all scenes in the given document into one. + * + * This will take the first scene, declare it as the default scene, + * attach the nodes from all other scenes to this one, and dispose + * the other scenes. + * + * @param document - The glTF-Transform document + */ + private static async 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(); + } + } + document.setLogger(new Logger(Logger.Verbosity.WARN)); + await document.transform(prune()); + } + + /** + * Creates a single glTF Transform document from the given GLB buffers. + * + * This will create a document with a single scene that contains all + * nodes that have been children of any scene in the given input + * GLBs. + * + * @param inputGlbBuffers - The buffers containing GLB data + * @returns The merged document + */ + static async merge(inputGlbBuffers: Buffer[]): Promise { + // Create one document from each buffer and merge them + const io = await GltfTransform.getIO(); + const mergedDocument = new Document(); + for (const inputGlbBuffer of inputGlbBuffers) { + const inputDocument = await io.readBinary(inputGlbBuffer); + mergedDocument.merge(inputDocument); + } + // Combine all scenes into one + await GltfTransform.combineScenes(mergedDocument); + await mergedDocument.transform(unpartition()); + return mergedDocument; + } } diff --git a/src/tools/migration/TileFormatsMigration.ts b/src/tools/migration/TileFormatsMigration.ts index 27664870..58080139 100644 --- a/src/tools/migration/TileFormatsMigration.ts +++ b/src/tools/migration/TileFormatsMigration.ts @@ -3,6 +3,7 @@ import { Document } from "@gltf-transform/core"; import { TileFormatsMigrationPnts } from "./TileFormatsMigrationPnts"; import { TileFormatsMigrationB3dm } from "./TileFormatsMigrationB3dm"; import { TileFormatsMigrationI3dm } from "./TileFormatsMigrationI3dm"; +import { TileFormatsMigrationCmpt } from "./TileFormatsMigrationCmpt"; /** * Methods for converting "legacy" tile formats into glTF assets @@ -57,6 +58,26 @@ export class TileFormatsMigration { ); } + /** + * Convert the given CMPT data into a single glTF asset + * + * @param cmptBuffer - The CMPT buffer + * @param externalGlbResolver - A function that will be used to resolve + * external GLB data if the CMPT contains I3DM that use + * `header.gltfFormat=0` (meaning that the payload is not GLB data, + * but only a GLB URI). + * @returns The GLB buffer + */ + static async convertCmptToGlb( + cmptBuffer: Buffer, + externalGlbResolver: (uri: string) => Promise + ): Promise { + return await TileFormatsMigrationCmpt.convertCmptToGlb( + cmptBuffer, + externalGlbResolver + ); + } + /** * Apply the given RTC_CENTER to the given glTF-Transform document, * by inserting a new root node that carries the given RTC_CENTER diff --git a/src/tools/migration/TileFormatsMigrationCmpt.ts b/src/tools/migration/TileFormatsMigrationCmpt.ts new file mode 100644 index 00000000..2b6fc726 --- /dev/null +++ b/src/tools/migration/TileFormatsMigrationCmpt.ts @@ -0,0 +1,83 @@ +import { ContentDataTypeRegistry } from "../../base"; +import { ContentDataTypes } from "../../base"; + +import { TileFormats } from "../../tilesets"; + +import { GltfTransform } from "../contentProcessing/GltfTransform"; + +import { TileFormatsMigration } from "./TileFormatsMigration"; + +import { Loggers } from "../../base"; +const logger = Loggers.get("migration"); + +/** + * Methods for converting CMPT tile data into GLB + * + * @internal + */ +export class TileFormatsMigrationCmpt { + /** + * Convert the given CMPT data into a glTF asset + * + * @param cmptBuffer - The CMPT buffer + * @param externalGlbResolver - A function that will be used to resolve + * external GLB data if the CMPT contains I3DM that use + * `header.gltfFormat=0` (meaning that the payload is not GLB data, + * but only a GLB URI). + * @returns The GLB buffer + * @throws TileFormatError If the CMPT contained I3DM with an external + * GLB URI * that could not resolved by the given resolver + */ + static async convertCmptToGlb( + cmptBuffer: Buffer, + externalGlbResolver: (uri: string) => Promise + ): Promise { + const compositeTileData = TileFormats.readCompositeTileData(cmptBuffer); + const innerTileBuffers = compositeTileData.innerTileBuffers; + + const innerTileGlbs: Buffer[] = []; + for (const innerTileBuffer of innerTileBuffers) { + const innerTileType = await ContentDataTypeRegistry.findType( + "", + innerTileBuffer + ); + if (innerTileType === ContentDataTypes.CONTENT_TYPE_PNTS) { + logger.trace("Converting inner PNTS tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertPntsToGlb( + innerTileBuffer + ); + innerTileGlbs.push(innerTileGlb); + } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_B3DM) { + logger.trace("Converting inner B3DM tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertB3dmToGlb( + innerTileBuffer + ); + innerTileGlbs.push(innerTileGlb); + } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_I3DM) { + logger.trace("Converting inner I3DM tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertI3dmToGlb( + innerTileBuffer, + externalGlbResolver + ); + innerTileGlbs.push(innerTileGlb); + } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_CMPT) { + logger.trace("Converting inner CMPT tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertCmptToGlb( + innerTileBuffer, + externalGlbResolver + ); + innerTileGlbs.push(innerTileGlb); + } else { + logger.warn( + "Unknown type for inner tile of CMPT: " + + innerTileType + + " - ignoring" + ); + } + } + const document = await GltfTransform.merge(innerTileGlbs); + const io = await GltfTransform.getIO(); + const glb = await io.writeBinary(document); + return Buffer.from(glb); + } +} diff --git a/src/tools/tilesetProcessing/TilesetUpgrader.ts b/src/tools/tilesetProcessing/TilesetUpgrader.ts index 2d435aa3..ffe564eb 100644 --- a/src/tools/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tools/tilesetProcessing/TilesetUpgrader.ts @@ -83,6 +83,7 @@ export class TilesetUpgrader { upgradePntsToGlb: false, upgradeB3dmToGlb: false, upgradeI3dmToGlb: false, + upgradeCmptToGlb: false, }; return options; } @@ -103,6 +104,7 @@ export class TilesetUpgrader { upgradePntsToGlb: true, upgradeB3dmToGlb: true, upgradeI3dmToGlb: true, + upgradeCmptToGlb: true, }; return options; } @@ -216,6 +218,11 @@ export class TilesetUpgrader { return Paths.replaceExtension(uri, ".glb"); } } + if (this.upgradeOptions.upgradeCmptToGlb) { + if (Paths.hasExtension(uri, ".cmpt")) { + return Paths.replaceExtension(uri, ".glb"); + } + } return uri; }; @@ -263,11 +270,12 @@ export class TilesetUpgrader { ): Promise => { if (type === ContentDataTypes.CONTENT_TYPE_PNTS) { return this.processEntryPnts(sourceEntry); - } - if (type === ContentDataTypes.CONTENT_TYPE_B3DM) { + } else if (type === ContentDataTypes.CONTENT_TYPE_B3DM) { return this.processEntryB3dm(sourceEntry); } else if (type === ContentDataTypes.CONTENT_TYPE_I3DM) { return this.processEntryI3dm(sourceEntry); + } else if (type === ContentDataTypes.CONTENT_TYPE_CMPT) { + return this.processEntryCmpt(sourceEntry); } else if (type == ContentDataTypes.CONTENT_TYPE_TILESET) { return this.processEntryTileset(sourceEntry); } @@ -394,6 +402,55 @@ export class TilesetUpgrader { return targetEntry; }; + /** + * Process the given tileset (content) entry that contains CMPT, + * and return the result. + * + * @param sourceEntry - The source entry + * @returns The processed entry + */ + private processEntryCmpt = async ( + sourceEntry: TilesetEntry + ): Promise => { + const sourceKey = sourceEntry.key; + const sourceValue = sourceEntry.value; + let targetKey = sourceKey; + let targetValue = sourceValue; + if (this.upgradeOptions.upgradeCmptToGlb) { + logger.debug(` Upgrading CMPT to GLB for ${sourceKey}`); + + targetKey = this.processContentUri(sourceKey); + + // Define the resolver for external GLB files in CMPT files: + // It will look up the entry using the 'tilesetProcessor' + const externalGlbResolver = async ( + uri: string + ): Promise => { + if (!this.tilesetProcessor) { + return undefined; + } + const externalGlbEntry = await this.tilesetProcessor.fetchSourceEntry( + uri + ); + if (!externalGlbEntry) { + return undefined; + } + return externalGlbEntry.value; + }; + targetValue = await TileFormatsMigration.convertCmptToGlb( + sourceValue, + externalGlbResolver + ); + } else { + logger.debug(` Not upgrading ${sourceKey} (disabled via option)`); + } + const targetEntry = { + key: targetKey, + value: targetValue, + }; + return targetEntry; + }; + /** * Process the given tileset (content) entry that contains the * JSON of an external tileset, and return the result. diff --git a/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts b/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts index 48a09632..4bad5315 100644 --- a/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts +++ b/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts @@ -47,4 +47,7 @@ export type TilesetUpgradeOptions = { // Whether attempts should be made to convert I3DM files to GLB // with metadata upgradeI3dmToGlb: boolean; + + // Whether attempts should be made to convert CMPT files to GLB + upgradeCmptToGlb: boolean; };