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 947a5af348..19224f1d4f 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; +}