From f82fc25f3bf99be0c36d150b096958111102c05d Mon Sep 17 00:00:00 2001 From: Viktor Belomestnov Date: Tue, 27 Jun 2023 17:04:56 +0200 Subject: [PATCH] chore(tile-converter): preprocess stage for I3SConverter (#2520) --- modules/3d-tiles/src/index.ts | 1 + .../src/lib/parsers/parse-3d-tile-gltf.ts | 16 +- .../deprecated/EXT_feature_metadata.ts | 9 +- .../extensions/EXT_feature_metadata.spec.js | 2 +- modules/i3s/src/index.ts | 3 +- .../i3s-converter/helpers/load-3d-tiles.ts | 15 +- .../helpers/node-index-document.ts | 24 +- .../helpers/preprocess-3d-tiles.ts | 81 +++++ .../helpers/tileset-traversal.ts | 51 ++++ .../src/i3s-converter/i3s-converter.ts | 277 +++++++++--------- .../tile-converter/src/i3s-converter/types.ts | 29 +- 11 files changed, 335 insertions(+), 173 deletions(-) create mode 100644 modules/tile-converter/src/i3s-converter/helpers/preprocess-3d-tiles.ts create mode 100644 modules/tile-converter/src/i3s-converter/helpers/tileset-traversal.ts diff --git a/modules/3d-tiles/src/index.ts b/modules/3d-tiles/src/index.ts index 69234e1886..3131278b59 100644 --- a/modules/3d-tiles/src/index.ts +++ b/modules/3d-tiles/src/index.ts @@ -24,3 +24,4 @@ export type { Tiles3DTileContent, ImplicitTilingExensionData } from './types'; +export type {Tiles3DLoaderOptions} from './tiles-3d-loader'; diff --git a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-gltf.ts b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-gltf.ts index 6dd061cad9..3844c853fb 100644 --- a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-gltf.ts +++ b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-gltf.ts @@ -19,11 +19,15 @@ export async function parseGltf3DTile( ? options['3d-tiles'].assetGltfUpAxis : 'Y'; - if (!context) { - return; + if (options?.['3d-tiles']?.loadGLTF) { + if (!context) { + return; + } + const {parse} = context; + const gltfWithBuffers = await parse(arrayBuffer, GLTFLoader, options, context); + tile.gltf = postProcessGLTF(gltfWithBuffers); + tile.gpuMemoryUsageInBytes = _getMemoryUsageGLTF(tile.gltf); + } else { + tile.gltfArrayBuffer = arrayBuffer; } - const {parse} = context; - const gltfWithBuffers = await parse(arrayBuffer, GLTFLoader, options, context); - tile.gltf = postProcessGLTF(gltfWithBuffers); - tile.gpuMemoryUsageInBytes = _getMemoryUsageGLTF(tile.gltf); } diff --git a/modules/gltf/src/lib/extensions/deprecated/EXT_feature_metadata.ts b/modules/gltf/src/lib/extensions/deprecated/EXT_feature_metadata.ts index f28a4619c4..f7c09a5963 100644 --- a/modules/gltf/src/lib/extensions/deprecated/EXT_feature_metadata.ts +++ b/modules/gltf/src/lib/extensions/deprecated/EXT_feature_metadata.ts @@ -13,22 +13,23 @@ import { GLTFMeshPrimitive } from '../../types/gltf-json-schema'; import {getComponentTypeFromArray} from '../../gltf-utils/gltf-utils'; +import {GLTFLoaderOptions} from '../../../gltf-loader'; /** Extension name */ const EXT_FEATURE_METADATA = 'EXT_feature_metadata'; export const name = EXT_FEATURE_METADATA; -export async function decode(gltfData: {json: GLTF}): Promise { +export async function decode(gltfData: {json: GLTF}, options: GLTFLoaderOptions): Promise { const scenegraph = new GLTFScenegraph(gltfData); - decodeExtFeatureMetadata(scenegraph); + decodeExtFeatureMetadata(scenegraph, options); } /** * Decodes feature metadata from extension * @param scenegraph */ -function decodeExtFeatureMetadata(scenegraph: GLTFScenegraph): void { +function decodeExtFeatureMetadata(scenegraph: GLTFScenegraph, options: GLTFLoaderOptions): void { const extension: GLTF_EXT_feature_metadata | null = scenegraph.getExtension(EXT_FEATURE_METADATA); if (!extension) return; @@ -47,7 +48,7 @@ function decodeExtFeatureMetadata(scenegraph: GLTFScenegraph): void { } const {featureTextures} = extension; - if (schemaClasses && featureTextures) { + if (schemaClasses && featureTextures && options.gltf?.loadImages) { for (const schemaName in schemaClasses) { const schemaClass = schemaClasses[schemaName]; const featureTexture = findFeatureTextureByName(featureTextures, schemaName); diff --git a/modules/gltf/test/lib/extensions/EXT_feature_metadata.spec.js b/modules/gltf/test/lib/extensions/EXT_feature_metadata.spec.js index 7b49681034..0e78f0a0a0 100644 --- a/modules/gltf/test/lib/extensions/EXT_feature_metadata.spec.js +++ b/modules/gltf/test/lib/extensions/EXT_feature_metadata.spec.js @@ -225,7 +225,7 @@ test('gltf#EXT_feature_metadata - Should handle feature texture attributes', asy } }; - await decodeExtensions(GLTF_WITH_TEXTURES); + await decodeExtensions(GLTF_WITH_TEXTURES, {gltf: {loadImages: true}}); const expectedResult = { buf: { diff --git a/modules/i3s/src/index.ts b/modules/i3s/src/index.ts index 54cf3f32ab..a2ca68f1ae 100644 --- a/modules/i3s/src/index.ts +++ b/modules/i3s/src/index.ts @@ -30,7 +30,8 @@ export type { ValueCount, BuildingSceneSublayer, DATA_TYPE, - OperationalLayer + OperationalLayer, + TextureSetDefinitionFormats } from './types'; export {COORDINATE_SYSTEM} from './lib/parsers/constants'; diff --git a/modules/tile-converter/src/i3s-converter/helpers/load-3d-tiles.ts b/modules/tile-converter/src/i3s-converter/helpers/load-3d-tiles.ts index 96b785a221..b46191ef7d 100644 --- a/modules/tile-converter/src/i3s-converter/helpers/load-3d-tiles.ts +++ b/modules/tile-converter/src/i3s-converter/helpers/load-3d-tiles.ts @@ -1,22 +1,22 @@ import type { + Tiles3DLoaderOptions, Tiles3DTileContent, Tiles3DTileJSONPostprocessed, Tiles3DTilesetJSONPostprocessed } from '@loaders.gl/3d-tiles'; import {load} from '@loaders.gl/core'; -import {Tiles3DLoadOptions} from '../types'; /** * Load nested 3DTiles tileset. If the sourceTile is not nested tileset - do nothing * @param sourceTileset - source root tileset JSON * @param sourceTile - source tile JSON that is supposed to has link to nested tileset - * @param globalLoadOptions - load options for Tiles3DLoader + * @param tilesetLoadOptions - load options for Tiles3DLoader * @returns nothing */ export const loadNestedTileset = async ( sourceTileset: Tiles3DTilesetJSONPostprocessed | null, sourceTile: Tiles3DTileJSONPostprocessed, - globalLoadOptions: Tiles3DLoadOptions + tilesetLoadOptions: Tiles3DLoaderOptions ): Promise => { const isTileset = sourceTile.type === 'json'; if (!sourceTileset || !sourceTile.contentUrl || !isTileset) { @@ -24,7 +24,7 @@ export const loadNestedTileset = async ( } const loadOptions = { - ...globalLoadOptions, + ...tilesetLoadOptions, [sourceTileset.loader.id]: { isTileset, assetGltfUpAxis: (sourceTileset.asset && sourceTileset.asset.gltfUpAxis) || 'Y' @@ -41,13 +41,13 @@ export const loadNestedTileset = async ( * Load 3DTiles tile content, that includes glTF object * @param sourceTileset - source root tileset JSON * @param sourceTile - source tile JSON that has link to content data - * @param globalLoadOptions - load options for Tiles3DLoader + * @param tilesetLoadOptions - load options for Tiles3DLoader * @returns - 3DTiles tile content or null */ export const loadTile3DContent = async ( sourceTileset: Tiles3DTilesetJSONPostprocessed | null, sourceTile: Tiles3DTileJSONPostprocessed, - globalLoadOptions: Tiles3DLoadOptions + tilesetLoadOptions: Tiles3DLoaderOptions ): Promise => { const isTileset = sourceTile.type === 'json'; if (!sourceTileset || !sourceTile.contentUrl || isTileset) { @@ -55,8 +55,9 @@ export const loadTile3DContent = async ( } const loadOptions = { - ...globalLoadOptions, + ...tilesetLoadOptions, [sourceTileset.loader.id]: { + ...(tilesetLoadOptions[sourceTileset.loader.id] || {}), isTileset, assetGltfUpAxis: (sourceTileset.asset && sourceTileset.asset.gltfUpAxis) || 'Y' } diff --git a/modules/tile-converter/src/i3s-converter/helpers/node-index-document.ts b/modules/tile-converter/src/i3s-converter/helpers/node-index-document.ts index 612cb9119b..d5c069a9c0 100644 --- a/modules/tile-converter/src/i3s-converter/helpers/node-index-document.ts +++ b/modules/tile-converter/src/i3s-converter/helpers/node-index-document.ts @@ -31,6 +31,15 @@ export class NodeIndexDocument { /** converter instance */ private converter: I3SConverter; + /** + * Finalized property. It means that all child nodes are saved and their data + * is unloaded + */ + private _finalized: boolean = false; + get finalized(): boolean { + return this._finalized; + } + /** * Constructor * @param id - id of the node in node pages @@ -90,6 +99,9 @@ export class NodeIndexDocument { * Add neighbors to child nodes of this node */ public async addNeighbors(): Promise { + if (this.finalized) { + return; + } const nodeData = await this.load(); for (const childNode of this.children) { const childNodeData = await childNode.load(); @@ -116,9 +128,9 @@ export class NodeIndexDocument { await childNode.write(childNodeData); } await childNode.save(); - // The save after adding neighbors is the last one. Flush the the node - childNode.flush(); } + // The save after adding neighbors is the last one. Finalize the the node + this.finalize(); } /** Save 3DNodeIndexDocument in file on disk */ @@ -128,6 +140,14 @@ export class NodeIndexDocument { } } + /** Finalize the node */ + private finalize(): void { + this._finalized = true; + for (const child of this.children) { + child.flush(); + } + } + /** * Write 3DNodeIndexDocument https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DNodeIndexDocument.cmn.md * @param node - Node3DIndexDocument object diff --git a/modules/tile-converter/src/i3s-converter/helpers/preprocess-3d-tiles.ts b/modules/tile-converter/src/i3s-converter/helpers/preprocess-3d-tiles.ts new file mode 100644 index 0000000000..fa320e4cb7 --- /dev/null +++ b/modules/tile-converter/src/i3s-converter/helpers/preprocess-3d-tiles.ts @@ -0,0 +1,81 @@ +import {Tiles3DTileContent, Tiles3DTileJSONPostprocessed} from '@loaders.gl/3d-tiles'; +import {GltfPrimitiveModeString, PreprocessData} from '../types'; +import {GLTF, GLTFLoader} from '@loaders.gl/gltf'; +import {parse} from '@loaders.gl/core'; + +/** + * glTF primitive modes + * @see https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_mode + */ +export const GLTF_PRIMITIVE_MODES = [ + GltfPrimitiveModeString.POINTS, // 0 + GltfPrimitiveModeString.LINES, // 1 + GltfPrimitiveModeString.LINE_LOOP, // 2 + GltfPrimitiveModeString.LINE_STRIP, // 3 + GltfPrimitiveModeString.TRIANGLES, // 4 + GltfPrimitiveModeString.TRIANGLE_STRIP, // 5 + GltfPrimitiveModeString.TRIANGLE_FAN // 6 +]; + +/** + * Analyze tile content. This function is used during preprocess stage of + * conversion + * @param tile - 3DTiles tile JSON metadata + * @param tileContent - 3DTiles tile content ArrayBuffer + * @returns + */ +export const analyzeTileContent = async ( + tile: Tiles3DTileJSONPostprocessed, + tileContent: Tiles3DTileContent | null +): Promise => { + const result: PreprocessData = { + meshTopologyTypes: new Set() + }; + if (!tileContent?.gltfArrayBuffer) { + return result; + } + + const gltfData = await parse(tileContent.gltfArrayBuffer, GLTFLoader, { + gltf: {normalize: false, loadBuffers: false, loadImages: false, decompressMeshes: false} + }); + const gltf = gltfData.json; + + if (!gltf) { + return result; + } + const meshTypes = getMeshTypesFromGltf(gltf); + result.meshTopologyTypes = meshTypes; + return result; +}; + +/** + * Get mesh topology types that the glb content has + * @param gltfJson - JSON part of GLB content + * @returns array of mesh types found + */ +const getMeshTypesFromGltf = (gltfJson: GLTF): Set => { + const result: Set = new Set(); + for (const mesh of gltfJson.meshes || []) { + for (const primitive of mesh.primitives) { + let {mode} = primitive; + if (typeof mode !== 'number') { + mode = 4; // Default is 4 - TRIANGLES + } + result.add(GLTF_PRIMITIVE_MODES[mode]); + } + } + return result; +}; + +/** + * Merge object2 into object1 + * @param object1 + * @param object2 + * @returns nothing + */ +export const mergePreprocessData = (object1: PreprocessData, object2: PreprocessData): void => { + // Merge topology mesh types info + for (const type of object2.meshTopologyTypes) { + object1.meshTopologyTypes.add(type); + } +}; diff --git a/modules/tile-converter/src/i3s-converter/helpers/tileset-traversal.ts b/modules/tile-converter/src/i3s-converter/helpers/tileset-traversal.ts new file mode 100644 index 0000000000..b358209ff4 --- /dev/null +++ b/modules/tile-converter/src/i3s-converter/helpers/tileset-traversal.ts @@ -0,0 +1,51 @@ +import {Tiles3DTileJSONPostprocessed} from '@loaders.gl/3d-tiles'; +import {NodeIndexDocument} from './node-index-document'; +import {Matrix4} from '@math.gl/core'; + +/** Traversal props for the conversion stage */ +export type TraversalConversionProps = { + /** Transformation matrix for the specific tile */ + transform: Matrix4; + /** Parent nodes of the converted tile. Multiple nodes can be if one tile is converted to multiple nodes*/ + parentNodes: NodeIndexDocument[]; +}; + +/** + * Travesal of 3DTile tiles tree with making specific actions with each tile + * @param tile - 3DTiles tile JSON metadata + * @param traversalProps - traversal props used to pass data through recursive calls + * @param processTile - callback to make some actions with the current tile + * @param postprocessTile - callback to make some action after processing of the current tile and all the subtree + * @param maxDepth - max recursive calls number the travesal function will do. If not set, the traversal function will + * go through all the tree. + * This value is used to limit the convertion with only partial number of levels of the tileset + * @param level - counter to keep recursive calls number of the tiles tree. This value used to be able to break + * traversal at the some level of the tree + * @returns void + */ +export const traverseDatasetWith = async ( + tile: Tiles3DTileJSONPostprocessed, + traversalProps: TProps, + processTile: (tile: Tiles3DTileJSONPostprocessed, traversalProps: TProps) => Promise, + postprocessTile?: (processResults: TProps[], currentTraversalProps: TProps) => Promise, + maxDepth?: number, + level = 0 +): Promise => { + if (maxDepth && level > maxDepth) { + return; + } + const processResults: TProps[] = []; + const newTraversalProps: TProps = await processTile(tile, traversalProps); + processResults.push(newTraversalProps); + for (const childTile of tile.children) { + await traverseDatasetWith( + childTile, + newTraversalProps, + processTile, + postprocessTile, + maxDepth, + level + 1 + ); + } + postprocessTile && (await postprocessTile(processResults, traversalProps)); +}; diff --git a/modules/tile-converter/src/i3s-converter/i3s-converter.ts b/modules/tile-converter/src/i3s-converter/i3s-converter.ts index 96e7793cab..fbdd46c380 100644 --- a/modules/tile-converter/src/i3s-converter/i3s-converter.ts +++ b/modules/tile-converter/src/i3s-converter/i3s-converter.ts @@ -2,6 +2,7 @@ import type { FeatureTableJson, + Tiles3DLoaderOptions, Tiles3DTileContent, Tiles3DTileJSONPostprocessed, Tiles3DTilesetJSONPostprocessed @@ -46,10 +47,15 @@ import {SHARED_RESOURCES as sharedResourcesTemplate} from './json-templates/shar import {validateNodeBoundingVolumes} from './helpers/node-debug'; import {KTX2BasisWriterWorker} from '@loaders.gl/textures'; import {LoaderWithParser} from '@loaders.gl/loader-utils'; -import {I3SMaterialDefinition, TextureSetDefinitionFormats} from '@loaders.gl/i3s/src/types'; +import {I3SMaterialDefinition, TextureSetDefinitionFormats} from '@loaders.gl/i3s'; import {ImageWriter} from '@loaders.gl/images'; import {GLTFImagePostprocessed} from '@loaders.gl/gltf'; -import {I3SConvertedResources, SharedResourcesArrays, Tiles3DLoadOptions} from './types'; +import { + GltfPrimitiveModeString, + I3SConvertedResources, + PreprocessData, + SharedResourcesArrays +} from './types'; import {getWorkerURL, WorkerFarm} from '@loaders.gl/worker-utils'; import {DracoWriterWorker} from '@loaders.gl/draco'; import WriteQueue from '../lib/utils/write-queue'; @@ -67,6 +73,8 @@ import {loadNestedTileset, loadTile3DContent} from './helpers/load-3d-tiles'; import {Matrix4} from '@math.gl/core'; import {BoundingSphere, OrientedBoundingBox} from '@math.gl/culling'; import {createBoundingVolume} from '@loaders.gl/tiles'; +import {TraversalConversionProps, traverseDatasetWith} from './helpers/tileset-traversal'; +import {analyzeTileContent, mergePreprocessData} from './helpers/preprocess-3d-tiles'; const ION_DEFAULT_TOKEN = process.env?.IonToken || // eslint-disable-line @@ -101,7 +109,7 @@ export default class I3SConverter { conversionStartTime: [number, number] = [0, 0]; refreshTokenTime: [number, number] = [0, 0]; sourceTileset: Tiles3DTilesetJSONPostprocessed | null = null; - loadOptions: Tiles3DLoadOptions = { + loadOptions: Tiles3DLoaderOptions = { _nodeWorkers: true, reuseWorkers: true, basis: { @@ -111,9 +119,7 @@ export default class I3SConverter { }, // We need to load local fs workers because nodejs can't load workers from the Internet draco: {workerUrl: './modules/draco/dist/draco-worker-node.js'}, - fetch: { - headers: null - } + fetch: {} }; geoidHeightModel: Geoid | null = null; Loader: LoaderWithParser = Tiles3DLoader; @@ -123,6 +129,9 @@ export default class I3SConverter { workerSource: {[key: string]: string} = {}; writeQueue: WriteQueue = new WriteQueue(); compressList: string[] | null = null; + preprocessData: PreprocessData = { + meshTopologyTypes: new Set() + }; constructor() { this.nodePages = new NodePages(writeFile, HARDCODED_NODES_PER_PAGE, this); @@ -237,16 +246,81 @@ export default class I3SConverter { } this.sourceTileset = await load(inputUrl, this.Loader, this.loadOptions); - await this._createAndSaveTileset(outputPath, tilesetName); - await this._finishConversion({slpk: Boolean(slpk), outputPath, tilesetName}); - return 'success'; + const preprocessResult = await this.preprocessConversion(); + + if (preprocessResult) { + await this._createAndSaveTileset(outputPath, tilesetName); + await this._finishConversion({slpk: Boolean(slpk), outputPath, tilesetName}); + } } catch (error) { throw error; } finally { + await this.writeQueue.finalize(); // Clean up worker pools const workerFarm = WorkerFarm.getWorkerFarm({}); workerFarm.destroy(); } + return 'success'; + } + + /** + * Preprocess stage of the tile converter. Traverse all the tiles tree and + * check a tile content to be sure that the data is supported + * @returns true - the conversion is possible, false - the tileset's content is not supported + */ + private async preprocessConversion(): Promise { + console.log(`Analyze source tileset`); + const sourceRootTile: Tiles3DTileJSONPostprocessed = this.sourceTileset!.root!; + await traverseDatasetWith( + sourceRootTile, + null, + this.analyzeTile.bind(this), + undefined, + this.options.maxDepth + ); + const {meshTopologyTypes} = this.preprocessData; + console.log(`------------------------------------------------`); + console.log(`Preprocess results:`); + console.log(`glTF mesh topology types: ${Array.from(meshTopologyTypes).join(', ')}`); + console.log(`------------------------------------------------`); + if ( + !meshTopologyTypes.has(GltfPrimitiveModeString.TRIANGLES) && + !meshTopologyTypes.has(GltfPrimitiveModeString.TRIANGLE_STRIP) + ) { + console.log( + 'The tileset is of unsupported mesh topology types. The conversion will be interrupted.' + ); + console.log(`------------------------------------------------`); + return false; + } + return true; + } + + /** + * Analyze a tile content. The callback for preprocess stage. + * @param sourceTile - 3DTiles tile JSON metadata + * @param traversalProps - mandatory argument but it is not used for the preprocess stage + * @returns - nothing + */ + private async analyzeTile( + sourceTile: Tiles3DTileJSONPostprocessed, + traversalProps: null + ): Promise { + if (sourceTile.type === 'json') { + await loadNestedTileset(this.sourceTileset, sourceTile, this.loadOptions); + return null; + } + if (sourceTile.id) { + console.log(`[analyze]: ${sourceTile.id}`); // eslint-disable-line + } + const tileContent = await loadTile3DContent(this.sourceTileset, sourceTile, { + ...this.loadOptions, + '3d-tiles': {...this.loadOptions['3d-tiles'], loadGLTF: false} + }); + const tilePreprocessData = await analyzeTileContent(sourceTile, tileContent); + mergePreprocessData(this.preprocessData, tilePreprocessData); + + return null; } /** @@ -291,7 +365,16 @@ export default class I3SConverter { }); const rootNode = await NodeIndexDocument.createRootNode(boundingVolumes, this); - await this._convertNodesTree(rootNode, sourceRootTile); + await traverseDatasetWith( + sourceRootTile, + { + transform: new Matrix4(sourceRootTile.transform), + parentNodes: [rootNode] + }, + this.convertTile.bind(this), + this.finalizeTile.bind(this), + this.options.maxDepth + ); this.layers0!.materialDefinitions = this.materialDefinitions; // @ts-ignore @@ -418,143 +501,62 @@ export default class I3SConverter { } /** - * Form object of 3DSceneLayer https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DSceneLayer.cmn.md - * @param parentNode - 3DNodeIndexDocument of the parent node https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DNodeIndexDocument.cmn.md - * @param sourceTile - Source 3DTiles tile data - * @param parentTransform - transformation matrix of the parent tile + * Convert the specific 3DTiles tile to I3S nodes. + * This is callback function for the traversal generic function + * @param sourceTile - current 3DTiles tile JSON metadata + * @param traversalProps - traversal properties calculated recursively + * @returns - traversal properties for the child tiles */ - private async _convertNodesTree( - parentNode: NodeIndexDocument, + private async convertTile( sourceTile: Tiles3DTileJSONPostprocessed, - parentTransform: Matrix4 = new Matrix4() - ): Promise { - let transformationMatrix: Matrix4 = parentTransform.clone(); - if (sourceTile.transform) { - transformationMatrix = transformationMatrix.multiplyRight(sourceTile.transform); - } - if (this.isContentSupported(sourceTile)) { - const childNodes = await this._createNode(parentNode, sourceTile, transformationMatrix, 0); - for (const childNode of childNodes) { - await childNode.save(); + traversalProps: TraversalConversionProps + ): Promise { + if (sourceTile.type === 'json' || sourceTile.type === 'empty') { + if (sourceTile.type === 'json') { + if (sourceTile.id) { + console.log(`[load]: ${sourceTile.id}`); // eslint-disable-line + } + await loadNestedTileset(this.sourceTileset, sourceTile, this.loadOptions); } - await parentNode.addChildren(childNodes); - } else { - await loadNestedTileset(this.sourceTileset, sourceTile, this.loadOptions); - await this._addChildrenWithNeighborsAndWriteFile({ - parentNode: parentNode, - sourceTiles: sourceTile.children, - parentTransform: transformationMatrix, - level: 1 - }); + return traversalProps; } if (sourceTile.id) { - console.log(sourceTile.id); // eslint-disable-line + console.log(`[convert]: ${sourceTile.id}`); // eslint-disable-line } - await parentNode.save(); - } - /** - * Add child nodes recursively and write them to files - * @param data - arguments - * @param data.parentNode - 3DNodeIndexDocument of parent node - * @param data.sourceTiles - array of source child nodes - * @param data.parentTransform - transformation matrix of the parent tile - * @param data.level - level of node (distanse to root node in the tree) - */ - private async _addChildrenWithNeighborsAndWriteFile(data: { - parentNode: NodeIndexDocument; - sourceTiles: Tiles3DTileJSONPostprocessed[]; - parentTransform: Matrix4; - level: number; - }): Promise { - await this._addChildren(data); - await data.parentNode.addNeighbors(); - } - - /** - * Convert nested subtree of 3DTiles dataset - * @param param0 - * @param data.parentNode - 3DNodeIndexDocument of parent node - * @param param0.sourceTile - source 3DTile data - * @param param0.transformationMatrix - transformation matrix of the current tile - * @param param0.level - tree level - */ - private async convertNestedTileset({ - parentNode, - sourceTile, - transformationMatrix, - level - }: { - parentNode: NodeIndexDocument; - sourceTile: Tiles3DTileJSONPostprocessed; - transformationMatrix: Matrix4; - level: number; - }) { - await loadNestedTileset(this.sourceTileset, sourceTile, this.loadOptions); - await this._addChildren({ - parentNode, - sourceTiles: sourceTile.children, - parentTransform: transformationMatrix, - level: level + 1 - }); - } - - /** - * Convert 3DTiles tile to I3S node - * @param param0 - * @param param0.parentNode - 3DNodeIndexDocument of parent node - * @param param0.sourceTile - source 3DTile data - * @param param0.transformationMatrix - transformation matrix of the current tile, calculated recursively multiplying - * transform of all parent tiles and transform of the current tile - * @param param0.level - tree level - */ - private async convertNode({ - parentNode, - sourceTile, - transformationMatrix, - level - }: { - parentNode: NodeIndexDocument; - sourceTile: Tiles3DTileJSONPostprocessed; - transformationMatrix: Matrix4; - level: number; - }) { - const childNodes = await this._createNode(parentNode, sourceTile, transformationMatrix, level); + const {parentNodes, transform} = traversalProps; + let transformationMatrix: Matrix4 = transform.clone(); + if (sourceTile.transform) { + transformationMatrix = transformationMatrix.multiplyRight(sourceTile.transform); + } + const parentNode = parentNodes[0]; + const childNodes = await this._createNode(parentNode, sourceTile, transformationMatrix); await parentNode.addChildren(childNodes); + + const newTraversalProps: TraversalConversionProps = { + transform: transformationMatrix, + parentNodes: childNodes + }; + return newTraversalProps; } /** - * Add child nodes recursively and write them to files - * @param param0 - arguments - * @param param0.parentNode - 3DNodeIndexDocument of parent node - * @param param0.sourceTile - source 3DTile data - * @param data.parentTransform - transformation matrix of the parent tile - * @param param0.level - tree level + * Do final action with nodes after the current node and all child nodes been converted. + * @param conversionResults - array of conversion results of the current node + * @param currentTraversalProps - traversal properties of the current node */ - private async _addChildren(data: { - parentNode: NodeIndexDocument; - sourceTiles: Tiles3DTileJSONPostprocessed[]; - parentTransform: Matrix4; - level: number; - }): Promise { - const {sourceTiles, parentTransform, parentNode, level} = data; - if (this.options.maxDepth && level > this.options.maxDepth) { - return; - } - for (const sourceTile of sourceTiles) { - let transformationMatrix: Matrix4 = parentTransform.clone(); - if (sourceTile.transform) { - transformationMatrix = transformationMatrix.multiplyRight(sourceTile.transform); - } - if (sourceTile.type === 'json') { - await this.convertNestedTileset({parentNode, sourceTile, transformationMatrix, level}); - } else { - await this.convertNode({parentNode, sourceTile, transformationMatrix, level}); - } - if (sourceTile.id) { - console.log(sourceTile.id); // eslint-disable-line + private async finalizeTile( + conversionResults: TraversalConversionProps[], + currentTraversalProps: TraversalConversionProps + ): Promise { + for (const result of conversionResults) { + for (const node of result.parentNodes) { + await node.addNeighbors(); } } + for (const node of currentTraversalProps.parentNodes) { + await node.save(); + } } /** @@ -568,8 +570,7 @@ export default class I3SConverter { private async _createNode( parentNode: NodeIndexDocument, sourceTile: Tiles3DTileJSONPostprocessed, - transformationMatrix: Matrix4, - level: number + transformationMatrix: Matrix4 ): Promise { this._checkAddRefinementTypeForTile(sourceTile); @@ -660,12 +661,6 @@ export default class I3SConverter { nodesInPage.push(nodeInPage); } - await this._addChildrenWithNeighborsAndWriteFile({ - parentNode: nodes[0], - sourceTiles: sourceTile.children, - parentTransform: transformationMatrix, - level: level + 1 - }); return nodes; } diff --git a/modules/tile-converter/src/i3s-converter/types.ts b/modules/tile-converter/src/i3s-converter/types.ts index 0849fed188..aedd2ca84e 100644 --- a/modules/tile-converter/src/i3s-converter/types.ts +++ b/modules/tile-converter/src/i3s-converter/types.ts @@ -164,15 +164,22 @@ export type TypedArrayConstructor = | Float32ArrayConstructor | Float64ArrayConstructor; -export type Tiles3DLoadOptions = { - _nodeWorkers: boolean; - reuseWorkers: boolean; - basis: { - format: string; - workerUrl: string; - }; - draco: {workerUrl: string}; - fetch: { - headers: any; - }; +/** + * glTF primitive modes (mesh topology types) + * @see https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_mode + */ +export enum GltfPrimitiveModeString { + POINTS = 'POINTS', + LINES = 'LINES', + LINE_LOOP = 'LINE_LOOP', + LINE_STRIP = 'LINE_STRIP', + TRIANGLES = 'TRIANGLES', + TRIANGLE_STRIP = 'TRIANGLE_STRIP', + TRIANGLE_FAN = 'TRIANGLE_FAN' +} + +/** Preprocessed data gathered from child tiles binary content */ +export type PreprocessData = { + /** Mesh topology types used in gltf primitives of the tileset */ + meshTopologyTypes: Set; };