From 84858b79c688c3855a3f739dea9babc1cdf2e611 Mon Sep 17 00:00:00 2001 From: Viktor Belomestnov Date: Thu, 15 Jun 2023 12:35:17 +0200 Subject: [PATCH 1/3] chore(3d-tiles): tile content type --- .../helpers/normalize-3d-tile-colors.ts | 6 +- .../helpers/normalize-3d-tile-normals.ts | 10 +- .../helpers/parse-3d-implicit-tiles.ts | 8 +- .../helpers/parse-3d-tile-gltf-view.ts | 48 ++++++--- .../parsers/helpers/parse-3d-tile-header.ts | 8 +- .../parsers/helpers/parse-3d-tile-tables.ts | 43 +++++--- .../parsers/parse-3d-tile-batched-model.ts | 21 +++- .../lib/parsers/parse-3d-tile-composite.ts | 13 +-- .../src/lib/parsers/parse-3d-tile-gltf.ts | 12 ++- .../parsers/parse-3d-tile-instanced-model.ts | 57 +++++------ .../lib/parsers/parse-3d-tile-point-cloud.ts | 70 +++++++++---- .../3d-tiles/src/lib/parsers/parse-3d-tile.ts | 15 ++- modules/3d-tiles/src/types.ts | 97 ++++++++++++++++++- .../lib/parsers/point-cloud-3d-tile.spec.ts | 2 +- 14 files changed, 303 insertions(+), 107 deletions(-) diff --git a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts index 94faacccf7..e6632fea03 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts @@ -1,7 +1,11 @@ import {decodeRGB565, GL} from '@loaders.gl/math'; /* eslint-disable complexity*/ -export function normalize3DTileColorAttribute(tile, colors, batchTable?) { +export function normalize3DTileColorAttribute( + tile, + colors, + batchTable? +): {type: number; value: Uint8ClampedArray; size: number; normalized: boolean} | null { // no colors defined if (!colors && (!tile || !tile.batchIds || !batchTable)) { return null; diff --git a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts index 04c3e8d0be..cfb33b27dc 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts @@ -1,16 +1,20 @@ import {Vector3} from '@math.gl/core'; import {GL, octDecode} from '@loaders.gl/math'; +import {Tiles3DTileContent} from 'modules/3d-tiles/src/types'; const scratchNormal = new Vector3(); -export function normalize3DTileNormalAttribute(tile, normals) { +export function normalize3DTileNormalAttribute( + tile: Tiles3DTileContent, + normals +): {type: number; size: number; value: Float32Array} | null { if (!normals) { return null; } if (tile.isOctEncoded16P) { - const decodedArray = new Float32Array(tile.pointsLength * 3); - for (let i = 0; i < tile.pointsLength; i++) { + const decodedArray = new Float32Array((tile.pointsLength || 0) * 3); + for (let i = 0; i < (tile.pointsLength || 0); i++) { octDecode(normals[i * 2], normals[i * 2 + 1], scratchNormal); // @ts-ignore scratchNormal.toArray(decodedArray, i * 3); diff --git a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-implicit-tiles.ts b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-implicit-tiles.ts index 5f772dec6a..3ed8042a15 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-implicit-tiles.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-implicit-tiles.ts @@ -1,4 +1,4 @@ -import type {Availability, BoundingVolume, Subtree} from '../../../types'; +import type {Availability, Tile3DBoundingVolume, Subtree} from '../../../types'; import {Tile3DSubtreeLoader} from '../../../tile-3d-subtree-loader'; import {load} from '@loaders.gl/core'; @@ -263,7 +263,7 @@ function formatTileData( const uri = tile.contentUrl && tile.contentUrl.replace(`${basePath}/`, ''); const lodMetricValue = rootLodMetricValue / 2 ** level; - const boundingVolume: BoundingVolume = s2VolumeBox?.box + const boundingVolume: Tile3DBoundingVolume = s2VolumeBox?.box ? {box: s2VolumeBox.box} : rootBoundingVolume; @@ -297,9 +297,9 @@ function formatTileData( */ function calculateBoundingVolumeForChildTile( level: number, - rootBoundingVolume: BoundingVolume, + rootBoundingVolume: Tile3DBoundingVolume, childCoordinates: {childTileX: number; childTileY: number; childTileZ: number} -): BoundingVolume { +): Tile3DBoundingVolume { if (rootBoundingVolume.region) { const {childTileX, childTileY, childTileZ} = childCoordinates; const [west, south, east, north, minimumHeight, maximumHeight] = rootBoundingVolume.region; diff --git a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-gltf-view.ts b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-gltf-view.ts index 446cc8f9b1..1bcc6c9fc5 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-gltf-view.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-gltf-view.ts @@ -8,28 +8,35 @@ // - Also, should we have hard dependency on gltf module or use injection or auto-discovery for gltf parser? import {GLTFLoader, postProcessGLTF, _getMemoryUsageGLTF} from '@loaders.gl/gltf'; -import {sliceArrayBuffer} from '@loaders.gl/loader-utils'; +import {LoaderContext, sliceArrayBuffer} from '@loaders.gl/loader-utils'; +import {Tiles3DLoaderOptions} from 'modules/3d-tiles/src/tiles-3d-loader'; +import {Tiles3DTileContent} from 'modules/3d-tiles/src/types'; export const GLTF_FORMAT = { URI: 0, EMBEDDED: 1 }; -export function parse3DTileGLTFViewSync(tile, arrayBuffer, byteOffset, options) { +export function parse3DTileGLTFViewSync( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number, + options: Tiles3DLoaderOptions | undefined +) { // Set flags // glTF models need to be rotated from Y to Z up // https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification#y-up-to-z-up tile.rotateYtoZ = true; // Assume glTF consumes rest of tile - const gltfByteLength = tile.byteOffset + tile.byteLength - byteOffset; + const gltfByteLength = (tile.byteOffset || 0) + (tile.byteLength || 0) - byteOffset; if (gltfByteLength === 0) { throw new Error('glTF byte length must be greater than 0.'); } // Save gltf up axis tile.gltfUpAxis = - options['3d-tiles'] && options['3d-tiles'].assetGltfUpAxis + options?.['3d-tiles'] && options['3d-tiles'].assetGltfUpAxis ? options['3d-tiles'].assetGltfUpAxis : 'Y'; @@ -50,18 +57,27 @@ export function parse3DTileGLTFViewSync(tile, arrayBuffer, byteOffset, options) } // Entire tile is consumed - return tile.byteOffset + tile.byteLength; + return (tile.byteOffset || 0) + (tile.byteLength || 0); } -export async function extractGLTF(tile, gltfFormat, options, context) { - const tile3DOptions = options['3d-tiles'] || {}; +export async function extractGLTF( + tile: Tiles3DTileContent, + gltfFormat: number, + options?: Tiles3DLoaderOptions, + context?: LoaderContext +): Promise { + const tile3DOptions = options?.['3d-tiles'] || {}; extractGLTFBufferOrURL(tile, gltfFormat, options); if (tile3DOptions.loadGLTF) { + if (!context) { + return; + } const {parse, fetch} = context; if (tile.gltfUrl) { - tile.gltfArrayBuffer = await fetch(tile.gltfUrl, options); + const response = await fetch(tile.gltfUrl, options); + tile.gltfArrayBuffer = await response.arrayBuffer(); tile.gltfByteOffset = 0; } if (tile.gltfArrayBuffer) { @@ -76,15 +92,21 @@ export async function extractGLTF(tile, gltfFormat, options, context) { } } -function extractGLTFBufferOrURL(tile, gltfFormat, options) { +function extractGLTFBufferOrURL( + tile: Tiles3DTileContent, + gltfFormat: number, + options: Tiles3DLoaderOptions | undefined +) { switch (gltfFormat) { case GLTF_FORMAT.URI: // We need to remove padding from the end of the model URL in case this tile was part of a composite tile. // This removes all white space and null characters from the end of the string. - const gltfUrlBytes = new Uint8Array(tile.gltfArrayBuffer, tile.gltfByteOffset); - const textDecoder = new TextDecoder(); - const gltfUrl = textDecoder.decode(gltfUrlBytes); - tile.gltfUrl = gltfUrl.replace(/[\s\0]+$/, ''); + if (tile.gltfArrayBuffer) { + const gltfUrlBytes = new Uint8Array(tile.gltfArrayBuffer, tile.gltfByteOffset); + const textDecoder = new TextDecoder(); + const gltfUrl = textDecoder.decode(gltfUrlBytes); + tile.gltfUrl = gltfUrl.replace(/[\s\0]+$/, ''); + } delete tile.gltfArrayBuffer; delete tile.gltfByteOffset; delete tile.gltfByteLength; diff --git a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-header.ts b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-header.ts index 5033c65c60..3beb5c7392 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-header.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-header.ts @@ -1,6 +1,8 @@ // This file is derived from the Cesium code base under Apache 2 license // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md +import {Tiles3DTileContent} from 'modules/3d-tiles/src/types'; + const SIZEOF_UINT32 = 4; /* PARSE FIXED HEADER: @@ -10,7 +12,11 @@ Populates version, byteLength */ -export function parse3DTileHeaderSync(tile, arrayBuffer, byteOffset = 0) { +export function parse3DTileHeaderSync( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number = 0 +) { const view = new DataView(arrayBuffer); tile.magic = view.getUint32(byteOffset, true); diff --git a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-tables.ts b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-tables.ts index f1356cc1c1..91d91f4d01 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-tables.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/parse-3d-tile-tables.ts @@ -1,13 +1,19 @@ // This file is derived from the Cesium code base under Apache 2 license // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md +import {Tiles3DTileContent} from 'modules/3d-tiles/src/types'; import {getStringFromArrayBuffer} from './parse-utils'; +import {Tiles3DLoaderOptions} from 'modules/3d-tiles/src/tiles-3d-loader'; const SIZEOF_UINT32 = 4; const DEPRECATION_WARNING = 'b3dm tile in legacy format.'; // eslint-disable-next-line max-statements -export function parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset) { +export function parse3DTileTablesHeaderSync( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number +) { const view = new DataView(arrayBuffer); let batchLength; @@ -58,20 +64,30 @@ export function parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset) { return byteOffset; } -export function parse3DTileTablesSync(tile, arrayBuffer, byteOffset, options) { +export function parse3DTileTablesSync( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number, + options?: Tiles3DLoaderOptions +) { byteOffset = parse3DTileFeatureTable(tile, arrayBuffer, byteOffset, options); byteOffset = parse3DTileBatchTable(tile, arrayBuffer, byteOffset, options); return byteOffset; } -function parse3DTileFeatureTable(tile, arrayBuffer, byteOffset, options) { - const {featureTableJsonByteLength, featureTableBinaryByteLength, batchLength} = tile.header; +function parse3DTileFeatureTable( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number, + options?: Tiles3DLoaderOptions +) { + const {featureTableJsonByteLength, featureTableBinaryByteLength, batchLength} = tile.header || {}; tile.featureTableJson = { BATCH_LENGTH: batchLength || 0 }; - if (featureTableJsonByteLength > 0) { + if (featureTableJsonByteLength && featureTableJsonByteLength > 0) { const featureTableString = getStringFromArrayBuffer( arrayBuffer, byteOffset, @@ -79,10 +95,10 @@ function parse3DTileFeatureTable(tile, arrayBuffer, byteOffset, options) { ); tile.featureTableJson = JSON.parse(featureTableString); } - byteOffset += featureTableJsonByteLength; + byteOffset += featureTableJsonByteLength || 0; tile.featureTableBinary = new Uint8Array(arrayBuffer, byteOffset, featureTableBinaryByteLength); - byteOffset += featureTableBinaryByteLength; + byteOffset += featureTableBinaryByteLength || 0; /* const featureTable = parseFeatureTable(featureTableJson, featureTableBinary); @@ -94,10 +110,15 @@ function parse3DTileFeatureTable(tile, arrayBuffer, byteOffset, options) { return byteOffset; } -function parse3DTileBatchTable(tile, arrayBuffer, byteOffset, options) { - const {batchTableJsonByteLength, batchTableBinaryByteLength} = tile.header; +function parse3DTileBatchTable( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number, + options?: Tiles3DLoaderOptions +) { + const {batchTableJsonByteLength, batchTableBinaryByteLength} = tile.header || {}; - if (batchTableJsonByteLength > 0) { + if (batchTableJsonByteLength && batchTableJsonByteLength > 0) { const batchTableString = getStringFromArrayBuffer( arrayBuffer, byteOffset, @@ -106,7 +127,7 @@ function parse3DTileBatchTable(tile, arrayBuffer, byteOffset, options) { tile.batchTableJson = JSON.parse(batchTableString); byteOffset += batchTableJsonByteLength; - if (batchTableBinaryByteLength > 0) { + if (batchTableBinaryByteLength && batchTableBinaryByteLength > 0) { // Has a batch table binary tile.batchTableBinary = new Uint8Array(arrayBuffer, byteOffset, batchTableBinaryByteLength); // Copy the batchTableBinary section and let the underlying ArrayBuffer be freed diff --git a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-batched-model.ts b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-batched-model.ts index a2e3fec273..2622065dc4 100644 --- a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-batched-model.ts +++ b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-batched-model.ts @@ -8,8 +8,17 @@ import Tile3DFeatureTable from '../classes/tile-3d-feature-table'; import {parse3DTileHeaderSync} from './helpers/parse-3d-tile-header'; import {parse3DTileTablesHeaderSync, parse3DTileTablesSync} from './helpers/parse-3d-tile-tables'; import {parse3DTileGLTFViewSync, extractGLTF, GLTF_FORMAT} from './helpers/parse-3d-tile-gltf-view'; - -export async function parseBatchedModel3DTile(tile, arrayBuffer, byteOffset, options, context) { +import {Tiles3DTileContent} from '../../types'; +import {Tiles3DLoaderOptions} from '../../tiles-3d-loader'; +import {LoaderContext} from '@loaders.gl/loader-utils'; + +export async function parseBatchedModel3DTile( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number, + options?: Tiles3DLoaderOptions, + context?: LoaderContext +) { byteOffset = parseBatchedModel(tile, arrayBuffer, byteOffset, options, context); await extractGLTF(tile, GLTF_FORMAT.EMBEDDED, options, context); @@ -21,7 +30,13 @@ export async function parseBatchedModel3DTile(tile, arrayBuffer, byteOffset, opt return byteOffset; } -function parseBatchedModel(tile, arrayBuffer, byteOffset, options, context) { +function parseBatchedModel( + tile: Tiles3DTileContent, + arrayBuffer: ArrayBuffer, + byteOffset: number, + options?: Tiles3DLoaderOptions, + context?: LoaderContext +) { byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset); byteOffset = parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset); diff --git a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-composite.ts b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-composite.ts index 780787ab0d..233074760c 100644 --- a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-composite.ts +++ b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-composite.ts @@ -7,23 +7,24 @@ import type {LoaderContext} from '@loaders.gl/loader-utils'; import type {Tiles3DLoaderOptions} from '../../tiles-3d-loader'; import {parse3DTileHeaderSync} from './helpers/parse-3d-tile-header'; +import {Tiles3DTileContent} from '../../types'; /** Resolve circulate dependency by passing in parsing function as argument */ type Parse3DTile = ( arrayBuffer: ArrayBuffer, byteOffset: number, - options: Tiles3DLoaderOptions, - context: LoaderContext, + options: Tiles3DLoaderOptions | undefined, + context: LoaderContext | undefined, subtile ) => Promise; // eslint-disable-next-line max-params export async function parseComposite3DTile( - tile, + tile: Tiles3DTileContent, arrayBuffer: ArrayBuffer, byteOffset: number, - options: Tiles3DLoaderOptions, - context: LoaderContext, + options: Tiles3DLoaderOptions | undefined, + context: LoaderContext | undefined, parse3DTile: Parse3DTile ): Promise { byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset); @@ -36,7 +37,7 @@ export async function parseComposite3DTile( // extract each tile from the byte stream tile.tiles = []; - while (tile.tiles.length < tile.tilesLength && tile.byteLength - byteOffset > 12) { + while (tile.tiles.length < tile.tilesLength && (tile.byteLength || 0) - byteOffset > 12) { const subtile = {}; tile.tiles.push(subtile); byteOffset = await parse3DTile(arrayBuffer, byteOffset, options, context, subtile); 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 b71f9c7f62..6dd061cad9 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 @@ -1,12 +1,13 @@ import type {LoaderContext} from '@loaders.gl/loader-utils'; import type {Tiles3DLoaderOptions} from '../../tiles-3d-loader'; import {_getMemoryUsageGLTF, GLTFLoader, postProcessGLTF} from '@loaders.gl/gltf'; +import {Tiles3DTileContent} from '../../types'; export async function parseGltf3DTile( - tile, + tile: Tiles3DTileContent, arrayBuffer: ArrayBuffer, - options: Tiles3DLoaderOptions, - context: LoaderContext + options?: Tiles3DLoaderOptions, + context?: LoaderContext ): Promise { // Set flags // glTF models need to be rotated from Y to Z up @@ -14,10 +15,13 @@ export async function parseGltf3DTile( tile.rotateYtoZ = true; // Save gltf up axis tile.gltfUpAxis = - options['3d-tiles'] && options['3d-tiles'].assetGltfUpAxis + options?.['3d-tiles'] && options['3d-tiles'].assetGltfUpAxis ? options['3d-tiles'].assetGltfUpAxis : 'Y'; + if (!context) { + return; + } const {parse} = context; const gltfWithBuffers = await parse(arrayBuffer, GLTFLoader, options, context); tile.gltf = postProcessGLTF(gltfWithBuffers); diff --git a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-instanced-model.ts b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-instanced-model.ts index f4bda7caf1..72898304af 100644 --- a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-instanced-model.ts +++ b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-instanced-model.ts @@ -12,25 +12,26 @@ import {parse3DTileTablesHeaderSync, parse3DTileTablesSync} from './helpers/pars import {parse3DTileGLTFViewSync, extractGLTF} from './helpers/parse-3d-tile-gltf-view'; import {Tiles3DLoaderOptions} from '../../tiles-3d-loader'; import {LoaderContext} from '@loaders.gl/loader-utils'; +import {Tiles3DTileContent} from '../../types'; export async function parseInstancedModel3DTile( - tile, + tile: Tiles3DTileContent, arrayBuffer: ArrayBuffer, byteOffset: number, - options: Tiles3DLoaderOptions, - context: LoaderContext + options?: Tiles3DLoaderOptions, + context?: LoaderContext ): Promise { byteOffset = parseInstancedModel(tile, arrayBuffer, byteOffset, options, context); - await extractGLTF(tile, tile.gltfFormat, options, context); + await extractGLTF(tile, tile.gltfFormat || 0, options, context); return byteOffset; } function parseInstancedModel( - tile, + tile: Tiles3DTileContent, arrayBuffer: ArrayBuffer, byteOffset: number, - options: Tiles3DLoaderOptions, - context: LoaderContext + options?: Tiles3DLoaderOptions, + context?: LoaderContext ): number { byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset); if (tile.version !== 1) { @@ -50,7 +51,7 @@ function parseInstancedModel( byteOffset = parse3DTileGLTFViewSync(tile, arrayBuffer, byteOffset, options); // TODO - Is the feature table sometimes optional or can check be moved into table header parser? - if (tile.featureTableJsonByteLength === 0) { + if (!tile?.header?.featureTableJsonByteLength || tile.header.featureTableJsonByteLength === 0) { throw new Error('i3dm parser: featureTableJsonByteLength is zero.'); } @@ -78,23 +79,13 @@ function parseInstancedModel( } // eslint-disable-next-line max-statements, complexity -function extractInstancedAttributes(tile, featureTable, batchTable, instancesLength) { - // Create model instance collection - const collectionOptions = { - instances: new Array(instancesLength), - batchTable: tile._batchTable, - cull: false, // Already culled by 3D Tiles - url: undefined, - // requestType: RequestType.TILES3D, - gltf: undefined, - basePath: undefined, - incrementallyLoadTextures: false, - // TODO - tileset is not available at this stage, tile is parsed independently - // upAxis: (tileset && tileset._gltfUpAxis) || [0, 1, 0], - forwardAxis: [1, 0, 0] - }; - - const instances = collectionOptions.instances; +function extractInstancedAttributes( + tile: Tiles3DTileContent, + featureTable: Tile3DFeatureTable, + batchTable: Tile3DBatchTable, + instancesLength: number +) { + const instances = new Array(instancesLength); const instancePosition = new Vector3(); const instanceNormalRight = new Vector3(); const instanceNormalUp = new Vector3(); @@ -106,8 +97,8 @@ function extractInstancedAttributes(tile, featureTable, batchTable, instancesLen const instanceTransform = new Matrix4(); const scratch1 = []; const scratch2 = []; - const scratchVector1 = new Vector3(); - const scratchVector2 = new Vector3(); + const scratch3 = []; + const scratch4 = []; for (let i = 0; i < instancesLength; i++) { let position; @@ -127,8 +118,7 @@ function extractInstancedAttributes(tile, featureTable, batchTable, instancesLen const quantizedVolumeOffset = featureTable.getGlobalProperty( 'QUANTIZED_VOLUME_OFFSET', GL.FLOAT, - 3, - scratchVector1 + 3 ); if (!quantizedVolumeOffset) { throw new Error( @@ -139,8 +129,7 @@ function extractInstancedAttributes(tile, featureTable, batchTable, instancesLen const quantizedVolumeScale = featureTable.getGlobalProperty( 'QUANTIZED_VOLUME_SCALE', GL.FLOAT, - 3, - scratchVector2 + 3 ); if (!quantizedVolumeScale) { throw new Error( @@ -180,12 +169,14 @@ function extractInstancedAttributes(tile, featureTable, batchTable, instancesLen 'NORMAL_UP_OCT32P', GL.UNSIGNED_SHORT, 2, + i, scratch1 ); tile.octNormalRight = featureTable.getProperty( 'NORMAL_RIGHT_OCT32P', GL.UNSIGNED_SHORT, 2, + i, scratch2 ); @@ -223,7 +214,7 @@ function extractInstancedAttributes(tile, featureTable, batchTable, instancesLen // Get the instance scale instanceScale.set(1.0, 1.0, 1.0); - const scale = featureTable.getProperty('SCALE', GL.FLOAT, 1, i); + const scale = featureTable.getProperty('SCALE', GL.FLOAT, 1, i, scratch3); if (Number.isFinite(scale)) { instanceScale.multiplyByScalar(scale); } @@ -236,7 +227,7 @@ function extractInstancedAttributes(tile, featureTable, batchTable, instancesLen instanceTranslationRotationScale.scale = instanceScale; // Get the batchId - let batchId = featureTable.getProperty('BATCH_ID', GL.UNSIGNED_SHORT, 1, i); + let batchId = featureTable.getProperty('BATCH_ID', GL.UNSIGNED_SHORT, 1, i, scratch4); if (batchId === undefined) { // If BATCH_ID semantic is undefined, batchId is just the instance number batchId = i; diff --git a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-point-cloud.ts b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-point-cloud.ts index 5e22f6c921..e554287967 100644 --- a/modules/3d-tiles/src/lib/parsers/parse-3d-tile-point-cloud.ts +++ b/modules/3d-tiles/src/lib/parsers/parse-3d-tile-point-cloud.ts @@ -14,13 +14,14 @@ import {normalize3DTileNormalAttribute} from './helpers/normalize-3d-tile-normal import {normalize3DTilePositionAttribute} from './helpers/normalize-3d-tile-positions'; import {Tiles3DLoaderOptions} from '../../tiles-3d-loader'; import {LoaderContext} from '@loaders.gl/loader-utils'; +import {Tiles3DTileContent} from '../../types'; export async function parsePointCloud3DTile( - tile, + tile: Tiles3DTileContent, arrayBuffer: ArrayBuffer, byteOffset: number, - options: Tiles3DLoaderOptions, - context: LoaderContext + options?: Tiles3DLoaderOptions, + context?: LoaderContext ): Promise { byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset); byteOffset = parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset); @@ -39,7 +40,7 @@ export async function parsePointCloud3DTile( return byteOffset; } -function initializeTile(tile): void { +function initializeTile(tile: Tiles3DTileContent): void { // Initialize point cloud tile defaults tile.attributes = { positions: null, @@ -53,7 +54,7 @@ function initializeTile(tile): void { tile.isOctEncoded16P = false; } -function parsePointCloudTables(tile): { +function parsePointCloudTables(tile: Tiles3DTileContent): { featureTable: Tile3DFeatureTable; batchTable: Tile3DBatchTable | null; } { @@ -77,10 +78,16 @@ function parsePointCloudTables(tile): { } function parsePositions( - tile, + tile: Tiles3DTileContent, featureTable: Tile3DFeatureTable, - options: Tiles3DLoaderOptions + options: Tiles3DLoaderOptions | undefined ): void { + tile.attributes = tile.attributes || { + positions: null, + colors: null, + normals: null, + batchIds: null + }; if (!tile.attributes.positions) { if (featureTable.hasProperty('POSITION')) { tile.attributes.positions = featureTable.getPropertyArray('POSITION', GL.FLOAT, 3); @@ -117,7 +124,17 @@ function parsePositions( } } -function parseColors(tile, featureTable: Tile3DFeatureTable, batchTable: Tile3DBatchTable): void { +function parseColors( + tile: Tiles3DTileContent, + featureTable: Tile3DFeatureTable, + batchTable: Tile3DBatchTable +): void { + tile.attributes = tile.attributes || { + positions: null, + colors: null, + normals: null, + batchIds: null + }; if (!tile.attributes.colors) { let colors = null; if (featureTable.hasProperty('RGBA')) { @@ -138,7 +155,13 @@ function parseColors(tile, featureTable: Tile3DFeatureTable, batchTable: Tile3DB } } -function parseNormals(tile, featureTable: Tile3DFeatureTable): void { +function parseNormals(tile: Tiles3DTileContent, featureTable: Tile3DFeatureTable): void { + tile.attributes = tile.attributes || { + positions: null, + colors: null, + normals: null, + batchIds: null + }; if (!tile.attributes.normals) { let normals = null; if (featureTable.hasProperty('NORMAL')) { @@ -152,7 +175,10 @@ function parseNormals(tile, featureTable: Tile3DFeatureTable): void { } } -function parseBatchIds(tile, featureTable: Tile3DFeatureTable): Tile3DBatchTable | null { +function parseBatchIds( + tile: Tiles3DTileContent, + featureTable: Tile3DFeatureTable +): Tile3DBatchTable | null { let batchTable: Tile3DBatchTable | null = null; if (!tile.batchIds && featureTable.hasProperty('BATCH_ID')) { tile.batchIds = featureTable.getPropertyArray('BATCH_ID', GL.UNSIGNED_SHORT, 1); @@ -171,11 +197,11 @@ function parseBatchIds(tile, featureTable: Tile3DFeatureTable): Tile3DBatchTable // eslint-disable-next-line complexity async function parseDraco( - tile, + tile: Tiles3DTileContent, featureTable: Tile3DFeatureTable, batchTable, - options: Tiles3DLoaderOptions, - context: LoaderContext + options?: Tiles3DLoaderOptions, + context?: LoaderContext ) { let dracoBuffer; let dracoFeatureTableProperties; @@ -197,7 +223,10 @@ async function parseDraco( throw new Error('Draco properties, byteOffset, and byteLength must be defined'); } - dracoBuffer = tile.featureTableBinary.slice(dracoByteOffset, dracoByteOffset + dracoByteLength); + dracoBuffer = (tile.featureTableBinary || []).slice( + dracoByteOffset, + dracoByteOffset + dracoByteLength + ); tile.hasPositions = Number.isFinite(dracoFeatureTableProperties.POSITION); tile.hasColors = @@ -225,16 +254,19 @@ async function parseDraco( // eslint-disable-next-line complexity, max-statements export async function loadDraco( - tile, + tile: Tiles3DTileContent, dracoData, - options: Tiles3DLoaderOptions, - context: LoaderContext -) { + options?: Tiles3DLoaderOptions, + context?: LoaderContext +): Promise { + if (!context) { + return; + } const {parse} = context; const dracoOptions = { ...options, draco: { - ...options.draco, + ...options?.draco, extraAttributes: dracoData.batchTableProperties || {} } }; diff --git a/modules/3d-tiles/src/lib/parsers/parse-3d-tile.ts b/modules/3d-tiles/src/lib/parsers/parse-3d-tile.ts index afbc4564fd..ba3f65f518 100644 --- a/modules/3d-tiles/src/lib/parsers/parse-3d-tile.ts +++ b/modules/3d-tiles/src/lib/parsers/parse-3d-tile.ts @@ -9,15 +9,21 @@ import {parseBatchedModel3DTile} from './parse-3d-tile-batched-model'; import {parseInstancedModel3DTile} from './parse-3d-tile-instanced-model'; import {parseComposite3DTile} from './parse-3d-tile-composite'; import {parseGltf3DTile} from './parse-3d-tile-gltf'; +import {LoaderContext} from '@loaders.gl/loader-utils'; +import {Tiles3DLoaderOptions} from '../../tiles-3d-loader'; +import {Tiles3DTileContent} from '../../types'; // Extracts -export async function parse3DTile(arrayBuffer, byteOffset = 0, options, context, tile = {}) { - // @ts-expect-error +export async function parse3DTile( + arrayBuffer: ArrayBuffer, + byteOffset = 0, + options: Tiles3DLoaderOptions | undefined, + context: LoaderContext | undefined, + tile: Tiles3DTileContent = {} +) { tile.byteOffset = byteOffset; - // @ts-expect-error tile.type = getMagicString(arrayBuffer, byteOffset); - // @ts-expect-error switch (tile.type) { case TILE3D_TYPE.COMPOSITE: // Note: We pass this function as argument so that embedded tiles can be parsed recursively @@ -43,7 +49,6 @@ export async function parse3DTile(arrayBuffer, byteOffset = 0, options, context, return await parsePointCloud3DTile(tile, arrayBuffer, byteOffset, options, context); default: - // @ts-expect-error throw new Error(`3DTileLoader: unknown type ${tile.type}`); // eslint-disable-line } } diff --git a/modules/3d-tiles/src/types.ts b/modules/3d-tiles/src/types.ts index 75abfddcea..f250af3656 100644 --- a/modules/3d-tiles/src/types.ts +++ b/modules/3d-tiles/src/types.ts @@ -100,7 +100,7 @@ export type Tiles3DTilesetJSONPostprocessed = Omit & */ export type Tiles3DTileJSON = { /** A bounding volume that encloses a tile or its content. */ - boundingVolume: BoundingVolume; + boundingVolume: Tile3DBoundingVolume; /** A bounding volume that encloses a tile or its content. */ viewerRequestVolume?: object; /** The error, in meters, introduced if this tile is rendered and its children are not. At runtime, the geometric error is used to compute screen space error (SSE), i.e., the error measured in pixels. */ @@ -155,7 +155,7 @@ export type Tiles3DTileContentJSON = { /** url doesn't allign the spec but we support it same way as uri */ url?: string; /** A bounding volume that encloses a tile or its content. At least one bounding volume property is required. Bounding volumes include box, region, or sphere. */ - boundingVolume?: BoundingVolume; + boundingVolume?: Tile3DBoundingVolume; /** Dictionary object with extension-specific objects. */ extensions?: object; /** Application-specific data. */ @@ -165,7 +165,7 @@ export type Tiles3DTileContentJSON = { /** A bounding volume that encloses a tile or its content. * https://github.com/CesiumGS/3d-tiles/tree/main/specification#bounding-volume */ -export type BoundingVolume = { +export type Tile3DBoundingVolume = { /** An array of 12 numbers that define an oriented bounding box. The first three elements define the x, y, and z values for the center of the box. * The next three elements (with indices 3, 4, and 5) define the x axis direction and half-length. The next three elements (indices 6, 7, and 8) define * the y axis direction and half-length. The last three elements (indices 9, 10, and 11) define the z axis direction and half-length. */ @@ -197,6 +197,97 @@ export type TilesetProperty = { extras?: any; }; +export type Tiles3DTileContent = { + /** Common properties */ + byteOffset?: number; + type?: string; + featureIds?: null; + + /** 3DTile header */ + magic?: number; + version?: number; + byteLength?: number; + + /** 3DTile tables header */ + header?: { + featureTableJsonByteLength?: number; + featureTableBinaryByteLength?: number; + batchTableJsonByteLength?: number; + batchTableBinaryByteLength?: number; + batchLength?: number; + }; + + /** 3DTile tables */ + featureTableJson?: + | { + BATCH_LENGTH?: number; + } + | Record; + featureTableBinary?: Uint8Array; + batchTableJson?: Record; + batchTableBinary?: Uint8Array; + rtcCenter?: number[]; + + /** 3DTile glTF */ + gltfArrayBuffer?: ArrayBuffer; + gltfByteOffset?: number; + gltfByteLength?: number; + rotateYtoZ?: boolean; + gltfUpAxis?: 'x' | 'X' | 'y' | 'Y' | 'z' | 'Z'; + gltfUrl?: string; + gpuMemoryUsageInBytes?: number; + gltf?: GLTFPostprocessed; + + /** For Composite tiles */ + tilesLength?: number; + tiles?: Tiles3DTileContent[]; + + /** For Instances model and Pointcloud tiles */ + featuresLength?: number; + + /** For Instanced model tiles */ + gltfFormat?: number; + eastNorthUp?: boolean; + normalUp?: number[]; + normalRight?: number[]; + hasCustomOrientation?: boolean; + octNormalUp?: number[]; + octNormalRight?: number[]; + instances?: { + modelMatrix: Matrix4; + batchId: number; + }[]; + + /** For Pointcloud tiles */ + attributes?: { + positions: null | number[]; + colors: + | null + | number[] + | {type: number; value: Uint8ClampedArray; size: number; normalized: boolean}; + normals: null | number[] | {type: number; size: number; value: Float32Array}; + batchIds: null | number[]; + }; + constantRGBA?: number[]; + isQuantized?: boolean; + isTranslucent?: boolean; + isRGB565?: boolean; + isOctEncoded16P?: boolean; + pointsLength?: number; + pointCount?: number; + batchIds?: number[]; + hasPositions?: boolean; + hasColors?: boolean; + hasNormals?: boolean; + hasBatchIds?: boolean; + quantizedVolumeScale?: Vector3; + quantizedVolumeOffset?: Vector3; + quantizedRange?: number; + isQuantizedDraco?: boolean; + octEncodedRange?: number; + isOctEncodedDraco?: boolean; +}; + /** * 3DTILES_implicit_tiling types * Spec - https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_implicit_tiling#subtree-file-format diff --git a/modules/3d-tiles/test/lib/parsers/point-cloud-3d-tile.spec.ts b/modules/3d-tiles/test/lib/parsers/point-cloud-3d-tile.spec.ts index 40ccf32485..d35c2c9837 100644 --- a/modules/3d-tiles/test/lib/parsers/point-cloud-3d-tile.spec.ts +++ b/modules/3d-tiles/test/lib/parsers/point-cloud-3d-tile.spec.ts @@ -99,7 +99,7 @@ test('loadDraco# Pass options to draco loader properly', async (t) => { worker: true, reuseWorkers: true }; - const tile = null; + const tile = {}; const context: LoaderContext = { parse: async (buffer, loader, resultOptions) => { t.deepEqual(resultOptions, resultObject); From bcf3707ed39268d7f629bebafa65ccc09e62e1ca Mon Sep 17 00:00:00 2001 From: Viktor Belomestnov Date: Thu, 15 Jun 2023 12:50:58 +0200 Subject: [PATCH 2/3] add types to helpers --- .../parsers/helpers/normalize-3d-tile-colors.ts | 14 ++++++++------ .../parsers/helpers/normalize-3d-tile-normals.ts | 2 +- .../helpers/normalize-3d-tile-colors.spec.ts | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts index e6632fea03..a0fddb0939 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-colors.ts @@ -1,17 +1,19 @@ +import {Tile3DBatchTable} from '@loaders.gl/3d-tiles'; import {decodeRGB565, GL} from '@loaders.gl/math'; +import {Tiles3DTileContent} from 'modules/3d-tiles/src/types'; /* eslint-disable complexity*/ export function normalize3DTileColorAttribute( - tile, - colors, - batchTable? + tile: Tiles3DTileContent, + colors: Uint8ClampedArray | null, + batchTable?: Tile3DBatchTable ): {type: number; value: Uint8ClampedArray; size: number; normalized: boolean} | null { // no colors defined if (!colors && (!tile || !tile.batchIds || !batchTable)) { return null; } - const {batchIds, isRGB565, pointCount} = tile; + const {batchIds, isRGB565, pointCount = 0} = tile; // Batch table, look up colors in table if (batchIds && batchTable) { const colorArray = new Uint8ClampedArray(pointCount * 3); @@ -33,7 +35,7 @@ export function normalize3DTileColorAttribute( } // RGB565 case, convert to RGB - if (isRGB565) { + if (colors && isRGB565) { const colorArray = new Uint8ClampedArray(pointCount * 3); for (let i = 0; i < pointCount; i++) { const color = decodeRGB565(colors[i]); @@ -62,7 +64,7 @@ export function normalize3DTileColorAttribute( // DEFAULT: RGBA case return { type: GL.UNSIGNED_BYTE, - value: colors, + value: colors || new Uint8ClampedArray(), size: 4, normalized: true }; diff --git a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts index cfb33b27dc..b5d689e58f 100644 --- a/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts +++ b/modules/3d-tiles/src/lib/parsers/helpers/normalize-3d-tile-normals.ts @@ -6,7 +6,7 @@ const scratchNormal = new Vector3(); export function normalize3DTileNormalAttribute( tile: Tiles3DTileContent, - normals + normals: Float32Array | null ): {type: number; size: number; value: Float32Array} | null { if (!normals) { return null; diff --git a/modules/3d-tiles/test/lib/parsers/helpers/normalize-3d-tile-colors.spec.ts b/modules/3d-tiles/test/lib/parsers/helpers/normalize-3d-tile-colors.spec.ts index 32c74e2cb8..540338cd38 100644 --- a/modules/3d-tiles/test/lib/parsers/helpers/normalize-3d-tile-colors.spec.ts +++ b/modules/3d-tiles/test/lib/parsers/helpers/normalize-3d-tile-colors.spec.ts @@ -5,7 +5,7 @@ import {normalize3DTileColorAttribute} from '../../../../src/lib/parsers/helpers const TEST_CASES = [ { - tile: null, + tile: {}, colors: null, batchTable: null, expected: null, From f8b099ba9ff5bfb323d5aa26832c865eabc33597 Mon Sep 17 00:00:00 2001 From: Viktor Belomestnov Date: Fri, 16 Jun 2023 18:13:47 +0200 Subject: [PATCH 3/3] chore(tile-converter): i3s-converter - mitigate "tiles" module dependency --- modules/3d-tiles/src/index.ts | 4 + modules/3d-tiles/src/types.ts | 6 +- .../helpers/coordinate-converter.ts | 21 +- .../helpers/geometry-converter.ts | 32 ++- .../i3s-converter/helpers/gltf-attributes.ts | 105 +++++-- .../i3s-converter/helpers/load-3d-tiles.ts | 67 +++++ .../src/i3s-converter/i3s-converter.ts | 259 +++++++++++------- .../tile-converter/src/i3s-converter/types.ts | 13 + .../src/lib/utils/lod-conversion-utils.ts | 8 +- .../helpers/geometry-converter.spec.js | 99 +++---- .../helpers/gltf-attributes.spec.js | 60 ++-- 11 files changed, 467 insertions(+), 207 deletions(-) create mode 100644 modules/tile-converter/src/i3s-converter/helpers/load-3d-tiles.ts diff --git a/modules/3d-tiles/src/index.ts b/modules/3d-tiles/src/index.ts index 060c7031bd..69234e1886 100644 --- a/modules/3d-tiles/src/index.ts +++ b/modules/3d-tiles/src/index.ts @@ -16,7 +16,11 @@ export {getIonTilesetMetadata as _getIonTilesetMetadata} from './lib/ion/ion'; export type { FeatureTableJson, B3DMContent, + Tile3DBoundingVolume, Tiles3DTileJSON, + Tiles3DTileJSONPostprocessed, Tiles3DTilesetJSON, + Tiles3DTilesetJSONPostprocessed, + Tiles3DTileContent, ImplicitTilingExensionData } from './types'; diff --git a/modules/3d-tiles/src/types.ts b/modules/3d-tiles/src/types.ts index f250af3656..c9d0aa4b81 100644 --- a/modules/3d-tiles/src/types.ts +++ b/modules/3d-tiles/src/types.ts @@ -54,6 +54,8 @@ export type Tiles3DTilesetJSON = { extensions?: object; /** Application-specific data. */ extras?: any; + /** Not mentioned in 1.0 spec but some tilesets contain this option */ + gltfUpAxis?: string; }; /** A dictionary object of metadata about per-feature properties. */ properties?: Record; @@ -128,7 +130,7 @@ export type Tiles3DTileJSON = { implicitTiling?: ImplicitTilingData; }; -export type Tiles3DTileJSONPostprocessed = Omit & { +export type Tiles3DTileJSONPostprocessed = Omit & { /** Unique ID */ id?: string; /** Content full URL */ @@ -146,6 +148,8 @@ export type Tiles3DTileJSONPostprocessed = Omit & { * The default is to inherit from the parent tile. */ refine?: TILE_REFINEMENT | string; + /** An array of objects that define child tiles. */ + children: Tiles3DTileJSONPostprocessed[]; }; /** Metadata about the tile's content and a link to the content. */ diff --git a/modules/tile-converter/src/i3s-converter/helpers/coordinate-converter.ts b/modules/tile-converter/src/i3s-converter/helpers/coordinate-converter.ts index 02aa0cf7b1..360417f695 100644 --- a/modules/tile-converter/src/i3s-converter/helpers/coordinate-converter.ts +++ b/modules/tile-converter/src/i3s-converter/helpers/coordinate-converter.ts @@ -8,34 +8,35 @@ import { makeBoundingSphereFromPoints, BoundingSphere } from '@math.gl/culling'; -import {Tile3D} from '@loaders.gl/tiles'; import {Geoid} from '@math.gl/geoid'; /** * Create bounding volumes object from tile and geoid height model. - * @param tile - * @param geoidHeightModel + * @param sourceBoundingVolume - initialized bounding volume of the source tile + * @param geoidHeightModel - instance of Geoid class that converts elevation from geoidal to ellipsoidal and back * @returns - Bounding volumes object */ -export function createBoundingVolumes(tile: Tile3D, geoidHeightModel: Geoid): BoundingVolumes { +export function createBoundingVolumes( + sourceBoundingVolume: OrientedBoundingBox | BoundingSphere, + geoidHeightModel: Geoid +): BoundingVolumes { let radius; let halfSize; let quaternion; - const boundingVolume = tile.boundingVolume; const cartographicCenter = Ellipsoid.WGS84.cartesianToCartographic( - boundingVolume.center, + sourceBoundingVolume.center, new Vector3() ); cartographicCenter[2] = cartographicCenter[2] - geoidHeightModel.getHeight(cartographicCenter[1], cartographicCenter[0]); - if (boundingVolume instanceof OrientedBoundingBox) { - halfSize = boundingVolume.halfSize; + if (sourceBoundingVolume instanceof OrientedBoundingBox) { + halfSize = sourceBoundingVolume.halfSize; radius = new Vector3(halfSize[0], halfSize[1], halfSize[2]).len(); - quaternion = boundingVolume.quaternion; + quaternion = sourceBoundingVolume.quaternion; } else { - radius = tile.boundingVolume.radius; + radius = sourceBoundingVolume.radius; halfSize = [radius, radius, radius]; quaternion = new Quaternion() .fromMatrix3(new Matrix3([halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]])) diff --git a/modules/tile-converter/src/i3s-converter/helpers/geometry-converter.ts b/modules/tile-converter/src/i3s-converter/helpers/geometry-converter.ts index 7f210dc2fd..d2e5edf314 100644 --- a/modules/tile-converter/src/i3s-converter/helpers/geometry-converter.ts +++ b/modules/tile-converter/src/i3s-converter/helpers/geometry-converter.ts @@ -1,4 +1,4 @@ -import type {B3DMContent, FeatureTableJson} from '@loaders.gl/3d-tiles'; +import type {FeatureTableJson, Tiles3DTileContent} from '@loaders.gl/3d-tiles'; import type { GLTF_EXT_feature_metadata, GLTF_EXT_mesh_features, @@ -52,6 +52,7 @@ import {GL} from '@loaders.gl/math'; */ import type {TypedArrayConstructor} from '../types'; import {generateSyntheticIndices} from '../../lib/utils/geometry-utils'; +import {BoundingSphere, OrientedBoundingBox} from '@math.gl/culling'; // Spec - https://github.com/Esri/i3s-spec/blob/master/docs/1.7/pbrMetallicRoughness.cmn.md const DEFAULT_ROUGHNESS_FACTOR = 1; @@ -81,6 +82,9 @@ let scratchVector = new Vector3(); * Convert binary data from b3dm file to i3s resources * * @param tileContent - 3d tile content + * @param tileTransform - transformation matrix of the tile, calculated recursively multiplying + * transform of all parent tiles and transform of the current tile + * @param tileBoundingVolume - initialized bounding volume of the source tile * @param addNodeToNodePage - function to add new node to node pages * @param propertyTable - batch table (corresponding to feature attributes data) * @param featuresHashArray - hash array of features that is needed to not to mix up same features in parent and child nodes @@ -93,7 +97,9 @@ let scratchVector = new Vector3(); * @returns Array of node resources to create one or more i3s nodes */ export default async function convertB3dmToI3sGeometry( - tileContent: B3DMContent, + tileContent: Tiles3DTileContent, + tileTransform: Matrix4, + tileBoundingVolume: OrientedBoundingBox | BoundingSphere, addNodeToNodePage: () => Promise, propertyTable: FeatureTableJson | null, featuresHashArray: string[], @@ -110,7 +116,11 @@ export default async function convertB3dmToI3sGeometry( shouldMergeMaterials ); - const dataForAttributesConversion = prepareDataForAttributesConversion(tileContent); + const dataForAttributesConversion = prepareDataForAttributesConversion( + tileContent, + tileTransform, + tileBoundingVolume + ); const convertedAttributesMap: Map = await convertAttributes( dataForAttributesConversion, materialAndTextureList, @@ -198,7 +208,7 @@ function _generateBoundingVolumesFromGeometry( * @param params.convertedAttributes - Converted geometry attributes * @param params.material - I3S PBR-like material definition * @param params.texture - texture content - * @param params.tileContent - B3DM decoded content + * @param params.tileContent - 3DTiles decoded content * @param params.nodeId - new node ID * @param params.featuresHashArray - hash array of features that is needed to not to mix up same features in parent and child nodes * @param params.propertyTable - batch table (corresponding to feature attributes data) @@ -222,7 +232,7 @@ async function _makeNodeResources({ convertedAttributes: ConvertedAttributes; material: I3SMaterialDefinition; texture?: {}; - tileContent: B3DMContent; + tileContent: Tiles3DTileContent; nodeId: number; featuresHashArray: string[]; propertyTable: FeatureTableJson | null; @@ -1545,10 +1555,14 @@ function generateFeatureIndexAttribute( /** * Find property table in tile * For example it can be batchTable for b3dm files or property table in gLTF extension. - * @param sourceTile + * @param tileContent - 3DTiles tile content * @return batch table from b3dm / feature properties from EXT_FEATURE_METADATA */ -export function getPropertyTable(tileContent: B3DMContent): FeatureTableJson | null { +export function getPropertyTable(tileContent: Tiles3DTileContent | null): FeatureTableJson | null { + if (!tileContent) { + return null; + } + const batchTableJson = tileContent?.batchTableJson; if (batchTableJson) { @@ -1572,10 +1586,10 @@ export function getPropertyTable(tileContent: B3DMContent): FeatureTableJson | n /** * Check extensions which can be with property table inside. - * @param sourceTile + * @param tileContent - 3DTiles tile content */ function getPropertyTableExtension( - tileContent: B3DMContent + tileContent: Tiles3DTileContent ): GLTF_EXT_feature_metadata | GLTF_EXT_mesh_features { const extensionsWithPropertyTables = [EXT_FEATURE_METADATA, EXT_MESH_FEATURES]; const extensionsUsed = tileContent?.gltf?.extensionsUsed; diff --git a/modules/tile-converter/src/i3s-converter/helpers/gltf-attributes.ts b/modules/tile-converter/src/i3s-converter/helpers/gltf-attributes.ts index 8efc8a79d7..138e5a7186 100644 --- a/modules/tile-converter/src/i3s-converter/helpers/gltf-attributes.ts +++ b/modules/tile-converter/src/i3s-converter/helpers/gltf-attributes.ts @@ -1,33 +1,27 @@ -import type {B3DMContent} from '@loaders.gl/3d-tiles'; +import type {Tiles3DTileContent} from '@loaders.gl/3d-tiles'; import type {GLTFAccessorPostprocessed, GLTFNodePostprocessed} from '@loaders.gl/gltf'; import type {B3DMAttributesData} from '../../i3s-attributes-worker'; +import {Matrix4, Vector3} from '@math.gl/core'; +import {BoundingSphere, OrientedBoundingBox} from '@math.gl/culling'; +import {Ellipsoid} from '@math.gl/geospatial'; type AttributesObject = { [k: string]: GLTFAccessorPostprocessed; }; -/** - * Keep only values for B3DM attributes to pass data to worker thread. - * @param attributes - */ -function getB3DMAttributesWithoutBufferView(attributes: AttributesObject): AttributesObject { - const attributesWithoutBufferView = {}; - - for (const attributeName in attributes) { - attributesWithoutBufferView[attributeName] = { - value: attributes[attributeName].value - }; - } - - return attributesWithoutBufferView; -} - /** * Prepare attributes for conversion to avoid binary data breaking in worker thread. - * @param tileContent + * @param tileContent - 3DTiles tile content + * @param tileTransform - transformation matrix of the tile, calculated recursively multiplying + * transform of all parent tiles and transform of the current tile + * @param boundingVolume - initialized bounding volume of the source tile * @returns */ -export function prepareDataForAttributesConversion(tileContent: B3DMContent): B3DMAttributesData { +export function prepareDataForAttributesConversion( + tileContent: Tiles3DTileContent, + tileTransform: Matrix4, + boundingVolume: OrientedBoundingBox | BoundingSphere +): B3DMAttributesData { let nodes = tileContent.gltf?.scene?.nodes || tileContent.gltf?.scenes?.[0]?.nodes || @@ -56,8 +50,11 @@ export function prepareDataForAttributesConversion(tileContent: B3DMContent): B3 prepareNodes(nodes); - const cartographicOrigin = tileContent.cartographicOrigin; - const cartesianModelMatrix = tileContent.cartesianModelMatrix; + const {cartographicOrigin, modelMatrix: cartesianModelMatrix} = calculateTransformProps( + tileContent, + tileTransform, + boundingVolume + ); return { nodes, @@ -67,6 +64,72 @@ export function prepareDataForAttributesConversion(tileContent: B3DMContent): B3 }; } +/** + * Keep only values for glTF attributes to pass data to worker thread. + * @param attributes - geometry attributes + */ +function getB3DMAttributesWithoutBufferView(attributes: AttributesObject): AttributesObject { + const attributesWithoutBufferView = {}; + + for (const attributeName in attributes) { + attributesWithoutBufferView[attributeName] = { + value: attributes[attributeName].value + }; + } + + return attributesWithoutBufferView; +} + +/** + * Calculate transformation properties to transform vertex attributes (POSITION, NORMAL, etc.) + * from METER_OFFSET coorditantes to LNGLAT_OFFSET coordinates + * @param tileContent - 3DTiles tile content + * @param tileTransform - transformation matrix of the tile, calculated recursively multiplying + * transform of all parent tiles and transform of the current tile + * @param boundingVolume - initialized bounding volume of the source tile + * @returns modelMatrix - transformation matrix to transform coordinates to cartographic coordinates + * cartographicOrigin - tile origin coordinates to calculate offsets + */ +export function calculateTransformProps( + tileContent: Tiles3DTileContent, + tileTransform: Matrix4, + boundingVolume: OrientedBoundingBox | BoundingSphere +): {modelMatrix: Matrix4; cartographicOrigin: Vector3} { + const {rtcCenter, gltfUpAxis} = tileContent; + const {center} = boundingVolume; + + let modelMatrix = new Matrix4(tileTransform); + + // Translate if appropriate + if (rtcCenter) { + modelMatrix.translate(rtcCenter); + } + + // glTF models need to be rotated from Y to Z up + // https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification#y-up-to-z-up + switch (gltfUpAxis) { + case 'Z': + break; + case 'Y': + const rotationY = new Matrix4().rotateX(Math.PI / 2); + modelMatrix = modelMatrix.multiplyRight(rotationY); + break; + case 'X': + const rotationX = new Matrix4().rotateY(-Math.PI / 2); + modelMatrix = modelMatrix.multiplyRight(rotationX); + break; + default: + break; + } + + const cartesianOrigin = new Vector3(center); + const cartographicOrigin = Ellipsoid.WGS84.cartesianToCartographic( + cartesianOrigin, + new Vector3() + ); + return {modelMatrix, cartographicOrigin}; +} + /** * Traverse all nodes to replace all sensible data with copy to avoid data corruption in worker. * @param nodes 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 new file mode 100644 index 0000000000..96b785a221 --- /dev/null +++ b/modules/tile-converter/src/i3s-converter/helpers/load-3d-tiles.ts @@ -0,0 +1,67 @@ +import type { + 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 + * @returns nothing + */ +export const loadNestedTileset = async ( + sourceTileset: Tiles3DTilesetJSONPostprocessed | null, + sourceTile: Tiles3DTileJSONPostprocessed, + globalLoadOptions: Tiles3DLoadOptions +): Promise => { + const isTileset = sourceTile.type === 'json'; + if (!sourceTileset || !sourceTile.contentUrl || !isTileset) { + return; + } + + const loadOptions = { + ...globalLoadOptions, + [sourceTileset.loader.id]: { + isTileset, + assetGltfUpAxis: (sourceTileset.asset && sourceTileset.asset.gltfUpAxis) || 'Y' + } + }; + const tileContent = await load(sourceTile.contentUrl, sourceTileset.loader, loadOptions); + + if (tileContent.root) { + sourceTile.children = [tileContent.root]; + } +}; + +/** + * 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 + * @returns - 3DTiles tile content or null + */ +export const loadTile3DContent = async ( + sourceTileset: Tiles3DTilesetJSONPostprocessed | null, + sourceTile: Tiles3DTileJSONPostprocessed, + globalLoadOptions: Tiles3DLoadOptions +): Promise => { + const isTileset = sourceTile.type === 'json'; + if (!sourceTileset || !sourceTile.contentUrl || isTileset) { + return null; + } + + const loadOptions = { + ...globalLoadOptions, + [sourceTileset.loader.id]: { + isTileset, + assetGltfUpAxis: (sourceTileset.asset && sourceTileset.asset.gltfUpAxis) || 'Y' + } + }; + const tileContent = await load(sourceTile.contentUrl, sourceTileset.loader, loadOptions); + + return tileContent; +}; diff --git a/modules/tile-converter/src/i3s-converter/i3s-converter.ts b/modules/tile-converter/src/i3s-converter/i3s-converter.ts index b067131944..96e7793cab 100644 --- a/modules/tile-converter/src/i3s-converter/i3s-converter.ts +++ b/modules/tile-converter/src/i3s-converter/i3s-converter.ts @@ -1,7 +1,11 @@ // loaders.gl, MIT license -import type {Tileset3DProps} from '@loaders.gl/tiles'; -import type {FeatureTableJson} from '@loaders.gl/3d-tiles'; +import type { + FeatureTableJson, + Tiles3DTileContent, + Tiles3DTileJSONPostprocessed, + Tiles3DTilesetJSONPostprocessed +} from '@loaders.gl/3d-tiles'; import type {WriteQueueItem} from '../lib/utils/write-queue'; import type { SceneLayer3D, @@ -10,7 +14,6 @@ import type { NodeInPage } from '@loaders.gl/i3s'; import {load, encode, fetchFile, getLoaderOptions, isBrowser} from '@loaders.gl/core'; -import {Tileset3D} from '@loaders.gl/tiles'; import {CesiumIonLoader, Tiles3DLoader} from '@loaders.gl/3d-tiles'; import {Geoid} from '@math.gl/geoid'; import {join} from 'path'; @@ -41,15 +44,12 @@ import {LAYERS as layersTemplate} from './json-templates/layers'; import {GEOMETRY_DEFINITION as geometryDefinitionTemlate} from './json-templates/geometry-definitions'; import {SHARED_RESOURCES as sharedResourcesTemplate} from './json-templates/shared-resources'; import {validateNodeBoundingVolumes} from './helpers/node-debug'; -// loaders.gl, MIT license - -import {Tile3D} from '@loaders.gl/tiles'; import {KTX2BasisWriterWorker} from '@loaders.gl/textures'; import {LoaderWithParser} from '@loaders.gl/loader-utils'; import {I3SMaterialDefinition, TextureSetDefinitionFormats} from '@loaders.gl/i3s/src/types'; import {ImageWriter} from '@loaders.gl/images'; import {GLTFImagePostprocessed} from '@loaders.gl/gltf'; -import {I3SConvertedResources, SharedResourcesArrays} from './types'; +import {I3SConvertedResources, SharedResourcesArrays, Tiles3DLoadOptions} from './types'; import {getWorkerURL, WorkerFarm} from '@loaders.gl/worker-utils'; import {DracoWriterWorker} from '@loaders.gl/draco'; import WriteQueue from '../lib/utils/write-queue'; @@ -63,6 +63,10 @@ import { getFieldAttributeType } from './helpers/feature-attributes'; import {NodeIndexDocument} from './helpers/node-index-document'; +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'; const ION_DEFAULT_TOKEN = process.env?.IonToken || // eslint-disable-line @@ -96,7 +100,21 @@ export default class I3SConverter { boundingVolumeWarnings?: string[] = []; conversionStartTime: [number, number] = [0, 0]; refreshTokenTime: [number, number] = [0, 0]; - sourceTileset: Tileset3D | null = null; + sourceTileset: Tiles3DTilesetJSONPostprocessed | null = null; + loadOptions: Tiles3DLoadOptions = { + _nodeWorkers: true, + reuseWorkers: true, + basis: { + format: 'rgba32', + // We need to load local fs workers because nodejs can't load workers from the Internet + workerUrl: './modules/textures/dist/basis-worker-node.js' + }, + // 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 + } + }; geoidHeightModel: Geoid | null = null; Loader: LoaderWithParser = Tiles3DLoader; generateTextures: boolean; @@ -160,7 +178,7 @@ export default class I3SConverter { generateTextures?: boolean; generateBoundingVolumes?: boolean; instantNodeWriting?: boolean; - }): Promise { + }): Promise { if (isBrowser) { console.log(BROWSER_ERROR_MESSAGE); return BROWSER_ERROR_MESSAGE; @@ -214,34 +232,14 @@ export default class I3SConverter { try { const preloadOptions = await this._fetchPreloadOptions(); - const tilesetOptions: Tileset3DProps = { - loadOptions: { - _nodeWorkers: true, - reuseWorkers: true, - basis: { - format: 'rgba32', - // We need to load local fs workers because nodejs can't load workers from the Internet - workerUrl: './modules/textures/dist/basis-worker-node.js' - }, - // 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'} - } - }; if (preloadOptions.headers) { - tilesetOptions.loadOptions!.fetch = {headers: preloadOptions.headers}; + this.loadOptions.fetch = {headers: preloadOptions.headers}; } - Object.assign(tilesetOptions, preloadOptions); - const sourceTilesetJson = await load(inputUrl, this.Loader, tilesetOptions.loadOptions); - // console.log(tilesetJson); // eslint-disable-line - this.sourceTileset = new Tileset3D(sourceTilesetJson, tilesetOptions); - - await this._createAndSaveTileset( - outputPath, - tilesetName, - sourceTilesetJson?.root?.boundingVolume?.region - ); + this.sourceTileset = await load(inputUrl, this.Loader, this.loadOptions); + + await this._createAndSaveTileset(outputPath, tilesetName); await this._finishConversion({slpk: Boolean(slpk), outputPath, tilesetName}); - return sourceTilesetJson; + return 'success'; } catch (error) { throw error; } finally { @@ -256,11 +254,7 @@ export default class I3SConverter { * @param outputPath - path to save output data * @param tilesetName - new tileset path */ - private async _createAndSaveTileset( - outputPath: string, - tilesetName: string, - boundingVolumeRegion?: number[] - ): Promise { + private async _createAndSaveTileset(outputPath: string, tilesetName: string): Promise { const tilesetPath = join(`${outputPath}`, `${tilesetName}`); // Removing the tilesetPath needed to exclude erroneous files after conversion try { @@ -271,13 +265,24 @@ export default class I3SConverter { this.layers0Path = join(tilesetPath, 'SceneServer', 'layers', '0'); - this._formLayers0(tilesetName, boundingVolumeRegion); - this.materialDefinitions = []; this.materialMap = new Map(); - const sourceRootTile: Tile3D = this.sourceTileset!.root!; - const boundingVolumes = createBoundingVolumes(sourceRootTile, this.geoidHeightModel!); + const sourceRootTile: Tiles3DTileJSONPostprocessed = this.sourceTileset!.root!; + const sourceBoundingVolume = createBoundingVolume( + sourceRootTile.boundingVolume, + new Matrix4(sourceRootTile.transform), + null + ); + + this._formLayers0( + tilesetName, + sourceBoundingVolume, + this.sourceTileset?.root?.boundingVolume?.region + ); + + const boundingVolumes = createBoundingVolumes(sourceBoundingVolume, this.geoidHeightModel!); + await this.nodePages.push({ index: 0, lodThreshold: 0, @@ -317,12 +322,19 @@ export default class I3SConverter { /** * Form object of 3DSceneLayer https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DSceneLayer.cmn.md - * @param tilesetName - Name of layer + * @param tilesetName - Name of layer + * @param sourceBoundingVolume - initialized bounding volume of the source root tile + * @param boundingVolumeRegion - region bounding volume of the source root tile */ - private _formLayers0(tilesetName: string, boundingVolumeRegion?: number[]): void { - const fullExtent = convertBoundingVolumeToI3SFullExtent( - this.sourceTileset?.boundingVolume || this.sourceTileset?.root?.boundingVolume - ); + private _formLayers0( + tilesetName: string, + sourceBoundingVolume: OrientedBoundingBox | BoundingSphere, + boundingVolumeRegion?: number[] + ): void { + if (!this.sourceTileset?.root) { + return; + } + const fullExtent = convertBoundingVolumeToI3SFullExtent(sourceBoundingVolume); if (boundingVolumeRegion) { fullExtent.zmin = boundingVolumeRegion[4]; fullExtent.zmax = boundingVolumeRegion[5]; @@ -346,33 +358,6 @@ export default class I3SConverter { this.layers0 = transform(layers0data, layersTemplate()); } - /** - * Form object of 3DSceneLayer https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DSceneLayer.cmn.md - * @param rootNode - 3DNodeIndexDocument of root node https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DNodeIndexDocument.cmn.md - * @param sourceRootTile - Source (3DTile) tile data - */ - private async _convertNodesTree( - rootNode: NodeIndexDocument, - sourceRootTile: Tile3D - ): Promise { - await this.sourceTileset!._loadTile(sourceRootTile); - if (this.isContentSupported(sourceRootTile)) { - const childNodes = await this._createNode(rootNode, sourceRootTile, 0); - for (const childNode of childNodes) { - await childNode.save(); - } - await rootNode.addChildren(childNodes); - } else { - await this._addChildrenWithNeighborsAndWriteFile({ - parentNode: rootNode, - sourceTiles: sourceRootTile.children, - level: 1 - }); - } - await sourceRootTile.unloadContent(); - await rootNode.save(); - } - /** * Write 3DSceneLayer https://github.com/Esri/i3s-spec/blob/master/docs/1.7/3DSceneLayer.cmn.md in file */ @@ -432,16 +417,54 @@ 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 + */ + private async _convertNodesTree( + parentNode: NodeIndexDocument, + 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(); + } + await parentNode.addChildren(childNodes); + } else { + await loadNestedTileset(this.sourceTileset, sourceTile, this.loadOptions); + await this._addChildrenWithNeighborsAndWriteFile({ + parentNode: parentNode, + sourceTiles: sourceTile.children, + parentTransform: transformationMatrix, + level: 1 + }); + } + if (sourceTile.id) { + console.log(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: Tile3D[]; + sourceTiles: Tiles3DTileJSONPostprocessed[]; + parentTransform: Matrix4; level: number; }): Promise { await this._addChildren(data); @@ -453,24 +476,27 @@ export default class I3SConverter { * @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: Tile3D; + sourceTile: Tiles3DTileJSONPostprocessed; + transformationMatrix: Matrix4; level: number; }) { - await this.sourceTileset!._loadTile(sourceTile); + await loadNestedTileset(this.sourceTileset, sourceTile, this.loadOptions); await this._addChildren({ parentNode, sourceTiles: sourceTile.children, + parentTransform: transformationMatrix, level: level + 1 }); - await sourceTile.unloadContent(); } /** @@ -478,18 +504,22 @@ export default class I3SConverter { * @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: Tile3D; + sourceTile: Tiles3DTileJSONPostprocessed; + transformationMatrix: Matrix4; level: number; }) { - const childNodes = await this._createNode(parentNode, sourceTile, level); + const childNodes = await this._createNode(parentNode, sourceTile, transformationMatrix, level); await parentNode.addChildren(childNodes); } @@ -498,22 +528,28 @@ export default class I3SConverter { * @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 */ private async _addChildren(data: { parentNode: NodeIndexDocument; - sourceTiles: Tile3D[]; + sourceTiles: Tiles3DTileJSONPostprocessed[]; + parentTransform: Matrix4; level: number; }): Promise { - const {sourceTiles, parentNode, level} = data; + 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, level}); + await this.convertNestedTileset({parentNode, sourceTile, transformationMatrix, level}); } else { - await this.convertNode({parentNode, sourceTile, level}); + await this.convertNode({parentNode, sourceTile, transformationMatrix, level}); } if (sourceTile.id) { console.log(sourceTile.id); // eslint-disable-line @@ -525,21 +561,29 @@ export default class I3SConverter { * Convert tile to one or more I3S nodes * @param parentNode - 3DNodeIndexDocument of parent node * @param sourceTile - source 3DTile data + * @param transformationMatrix - transformation matrix of the current tile, calculated recursively multiplying + * transform of all parent tiles and transform of the current tile * @param level - tree level */ private async _createNode( parentNode: NodeIndexDocument, - sourceTile: Tile3D, + sourceTile: Tiles3DTileJSONPostprocessed, + transformationMatrix: Matrix4, level: number ): Promise { this._checkAddRefinementTypeForTile(sourceTile); await this._updateTilesetOptions(); - await this.sourceTileset!._loadTile(sourceTile); - let boundingVolumes = createBoundingVolumes(sourceTile, this.geoidHeightModel!); + const tileContent = await loadTile3DContent(this.sourceTileset, sourceTile, this.loadOptions); + const sourceBoundingVolume = createBoundingVolume( + sourceTile.boundingVolume, + transformationMatrix, + null + ); + let boundingVolumes = createBoundingVolumes(sourceBoundingVolume, this.geoidHeightModel!); - const propertyTable = getPropertyTable(sourceTile.content); + const propertyTable = getPropertyTable(tileContent); if (propertyTable && !this.layers0?.attributeStorageInfo?.length) { this._convertPropertyTableToNodeAttributes(propertyTable); @@ -547,6 +591,9 @@ export default class I3SConverter { const resourcesData = await this._convertResources( sourceTile, + transformationMatrix, + sourceBoundingVolume, + tileContent, parentNode.inPageId, propertyTable ); @@ -613,11 +660,10 @@ export default class I3SConverter { nodesInPage.push(nodeInPage); } - sourceTile.unloadContent(); - await this._addChildrenWithNeighborsAndWriteFile({ parentNode: nodes[0], sourceTiles: sourceTile.children, + parentTransform: transformationMatrix, level: level + 1 }); return nodes; @@ -626,16 +672,23 @@ export default class I3SConverter { /** * Convert tile to one or more I3S nodes * @param sourceTile - source tile (3DTile) + * @param transformationMatrix - transformation matrix of the current tile, calculated recursively multiplying + * transform of all parent tiles and transform of the current tile + * @param boundingVolume - initialized bounding volume of the source tile + * @param tileContent - content of the source tile * @param parentId - id of parent node in node pages * @param propertyTable - batch table from b3dm / feature properties from EXT_FEATURE_METADATA * @returns - converted node resources */ private async _convertResources( - sourceTile: Tile3D, + sourceTile: Tiles3DTileJSONPostprocessed, + transformationMatrix: Matrix4, + boundingVolume: OrientedBoundingBox | BoundingSphere, + tileContent: Tiles3DTileContent | null, parentId: number, propertyTable: FeatureTableJson | null ): Promise { - if (!this.isContentSupported(sourceTile)) { + if (!this.isContentSupported(sourceTile) || !tileContent) { return null; } const draftObb = { @@ -644,7 +697,9 @@ export default class I3SConverter { quaternion: [] }; const resourcesData = await convertB3dmToI3sGeometry( - sourceTile.content, + tileContent, + transformationMatrix, + boundingVolume, async () => (await this.nodePages.push({index: 0, obb: draftObb}, parentId)).index, propertyTable, this.featuresHashArray, @@ -676,7 +731,7 @@ export default class I3SConverter { private async _updateNodeInNodePages( maxScreenThresholdSQ: MaxScreenThresholdSQ, boundingVolumes: BoundingVolumes, - sourceTile: Tile3D, + sourceTile: Tiles3DTileJSONPostprocessed, parentId: number, resources: I3SConvertedResources ): Promise { @@ -1096,10 +1151,9 @@ export default class I3SConverter { this.refreshTokenTime = process.hrtime(); const preloadOptions = await this._fetchPreloadOptions(); - this.sourceTileset!.options = {...this.sourceTileset!.options, ...preloadOptions}; if (preloadOptions.headers) { - this.sourceTileset!.loadOptions.fetch = { - ...this.sourceTileset!.loadOptions.fetch, + this.loadOptions.fetch = { + ...this.loadOptions.fetch, headers: preloadOptions.headers }; console.log('Authorization Bearer token has been updated'); // eslint-disable-line no-undef, no-console @@ -1109,7 +1163,7 @@ export default class I3SConverter { /** Do calculations of all tiles and tiles with "ADD" type of refinement. * @param tile */ - private _checkAddRefinementTypeForTile(tile: Tile3D): void { + private _checkAddRefinementTypeForTile(tile: Tiles3DTileJSONPostprocessed): void { const ADD_TILE_REFINEMENT = 1; if (tile.refine === ADD_TILE_REFINEMENT) { @@ -1119,13 +1173,14 @@ export default class I3SConverter { this.refinementCounter.tilesCount += 1; } + /** * Check if the tile's content format is supported by the converter - * @param sourceRootTile + * @param sourceTile * @returns */ - private isContentSupported(sourceRootTile: Tile3D): boolean { - return ['b3dm', 'glTF'].includes(sourceRootTile?.content?.type); + private isContentSupported(sourceTile: Tiles3DTileJSONPostprocessed): boolean { + return ['b3dm', 'glTF', 'scenegraph'].includes(sourceTile.type || ''); } private async loadWorkers(): Promise { diff --git a/modules/tile-converter/src/i3s-converter/types.ts b/modules/tile-converter/src/i3s-converter/types.ts index 5088203c99..0849fed188 100644 --- a/modules/tile-converter/src/i3s-converter/types.ts +++ b/modules/tile-converter/src/i3s-converter/types.ts @@ -163,3 +163,16 @@ export type TypedArrayConstructor = | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; + +export type Tiles3DLoadOptions = { + _nodeWorkers: boolean; + reuseWorkers: boolean; + basis: { + format: string; + workerUrl: string; + }; + draco: {workerUrl: string}; + fetch: { + headers: any; + }; +}; diff --git a/modules/tile-converter/src/lib/utils/lod-conversion-utils.ts b/modules/tile-converter/src/lib/utils/lod-conversion-utils.ts index 8b9b39e7df..e817f2578d 100644 --- a/modules/tile-converter/src/lib/utils/lod-conversion-utils.ts +++ b/modules/tile-converter/src/lib/utils/lod-conversion-utils.ts @@ -1,3 +1,4 @@ +import {Tiles3DTileJSONPostprocessed} from '@loaders.gl/3d-tiles'; import {BoundingVolumes} from '@loaders.gl/i3s'; import {Tile3D} from '@loaders.gl/tiles'; @@ -16,7 +17,7 @@ const DEFAULT_MAXIMUM_SCREEN_SPACE_ERROR = 16; * To avoid infinity values when we do calculations of maxError we shold replace 0 with value which allows us * to make child maxError bigger than his parent maxError. * - * @param tile - 3d-tiles tile Object + * @param tile - 3d-tiles tile JSON * @param coordinates - node converted coordinates * @returns An array of LOD metrics in format compatible with i3s 3DNodeIndexDocument.lodSelection * @example @@ -31,7 +32,10 @@ const DEFAULT_MAXIMUM_SCREEN_SPACE_ERROR = 16; } ] */ -export function convertGeometricErrorToScreenThreshold(tile: Tile3D, coordinates: BoundingVolumes) { +export function convertGeometricErrorToScreenThreshold( + tile: Tiles3DTileJSONPostprocessed, + coordinates: BoundingVolumes +) { const lodSelection: {metricType: string; maxError: number}[] = []; const boundingVolume = tile.boundingVolume; const lodMetricValue = tile.lodMetricValue || 0.1; diff --git a/modules/tile-converter/test/i3s-converter/helpers/geometry-converter.spec.js b/modules/tile-converter/test/i3s-converter/helpers/geometry-converter.spec.js index 1aec7aedfd..bd74669911 100644 --- a/modules/tile-converter/test/i3s-converter/helpers/geometry-converter.spec.js +++ b/modules/tile-converter/test/i3s-converter/helpers/geometry-converter.spec.js @@ -7,9 +7,10 @@ import convertB3dmToI3sGeometry, { getPropertyTable } from '../../../src/i3s-converter/helpers/geometry-converter'; import {PGMLoader} from '../../../src/pgm-loader'; -import {calculateTransformProps} from '../../../../tiles/src/tileset/helpers/transform-utils'; import {createdStorageAttribute} from '../../../src/i3s-converter/helpers/feature-attributes'; import {I3SAttributesWorker} from '../../../src/i3s-attributes-worker'; +import {BoundingSphere} from '@math.gl/culling'; +import {Matrix4} from '@math.gl/core'; const PGM_FILE_PATH = '@loaders.gl/tile-converter/test/data/egm84-30.pgm'; const FRANKFURT_B3DM_FILE_PATH = @@ -37,15 +38,14 @@ test.skip('tile-converter - I3S Geometry converter # should convert Frankfurt ti let nodeId = 1; const addNodeToNodePage = async () => nodeId++; const featuresHashArray = []; - const tileHeaderRequiredProps = { - computedTransform: [ - 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4055182.44018, 615965.038498, 4867494.346586, 1 - ], - boundingVolume: {center: [4051833.805439, 618316.801881, 4870677.172590001]} - }; const tileContent = await load(FRANKFURT_B3DM_FILE_PATH, Tiles3DLoader); const propertyTable = getPropertyTable(tileContent); - calculateTransformProps(tileHeaderRequiredProps, tileContent); + const tileTransform = new Matrix4([ + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4055182.44018, 615965.038498, 4867494.346586, 1 + ]); + const tileBoundingVolume = new BoundingSphere([ + 4051833.805439, 618316.801881, 4870677.172590001 + ]); const geoidHeightModel = await load(PGM_FILE_PATH, PGMLoader); const workerSource = await getWorkersSource(); const attributeStorageInfo = []; @@ -53,6 +53,8 @@ test.skip('tile-converter - I3S Geometry converter # should convert Frankfurt ti try { const convertedResources = await convertB3dmToI3sGeometry( tileContent, + tileTransform, + tileBoundingVolume, addNodeToNodePage, propertyTable, featuresHashArray, @@ -124,19 +126,20 @@ test('tile-converter - I3S Geometry converter # should convert Berlin tile conte const draco = true; const generageBoundingVolumes = false; const shouldMergeMaterials = false; - const tileHeaderRequiredProps = { - computedTransform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - boundingVolume: {center: [3781178.760596639, 902182.0936989671, 5039803.738586299]} - }; const tileContent = await load(BERLIN_B3DM_FILE_PATH, Tiles3DLoader); const propertyTable = getPropertyTable(tileContent); - calculateTransformProps(tileHeaderRequiredProps, tileContent); + const tileTransform = new Matrix4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); + const tileBoundingVolume = new BoundingSphere([ + 3781178.760596639, 902182.0936989671, 5039803.738586299 + ]); const geoidHeightModel = await load(PGM_FILE_PATH, PGMLoader); const workerSource = await getWorkersSource(); const attributeStorageInfo = []; try { const convertedResources = await convertB3dmToI3sGeometry( tileContent, + tileTransform, + tileBoundingVolume, addNodeToNodePage, propertyTable, featuresHashArray, @@ -202,21 +205,22 @@ test('tile-converter - I3S Geometry converter # should convert New York tile con const draco = true; const generageBoundingVolumes = false; const shouldMergeMaterials = false; - const tileHeaderRequiredProps = { - computedTransform: [ - 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4055182.44018, 615965.038498, 4867494.346586, 1 - ], - boundingVolume: {center: [1319833.032477655, -4673588.626640962, 4120866.796624521]} - }; const tileContent = await load(NEW_YORK_B3DM_FILE_PATH, Tiles3DLoader); const propertyTable = getPropertyTable(tileContent); - calculateTransformProps(tileHeaderRequiredProps, tileContent); + const tileTransform = new Matrix4([ + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4055182.44018, 615965.038498, 4867494.346586, 1 + ]); + const tileBoundingVolume = new BoundingSphere([ + 1319833.032477655, -4673588.626640962, 4120866.796624521 + ]); const geoidHeightModel = await load(PGM_FILE_PATH, PGMLoader); const workerSource = await getWorkersSource(); const attributeStorageInfo = getAttributeStorageInfo(propertyTable); try { const convertedResources = await convertB3dmToI3sGeometry( tileContent, + tileTransform, + tileBoundingVolume, addNodeToNodePage, propertyTable, featuresHashArray, @@ -265,23 +269,24 @@ test('tile-converter - I3S Geometry converter # should convert Ferry tile conten const draco = true; const generageBoundingVolumes = false; const shouldMergeMaterials = false; - const tileHeaderRequiredProps = { - computedTransform: [ - 0.8443837640659682, -0.5357387973460459, 0, 0, 0.32832660036003297, 0.5174791372742712, - 0.7902005985709575, 0, -0.42334111834053034, -0.667232555788526, 0.6128482797708588, 0, - -2703514.4440963655, -4261038.614006309, 3887533.151398322, 1 - ], - boundingVolume: {center: [-2703528.7614193764, -4261014.993900511, 3887572.9889940596]} - }; const tileContent = await load(FERRY_GLTF_FILE_PATH, Tiles3DLoader); const propertyTable = getPropertyTable(tileContent); - calculateTransformProps(tileHeaderRequiredProps, tileContent); + const tileTransform = new Matrix4([ + 0.8443837640659682, -0.5357387973460459, 0, 0, 0.32832660036003297, 0.5174791372742712, + 0.7902005985709575, 0, -0.42334111834053034, -0.667232555788526, 0.6128482797708588, 0, + -2703514.4440963655, -4261038.614006309, 3887533.151398322, 1 + ]); + const tileBoundingVolume = new BoundingSphere([ + -2703528.7614193764, -4261014.993900511, 3887572.9889940596 + ]); const geoidHeightModel = await load(PGM_FILE_PATH, PGMLoader); const workerSource = await getWorkersSource(); const attributeStorageInfo = getAttributeStorageInfo(propertyTable); try { const convertedResources = await convertB3dmToI3sGeometry( tileContent, + tileTransform, + tileBoundingVolume, addNodeToNodePage, propertyTable, featuresHashArray, @@ -339,22 +344,23 @@ test('tile-converter - I3S Geometry converter # TRIANGLE_STRIPS should be conver const draco = true; const generageBoundingVolumes = false; const shouldMergeMaterials = false; - const tileHeaderRequiredProps = { - computedTransform: [ - -0.16491735, -0.98630739, 0, 0, -0.70808611, 0.11839684, 0.69612948, 0, -0.68659765, - 0.11480383, -0.71791625, 0, -4386786.82071079, 733504.6938935, -4556188.9172627, 1 - ], - boundingVolume: {center: [-4386794.587985844, 733486.8163247632, -4556196.147240348]} - }; const tileContent = await load(TRIANGLE_STRIP_B3DM_FILE_PATH, Tiles3DLoader); const propertyTable = getPropertyTable(tileContent); - calculateTransformProps(tileHeaderRequiredProps, tileContent); + const tileTransform = new Matrix4([ + -0.16491735, -0.98630739, 0, 0, -0.70808611, 0.11839684, 0.69612948, 0, -0.68659765, 0.11480383, + -0.71791625, 0, -4386786.82071079, 733504.6938935, -4556188.9172627, 1 + ]); + const tileBoundingVolume = new BoundingSphere([ + -4386794.587985844, 733486.8163247632, -4556196.147240348 + ]); const geoidHeightModel = await load(PGM_FILE_PATH, PGMLoader); const workerSource = await getWorkersSource(); const attributeStorageInfo = getAttributeStorageInfo(propertyTable); try { const convertedResources = await convertB3dmToI3sGeometry( tileContent, + tileTransform, + tileBoundingVolume, addNodeToNodePage, propertyTable, featuresHashArray, @@ -394,23 +400,24 @@ test('tile-converter - I3S Geometry converter # should not convert point geometr const draco = true; const generageBoundingVolumes = false; const shouldMergeMaterials = false; - const tileHeaderRequiredProps = { - computedTransform: [ - -0.4222848483394723, 0.9064631856081685, 0, 0, -0.786494516061795, -0.3663962560290312, - 0.49717216311116175, 0, 0.4506682627694476, 0.2099482714980043, 0.8676519119020993, 0, - 2881693.941235528, 1342465.6491912308, 5510858.997465198, 1 - ], - boundingVolume: {center: [2881727.346362028, 1342482.044833547, 5510923.203394569]} - }; const tileContent = await load(HELSINKI_GLB_FILE_PATH, Tiles3DLoader); const propertyTable = getPropertyTable(tileContent); - calculateTransformProps(tileHeaderRequiredProps, tileContent); + const tileTransform = new Matrix4([ + -0.4222848483394723, 0.9064631856081685, 0, 0, -0.786494516061795, -0.3663962560290312, + 0.49717216311116175, 0, 0.4506682627694476, 0.2099482714980043, 0.8676519119020993, 0, + 2881693.941235528, 1342465.6491912308, 5510858.997465198, 1 + ]); + const tileBoundingVolume = new BoundingSphere([ + 2881727.346362028, 1342482.044833547, 5510923.203394569 + ]); const geoidHeightModel = await load(PGM_FILE_PATH, PGMLoader); const workerSource = await getWorkersSource(); const attributeStorageInfo = getAttributeStorageInfo(propertyTable); try { await convertB3dmToI3sGeometry( tileContent, + tileTransform, + tileBoundingVolume, addNodeToNodePage, propertyTable, featuresHashArray, diff --git a/modules/tile-converter/test/i3s-converter/helpers/gltf-attributes.spec.js b/modules/tile-converter/test/i3s-converter/helpers/gltf-attributes.spec.js index a192280bae..9c5ed90f73 100644 --- a/modules/tile-converter/test/i3s-converter/helpers/gltf-attributes.spec.js +++ b/modules/tile-converter/test/i3s-converter/helpers/gltf-attributes.spec.js @@ -1,5 +1,7 @@ +import {BoundingSphere} from '@math.gl/culling'; import {prepareDataForAttributesConversion} from '../../../src/i3s-converter/helpers/gltf-attributes'; import test from 'tape-promise/tape'; +import {Matrix4} from '@math.gl/core'; test('gltf-attributes - Should generate attributes object from tileContent without images', async (t) => { const tileContent = { @@ -46,9 +48,7 @@ test('gltf-attributes - Should generate attributes object from tileContent witho } ] } - }, - cartographicOrigin: [1, 2, 3], - cartesianModelMatrix: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + } }; const expectedResult = { @@ -74,16 +74,23 @@ test('gltf-attributes - Should generate attributes object from tileContent witho } ], images: [], - cartographicOrigin: [1, 2, 3], - cartesianModelMatrix: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + cartographicOrigin: [8.676496951388435, 50.108416671362576, 189.47502169783516], + cartesianModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }; - // @ts-expect-error - const result = prepareDataForAttributesConversion(tileContent); + const result = prepareDataForAttributesConversion( + // @ts-expect-error + tileContent, + new Matrix4(), + new BoundingSphere([4051833.805439, 618316.801881, 4870677.172590001]) + ); t.ok(result); // @ts-expect-error delete result.nodes[0].mesh.primitives[0].material.uniqueId; - t.deepEqual(result, expectedResult); + t.deepEqual(result.nodes, expectedResult.nodes); + t.deepEqual(result.images, expectedResult.images); + t.ok(areNumberArraysEqual(result.cartographicOrigin, expectedResult.cartographicOrigin)); + t.ok(areNumberArraysEqual(result.cartesianModelMatrix, expectedResult.cartesianModelMatrix)); t.end(); }); @@ -160,9 +167,7 @@ test('gltf-attributes - Should generate attributes object from tileContent with } ] } - }, - cartographicOrigin: [1, 2, 3], - cartesianModelMatrix: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + } }; const expectedResult = { @@ -200,15 +205,38 @@ test('gltf-attributes - Should generate attributes object from tileContent with data: new Uint8Array([3, 3, 3, 255, 4, 4, 4, 255]) } ], - cartographicOrigin: [1, 2, 3], - cartesianModelMatrix: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + cartographicOrigin: [8.676496951388435, 50.108416671362576, 189.47502169783516], + cartesianModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }; - // @ts-expect-error - const result = prepareDataForAttributesConversion(tileContent); + + const result = prepareDataForAttributesConversion( + // @ts-expect-error + tileContent, + new Matrix4(), + new BoundingSphere([4051833.805439, 618316.801881, 4870677.172590001]) + ); t.ok(result); // @ts-expect-error delete result.nodes[0].mesh.primitives[0].material.uniqueId; - t.deepEqual(result, expectedResult); + t.deepEqual(result.nodes, expectedResult.nodes); + t.deepEqual(result.images, expectedResult.images); + t.ok(areNumberArraysEqual(result.cartographicOrigin, expectedResult.cartographicOrigin)); + t.ok(areNumberArraysEqual(result.cartesianModelMatrix, expectedResult.cartesianModelMatrix)); t.end(); }); + +const EPSILON = 0.000000001; +function areNumberArraysEqual(array1, array2) { + let result = true; + if (array1.length !== array2.length) { + return false; + } + for (let i = 0; i < array1.length; i++) { + if (Math.abs(array1[i] - array2[i]) > EPSILON) { + result = false; + break; + } + } + return result; +}