From 7ae639c6b64fb246c38a20b3741b159aafba7e9c Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Fri, 9 Aug 2024 20:27:57 +0300 Subject: [PATCH 1/8] feat(converter): support writing 3DTILES of version 1.1 --- .../src/lib/encoders/encode-3d-tile-gltf.ts | 18 +++++++++++ .../src/lib/encoders/encode-3d-tile.ts | 3 ++ modules/gltf/src/glb-writer.ts | 2 +- .../3d-tiles-converter/3d-tiles-converter.ts | 32 +++++++++++++------ .../helpers/b3dm-converter.ts | 16 +++++++--- modules/tile-converter/src/converter-cli.ts | 18 +++++++++++ .../src/lib/utils/conversion-dump.ts | 7 ++-- .../src/lib/utils/statistic-utills.ts | 32 ++++++++++++++++--- .../test/lib/utils/conversion-dump.spec.ts | 3 +- 9 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts diff --git a/modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts b/modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts new file mode 100644 index 0000000000..34132dc652 --- /dev/null +++ b/modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts @@ -0,0 +1,18 @@ +// loaders.gl +// SPDX-License-Identifier: MIT AND Apache-2.0 +// Copyright vis.gl contributors + +// 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 {copyBinaryToDataView} from '@loaders.gl/loader-utils'; + +// Procedurally encode the tile array dataView for testing purposes +export function encodeGltf3DTile(tile, dataView, byteOffset, options) { + const gltfEncoded = tile.gltfEncoded; + if (gltfEncoded) { + byteOffset = copyBinaryToDataView(dataView, byteOffset, gltfEncoded, gltfEncoded.byteLength); + } + + return byteOffset; +} diff --git a/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts b/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts index b746df5164..d9b6db27cb 100644 --- a/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts +++ b/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts @@ -12,6 +12,7 @@ import {encodeComposite3DTile} from './encode-3d-tile-composite'; import {encodeBatchedModel3DTile} from './encode-3d-tile-batched-model'; import {encodeInstancedModel3DTile} from './encode-3d-tile-instanced-model'; import {encodePointCloud3DTile} from './encode-3d-tile-point-cloud'; +import {encodeGltf3DTile} from './encode-3d-tile-gltf'; export default function encode3DTile(tile, options) { const byteLength = encode3DTileToDataView(tile, null, 0, options); @@ -33,6 +34,8 @@ function encode3DTileToDataView(tile, dataView, byteOffset, options) { return encodeBatchedModel3DTile(tile, dataView, byteOffset, options); case TILE3D_TYPE.INSTANCED_3D_MODEL: return encodeInstancedModel3DTile(tile, dataView, byteOffset, options); + case TILE3D_TYPE.GLTF: + return encodeGltf3DTile(tile, dataView, byteOffset, options); default: throw new Error('3D Tiles: unknown tile type'); } diff --git a/modules/gltf/src/glb-writer.ts b/modules/gltf/src/glb-writer.ts index b19d4b0529..d5baec23cb 100644 --- a/modules/gltf/src/glb-writer.ts +++ b/modules/gltf/src/glb-writer.ts @@ -34,7 +34,7 @@ export const GLBWriter = { } as const satisfies WriterWithEncoder; function encodeSync(glb, options) { - const {byteOffset = 0} = options; + const {byteOffset = 0} = options ?? {}; // Calculate length and allocate buffer const byteLength = encodeGLBSync(glb, null, byteOffset, options); diff --git a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts index 3175ce3246..a83faadf42 100644 --- a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts @@ -59,6 +59,7 @@ export default class Tiles3DConverter { }; conversionDump: ConversionDump; progress: Progress; + fileExt: string; constructor() { this.options = {}; @@ -71,6 +72,7 @@ export default class Tiles3DConverter { this.workerSource = {}; this.conversionDump = new ConversionDump(); this.progress = new Progress(); + this.fileExt = 'b3dm'; } /** @@ -78,6 +80,7 @@ export default class Tiles3DConverter { * @param options * @param options.inputUrl the url to read the tileset from * @param options.outputPath the output filename + * @param options.tilesVersion the version of 3DTiles * @param options.tilesetName the output name of the tileset * @param options.egmFilePath location of *.pgm file to convert heights from ellipsoidal to gravity-related format * @param options.maxDepth The max tree depth of conversion @@ -87,6 +90,7 @@ export default class Tiles3DConverter { inputUrl: string; outputPath: string; tilesetName: string; + tilesVersion?: string; maxDepth?: number; egmFilePath: string; inquirer?: {prompt: PromptModule}; @@ -96,9 +100,19 @@ export default class Tiles3DConverter { console.log(BROWSER_ERROR_MESSAGE); // eslint-disable-line no-console return BROWSER_ERROR_MESSAGE; } - const {inputUrl, outputPath, tilesetName, maxDepth, egmFilePath, inquirer, analyze} = options; + const { + inputUrl, + outputPath, + tilesVersion, + tilesetName, + maxDepth, + egmFilePath, + inquirer, + analyze + } = options; this.conversionStartTime = process.hrtime(); - this.options = {maxDepth, inquirer}; + this.options = {maxDepth, inquirer, tilesVersion}; + this.fileExt = this.options.tilesVersion === '1.0' ? 'b3dm' : 'glb'; console.log('Loading egm file...'); // eslint-disable-line this.geoidHeightModel = await load(egmFilePath, PGMLoader); @@ -173,7 +187,7 @@ export default class Tiles3DConverter { await this._addChildren(rootNode, rootTile, 1); - const tileset = transform({root: rootTile}, tilesetTemplate()); + const tileset = transform({asset: {version: tilesVersion}, root: rootTile}, tilesetTemplate()); await writeFile(this.tilesetPath, JSON.stringify(tileset), 'tileset.json'); await this.conversionDump.deleteDumpFile(); @@ -244,7 +258,7 @@ export default class Tiles3DConverter { if (sourceChild.contentUrl) { if ( this.conversionDump.restored && - this.conversionDump.isFileConversionComplete(`${sourceChild.id}.b3dm`) && + this.conversionDump.isFileConversionComplete(`${sourceChild.id}.${this.fileExt}`) && (sourceChild.obb || sourceChild.mbs) ) { const {child} = this._createChildAndBoundingVolume(sourceChild); @@ -279,13 +293,13 @@ export default class Tiles3DConverter { textureFormat: sourceChild.textureFormat }; - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new B3dmConverter({tilesVersion: this.options.tilesVersion}); const b3dm = await b3dmConverter.convert(i3sAttributesData, featureAttributes); - await this.conversionDump.addNode(`${sourceChild.id}.b3dm`, sourceChild.id); - await writeFile(this.tilesetPath, new Uint8Array(b3dm), `${sourceChild.id}.b3dm`); + await this.conversionDump.addNode(`${sourceChild.id}.${this.fileExt}`, sourceChild.id); + await writeFile(this.tilesetPath, new Uint8Array(b3dm), `${sourceChild.id}.${this.fileExt}`); await this.conversionDump.updateConvertedNodesDumpFile( - `${sourceChild.id}.b3dm`, + `${sourceChild.id}.${this.fileExt}`, sourceChild.id, true ); @@ -379,7 +393,7 @@ export default class Tiles3DConverter { geometricError: convertScreenThresholdToGeometricError(sourceChild), children: [], content: { - uri: `${sourceChild.id}.b3dm`, + uri: `${sourceChild.id}.${this.fileExt}`, boundingVolume } }; diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts index fb91488fb5..6cf429ad84 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts @@ -1,7 +1,10 @@ -import type {I3STileContent} from '@loaders.gl/i3s'; +/* eslint-disable no-console */ + +import type {I3STileContent, FeatureAttribute} from '@loaders.gl/i3s'; import {encodeSync} from '@loaders.gl/core'; import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf'; import {Tile3DWriter} from '@loaders.gl/3d-tiles'; +import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; import {Matrix4, Vector3} from '@math.gl/core'; import {Ellipsoid} from '@math.gl/geospatial'; import {convertTextureAtlas} from './texture-atlas'; @@ -26,6 +29,11 @@ export default class B3dmConverter { // @ts-expect-error rtcCenter: Float32Array; i3sTile: any; + tilesVersion: string; + + constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { + this.tilesVersion = options.tilesVersion; + } /** * The starter of content conversion @@ -34,13 +42,13 @@ export default class B3dmConverter { */ async convert( i3sAttributesData: I3SAttributesData, - featureAttributes: any = null + featureAttributes: FeatureAttribute | null = null ): Promise { const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes); const b3dm = encodeSync( { gltfEncoded: new Uint8Array(gltf), - type: 'b3dm', + type: this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF, featuresLength: this._getFeaturesLength(featureAttributes), batchTable: featureAttributes }, @@ -57,7 +65,7 @@ export default class B3dmConverter { // eslint-disable-next-line max-statements async buildGLTF( i3sAttributesData: I3SAttributesData, - featureAttributes: any + featureAttributes: FeatureAttribute | null ): Promise { const {tileContent, textureFormat, box} = i3sAttributesData; const {material, attributes, indices: originalIndices, modelMatrix} = tileContent; diff --git a/modules/tile-converter/src/converter-cli.ts b/modules/tile-converter/src/converter-cli.ts index 2b1388bd8b..cb86d64fb1 100644 --- a/modules/tile-converter/src/converter-cli.ts +++ b/modules/tile-converter/src/converter-cli.ts @@ -26,6 +26,9 @@ type TileConversionOptions = { /** Output folder. This folder will be created by converter if doesn't exist. It is relative to the converter path. * Default: "data" folder */ output: string; + /** 3DTile version. + * Default: version "1.1" */ + tilesVersion?: string; /** Keep created 3DNodeIndexDocument files on disk instead of memory. This option reduce memory usage but decelerates conversion speed */ instantNodeWriting: boolean; /** Try to merge similar materials to be able to merge meshes into one node (I3S to 3DTiles conversion only) */ @@ -178,6 +181,7 @@ function printHelp(): void { '--tileset [tileset.json file (3DTiles) / http://..../SceneServer/layers/0 resource (I3S)]' ); console.log('--input-type [tileset input type: I3S or 3DTILES]'); + console.log('--tile-version [3dtile version: 1.0 or 1.1, default: 1.1]'); console.log( '--egm [location of Earth Gravity Model *.pgm file to convert heights from ellipsoidal to gravity-related format. A model file can be loaded from GeographicLib https://geographiclib.sourceforge.io/html/geoid.html], default: "./deps/egm2008-5.zip"' ); @@ -214,6 +218,7 @@ async function convert(options: ValidatedTileConversionOptions) { await tiles3DConverter.convert({ inputUrl: options.tileset, outputPath: options.output, + tilesVersion: options.tilesVersion, tilesetName: options.name, maxDepth: options.maxDepth, egmFilePath: options.egm, @@ -274,6 +279,15 @@ function validateOptions( console.log('Missed/Incorrect: --input-type [tileset input type: I3S or 3DTILES]'), condition: (value) => addHash || (Boolean(value) && Object.values(TILESET_TYPE).includes(value.toUpperCase())) + }, + tilesVersion: { + getMessage: () => console.log('Incorrect: --tilesVersion [1.0 or 1.1]'), + condition: (value) => + addHash || + (Boolean(value) && + Object.values(['1.0', '1.1']).includes(value) && + Boolean(options.inputType === 'I3S')) || + Boolean(options.analyze) } }; const exceptions: (() => void)[] = []; @@ -302,6 +316,7 @@ function validateOptions( function parseOptions(args: string[]): TileConversionOptions { const opts: TileConversionOptions = { output: 'data', + tilesVersion: '1.1', instantNodeWriting: false, mergeMaterials: true, egm: join(process.cwd(), 'deps', 'egm2008-5.pgm'), @@ -331,6 +346,9 @@ function parseOptions(args: string[]): TileConversionOptions { case '--output': opts.output = getStringValue(index, args); break; + case '--tiles-version': + opts.tilesVersion = getStringValue(index, args); + break; case '--instant-node-writing': opts.instantNodeWriting = getBooleanValue(index, args); break; diff --git a/modules/tile-converter/src/lib/utils/conversion-dump.ts b/modules/tile-converter/src/lib/utils/conversion-dump.ts index 82143ed9c4..5db3d1e0d2 100644 --- a/modules/tile-converter/src/lib/utils/conversion-dump.ts +++ b/modules/tile-converter/src/lib/utils/conversion-dump.ts @@ -22,6 +22,7 @@ export type ConversionDumpOptions = { generateBoundingVolumes: boolean; metadataClass: string; analyze: boolean; + tilesVersion: string; }; type NodeDoneStatus = { @@ -87,7 +88,8 @@ export class ConversionDump { generateBoundingVolumes, mergeMaterials = true, metadataClass, - analyze = false + analyze = false, + tilesVersion = '1.1' } = currentOptions; this.options = { tilesetName, @@ -102,7 +104,8 @@ export class ConversionDump { generateBoundingVolumes, mergeMaterials, metadataClass, - analyze + analyze, + tilesVersion }; const dumpFilename = join( diff --git a/modules/tile-converter/src/lib/utils/statistic-utills.ts b/modules/tile-converter/src/lib/utils/statistic-utills.ts index dd9c77e502..4079569de7 100644 --- a/modules/tile-converter/src/lib/utils/statistic-utills.ts +++ b/modules/tile-converter/src/lib/utils/statistic-utills.ts @@ -49,16 +49,38 @@ function timeConverterFromSecondsAndMilliseconds(timeInSeconds: number, millisec return result; } -export async function calculateFilesSize(params: {outputPath: string; tilesetName: string}) { - const {outputPath, tilesetName} = params; +export async function calculateFilesSize(params) { + const {slpk, outputPath, tilesetName} = params; const fullOutputPath = getAbsoluteFilePath(outputPath); try { - const slpkPath = join(fullOutputPath, `${tilesetName}.slpk`); - const stat = await fs.stat(slpkPath); - return stat.size; + if (slpk) { + const slpkPath = join(fullOutputPath, `${tilesetName}.slpk`); + const stat = await fs.stat(slpkPath); + return stat.size; + } + + const directoryPath = join(fullOutputPath, tilesetName); + const totalSize = await getTotalFilesSize(directoryPath); + return totalSize; } catch (error) { console.log('Calculate file sizes error: ', error); // eslint-disable-line return null; } } + +async function getTotalFilesSize(dirPath) { + let totalFileSize = 0; + + const files = await fs.readdir(dirPath); + + for (const file of files) { + const fileStat = await fs.stat(join(dirPath, file)); + if (fileStat.isDirectory()) { + totalFileSize += await getTotalFilesSize(join(dirPath, file)); + } else { + totalFileSize += fileStat.size; + } + } + return totalFileSize; +} diff --git a/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts b/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts index 9459a54d4a..50f489ea17 100644 --- a/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts +++ b/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts @@ -51,7 +51,8 @@ const testOptions = { generateTextures: true, generateBoundingVolumes: true, metadataClass: 'testMetadataClass', - analyze: true + analyze: true, + tilesVersion: '1.0' }; const testMaterialDefinitions = [ { From 41025023d9ceae522930e005f1df61bc278ae37f Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Fri, 9 Aug 2024 23:15:52 +0300 Subject: [PATCH 2/8] two converter classes extending a tiles-converter --- .../3d-tiles-converter/3d-tiles-converter.ts | 9 +- .../helpers/b3dm-converter.ts | 355 +----------------- .../helpers/gltf-converter.ts | 7 + .../helpers/tiles-converter.ts | 354 +++++++++++++++++ 4 files changed, 371 insertions(+), 354 deletions(-) create mode 100644 modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts create mode 100644 modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts diff --git a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts index a83faadf42..374eb26411 100644 --- a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts @@ -22,7 +22,9 @@ import {TILESET as tilesetTemplate} from './json-templates/tileset'; import {createObbFromMbs} from '../i3s-converter/helpers/coordinate-converter'; import {WorkerFarm} from '@loaders.gl/worker-utils'; import {BROWSER_ERROR_MESSAGE} from '../constants'; -import B3dmConverter, {I3SAttributesData} from './helpers/b3dm-converter'; +import {I3SAttributesData} from './helpers/tiles-converter'; +import B3dmConverter from './helpers/b3dm-converter'; +import GltfConverter from './helpers/gltf-converter'; import {I3STileHeader} from '@loaders.gl/i3s/src/types'; import {getNodeCount, loadFromArchive, loadI3SContent, openSLPK} from './helpers/load-i3s'; import {I3SLoaderOptions} from '@loaders.gl/i3s/src/i3s-loader'; @@ -293,8 +295,9 @@ export default class Tiles3DConverter { textureFormat: sourceChild.textureFormat }; - const b3dmConverter = new B3dmConverter({tilesVersion: this.options.tilesVersion}); - const b3dm = await b3dmConverter.convert(i3sAttributesData, featureAttributes); + const converter = + this.options.tilesVersion === '1.0' ? new B3dmConverter() : new GltfConverter(); + const b3dm = await converter.convert(i3sAttributesData, featureAttributes); await this.conversionDump.addNode(`${sourceChild.id}.${this.fileExt}`, sourceChild.id); await writeFile(this.tilesetPath, new Uint8Array(b3dm), `${sourceChild.id}.${this.fileExt}`); diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts index 6cf429ad84..c78dba0a91 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts @@ -1,354 +1,7 @@ -/* eslint-disable no-console */ +import {TilesConverter} from './tiles-converter'; -import type {I3STileContent, FeatureAttribute} from '@loaders.gl/i3s'; -import {encodeSync} from '@loaders.gl/core'; -import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf'; -import {Tile3DWriter} from '@loaders.gl/3d-tiles'; -import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; -import {Matrix4, Vector3} from '@math.gl/core'; -import {Ellipsoid} from '@math.gl/geospatial'; -import {convertTextureAtlas} from './texture-atlas'; -import {generateSyntheticIndices} from '../../lib/utils/geometry-utils'; - -const Z_UP_TO_Y_UP_MATRIX = new Matrix4([1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]); -const scratchVector = new Vector3(); -const KHR_MATERIALS_UNLIT = 'KHR_materials_unlit'; -const METALLIC_FACTOR_DEFAULT = 1.0; -const ROUGHNESS_FACTOR_DEFAULT = 1.0; - -export type I3SAttributesData = { - tileContent: I3STileContent; - box: number[]; - textureFormat: string; -}; - -/** - * Converts content of an I3S node to *.b3dm's file content - */ -export default class B3dmConverter { - // @ts-expect-error - rtcCenter: Float32Array; - i3sTile: any; - tilesVersion: string; - - constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { - this.tilesVersion = options.tilesVersion; - } - - /** - * The starter of content conversion - * @param i3sTile - Tile3D instance for I3S node - * @returns - encoded content - */ - async convert( - i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null = null - ): Promise { - const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes); - const b3dm = encodeSync( - { - gltfEncoded: new Uint8Array(gltf), - type: this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF, - featuresLength: this._getFeaturesLength(featureAttributes), - batchTable: featureAttributes - }, - Tile3DWriter - ); - return b3dm; - } - - /** - * Build and encode gltf - * @param i3sTile - Tile3D instance for I3S node - * @returns - encoded glb content - */ - // eslint-disable-next-line max-statements - async buildGLTF( - i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null - ): Promise { - const {tileContent, textureFormat, box} = i3sAttributesData; - const {material, attributes, indices: originalIndices, modelMatrix} = tileContent; - const gltfBuilder = new GLTFScenegraph(); - - const textureIndex = await this._addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder); - - // Add KHR_MATERIALS_UNLIT extension in the following cases: - // - metallicFactor or roughnessFactor are set to default values - // - metallicFactor or roughnessFactor are not set - const pbrMetallicRoughness = material?.pbrMetallicRoughness; - if ( - pbrMetallicRoughness && - (pbrMetallicRoughness.metallicFactor === undefined || - pbrMetallicRoughness.metallicFactor === METALLIC_FACTOR_DEFAULT) && - (pbrMetallicRoughness.roughnessFactor === undefined || - pbrMetallicRoughness.roughnessFactor === ROUGHNESS_FACTOR_DEFAULT) - ) { - gltfBuilder.addObjectExtension(material, KHR_MATERIALS_UNLIT, {}); - gltfBuilder.addExtension(KHR_MATERIALS_UNLIT); - } - - const pbrMaterialInfo = this._convertI3sMaterialToGLTFMaterial(material, textureIndex); - const materialIndex = gltfBuilder.addMaterial(pbrMaterialInfo); - - const positions = attributes.positions; - const positionsValue = positions.value; - - if (attributes.uvRegions && attributes.texCoords) { - attributes.texCoords.value = convertTextureAtlas( - attributes.texCoords.value, - attributes.uvRegions.value - ); - } - - const cartesianOrigin = new Vector3(box); - const cartographicOrigin = Ellipsoid.WGS84.cartesianToCartographic( - cartesianOrigin, - new Vector3() - ); - - attributes.positions.value = this._normalizePositions( - positionsValue, - cartesianOrigin, - cartographicOrigin, - modelMatrix - ); - this._createBatchIds(tileContent, featureAttributes); - if (attributes.normals && !this._checkNormals(attributes.normals.value)) { - delete attributes.normals; - } - const indices = - originalIndices || generateSyntheticIndices(positionsValue.length / positions.size); - const meshIndex = gltfBuilder.addMesh({ - attributes, - indices, - material: materialIndex, - mode: 4 - }); - const transformMatrix = this._generateTransformMatrix(cartesianOrigin); - const nodeIndex = gltfBuilder.addNode({meshIndex, matrix: transformMatrix}); - const sceneIndex = gltfBuilder.addScene({nodeIndices: [nodeIndex]}); - gltfBuilder.setDefaultScene(sceneIndex); - - gltfBuilder.createBinaryChunk(); - - const gltfBuffer = encodeSync(gltfBuilder.gltf, GLTFWriter); - - return gltfBuffer; - } - - /** - * Update gltfBuilder with texture from I3S tile - * @param {object} i3sTile - Tile3D object - * @param {GLTFScenegraph} gltfBuilder - gltfScenegraph instance to construct GLTF - * @returns {Promise} - GLTF texture index - */ - async _addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder) { - const {texture, material, attributes} = tileContent; - let textureIndex = null; - let selectedTexture = texture; - if (!texture && material) { - selectedTexture = - material.pbrMetallicRoughness && - material.pbrMetallicRoughness.baseColorTexture && - material.pbrMetallicRoughness.baseColorTexture.texture.source.image; - } - if (selectedTexture) { - const mimeType = this._deduceMimeTypeFromFormat(textureFormat); - const imageIndex = gltfBuilder.addImage(selectedTexture, mimeType); - textureIndex = gltfBuilder.addTexture({imageIndex}); - delete attributes.colors; - } - return textureIndex; - } - - /** - * Generate a positions array which is correct for 3DTiles/GLTF format - * @param {Float64Array} positionsValue - the input geometry positions array - * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system - * @param {number[]} cartographicOrigin - the tile center in the cartographic coordinate system - * @param {number[]} modelMatrix - the model matrix of geometry - * @returns {Float32Array} - the output geometry positions array - */ - _normalizePositions(positionsValue, cartesianOrigin, cartographicOrigin, modelMatrix) { - const newPositionsValue = new Float32Array(positionsValue.length); - for (let index = 0; index < positionsValue.length; index += 3) { - const vertex = positionsValue.subarray(index, index + 3); - const cartesianOriginVector = new Vector3(cartesianOrigin); - let vertexVector = new Vector3(Array.from(vertex)) - .transform(modelMatrix) - .add(cartographicOrigin); - Ellipsoid.WGS84.cartographicToCartesian(vertexVector, scratchVector); - vertexVector = scratchVector.subtract(cartesianOriginVector); - newPositionsValue.set(vertexVector, index); - } - return newPositionsValue; - } - - /** - * Generate the transformation matrix for GLTF node: - * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node - * 1. Create the translate transformation from cartesianOrigin (the positions array stores offsets from this cartesianOrigin) - * 2. Create the rotation transformation to rotate model from z-up coordinates (I3S specific) to y-up coordinates (GLTF specific) - * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system - * @returns {Matrix4} - an array of 16 numbers (4x4 matrix) - */ - _generateTransformMatrix(cartesianOrigin) { - const translateOriginMatrix = new Matrix4().translate(cartesianOrigin); - const result = translateOriginMatrix.multiplyLeft(Z_UP_TO_Y_UP_MATRIX); - return result; - } - - /** - * Create _BATCHID attribute - * @param {Object} i3sContent - the source object - * @returns {void} - */ - _createBatchIds(i3sContent, featureAttributes) { - const {featureIds} = i3sContent; - const {OBJECTID: objectIds} = featureAttributes || {}; - if (!featureIds || !objectIds) { - return; - } - - for (let i = 0; i < featureIds.length; i++) { - const featureId = featureIds[i]; - const batchId = objectIds.indexOf(featureId); - featureIds[i] = batchId; - } - - i3sContent.attributes._BATCHID = { - size: 1, - byteOffset: 0, - value: featureIds - }; - } - - /** - * Deduce mime type by format from `textureSetDefinition.formats[0].format` - * https://github.com/Esri/i3s-spec/blob/master/docs/1.7/textureSetDefinitionFormat.cmn.md - * @param {string} format - format name - * @returns {string} mime type. - */ - _deduceMimeTypeFromFormat(format) { - switch (format) { - case 'jpg': - return 'image/jpeg'; - case 'png': - return 'image/png'; - case 'ktx2': - return 'image/ktx2'; - default: - console.warn(`Unexpected texture format in I3S: ${format}`); // eslint-disable-line no-console, no-undef - return 'image/jpeg'; - } - } - - /** - * Convert i3s material to GLTF compatible material - * @param {object} material - i3s material definition - * @param {number | null} textureIndex - texture index in GLTF - * @returns {object} GLTF material - */ - _convertI3sMaterialToGLTFMaterial(material, textureIndex) { - const isTextureIndexExists = textureIndex !== null; - - if (!material) { - material = { - alphaMode: 'OPAQUE', - doubleSided: false, - pbrMetallicRoughness: { - metallicFactor: 0, - roughnessFactor: 1 - } - }; - - if (isTextureIndexExists) { - material.pbrMetallicRoughness.baseColorTexture = { - index: textureIndex, - texCoord: 0 - }; - } else { - material.pbrMetallicRoughness.baseColorFactor = [1, 1, 1, 1]; - } - - return material; - } - - if (textureIndex !== null) { - material = this._setGLTFTexture(material, textureIndex); - } - - return material; - } - - /** - * Set texture properties in material with GLTF textureIndex - * @param {object} materialDefinition - i3s material definition - * @param {number} textureIndex - texture index in GLTF - * @returns {void} - */ - _setGLTFTexture(materialDefinition, textureIndex) { - const material = { - ...materialDefinition, - pbrMetallicRoughness: {...materialDefinition.pbrMetallicRoughness} - }; - // I3SLoader now support loading only one texture. This elseif sequence will assign this texture to one of - // properties defined in materialDefinition - if ( - materialDefinition.pbrMetallicRoughness && - materialDefinition.pbrMetallicRoughness.baseColorTexture - ) { - material.pbrMetallicRoughness.baseColorTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if (materialDefinition.emissiveTexture) { - material.emissiveTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if ( - materialDefinition.pbrMetallicRoughness && - materialDefinition.pbrMetallicRoughness.metallicRoughnessTexture - ) { - material.pbrMetallicRoughness.metallicRoughnessTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if (materialDefinition.normalTexture) { - material.normalTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if (materialDefinition.occlusionTexture) { - material.occlusionTexture = { - index: textureIndex, - texCoord: 0 - }; - } - return material; - } - - /* - * Returns Features length based on attribute array in attribute object. - * @param {Object} attributes - * @returns {Number} Features length . - */ - _getFeaturesLength(attributes) { - if (!attributes) { - return 0; - } - const firstKey = Object.keys(attributes)[0]; - return firstKey ? attributes[firstKey].length : 0; - } - - /* Checks that normals buffer is correct - * @param {TypedArray} normals - * @returns {boolean} true - normals are correct; false - normals are incorrect - */ - _checkNormals(normals) { - // If all normals === 0, the resulting tileset is all in black colors on Cesium - return normals.find((value) => value); +export default class B3dmConverter extends TilesConverter { + constructor() { + super({tilesVersion: '1.0'}); } } diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts new file mode 100644 index 0000000000..2900463c91 --- /dev/null +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts @@ -0,0 +1,7 @@ +import {TilesConverter} from './tiles-converter'; + +export default class GltfConverter extends TilesConverter { + constructor() { + super({tilesVersion: '1.1'}); + } +} diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts new file mode 100644 index 0000000000..f762da1eec --- /dev/null +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts @@ -0,0 +1,354 @@ +/* eslint-disable no-console */ + +import type {I3STileContent, FeatureAttribute} from '@loaders.gl/i3s'; +import {encodeSync} from '@loaders.gl/core'; +import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf'; +import {Tile3DWriter} from '@loaders.gl/3d-tiles'; +import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; +import {Matrix4, Vector3} from '@math.gl/core'; +import {Ellipsoid} from '@math.gl/geospatial'; +import {convertTextureAtlas} from './texture-atlas'; +import {generateSyntheticIndices} from '../../lib/utils/geometry-utils'; + +const Z_UP_TO_Y_UP_MATRIX = new Matrix4([1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]); +const scratchVector = new Vector3(); +const KHR_MATERIALS_UNLIT = 'KHR_materials_unlit'; +const METALLIC_FACTOR_DEFAULT = 1.0; +const ROUGHNESS_FACTOR_DEFAULT = 1.0; + +export type I3SAttributesData = { + tileContent: I3STileContent; + box: number[]; + textureFormat: string; +}; + +/** + * Converts content of an I3S node to 3D Tiles file content + */ +export class TilesConverter { + // @ts-expect-error + rtcCenter: Float32Array; + i3sTile: any; + tilesVersion: string; + + protected constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { + this.tilesVersion = options.tilesVersion; + } + + /** + * The starter of content conversion + * @param i3sTile - Tile3D instance for I3S node + * @returns - encoded content + */ + async convert( + i3sAttributesData: I3SAttributesData, + featureAttributes: FeatureAttribute | null = null + ): Promise { + const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes); + const tiles = encodeSync( + { + gltfEncoded: new Uint8Array(gltf), + type: this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF, + featuresLength: this._getFeaturesLength(featureAttributes), + batchTable: featureAttributes + }, + Tile3DWriter + ); + return tiles; + } + + /** + * Build and encode gltf + * @param i3sTile - Tile3D instance for I3S node + * @returns - encoded glb content + */ + // eslint-disable-next-line max-statements + async buildGLTF( + i3sAttributesData: I3SAttributesData, + featureAttributes: FeatureAttribute | null + ): Promise { + const {tileContent, textureFormat, box} = i3sAttributesData; + const {material, attributes, indices: originalIndices, modelMatrix} = tileContent; + const gltfBuilder = new GLTFScenegraph(); + + const textureIndex = await this._addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder); + + // Add KHR_MATERIALS_UNLIT extension in the following cases: + // - metallicFactor or roughnessFactor are set to default values + // - metallicFactor or roughnessFactor are not set + const pbrMetallicRoughness = material?.pbrMetallicRoughness; + if ( + pbrMetallicRoughness && + (pbrMetallicRoughness.metallicFactor === undefined || + pbrMetallicRoughness.metallicFactor === METALLIC_FACTOR_DEFAULT) && + (pbrMetallicRoughness.roughnessFactor === undefined || + pbrMetallicRoughness.roughnessFactor === ROUGHNESS_FACTOR_DEFAULT) + ) { + gltfBuilder.addObjectExtension(material, KHR_MATERIALS_UNLIT, {}); + gltfBuilder.addExtension(KHR_MATERIALS_UNLIT); + } + + const pbrMaterialInfo = this._convertI3sMaterialToGLTFMaterial(material, textureIndex); + const materialIndex = gltfBuilder.addMaterial(pbrMaterialInfo); + + const positions = attributes.positions; + const positionsValue = positions.value; + + if (attributes.uvRegions && attributes.texCoords) { + attributes.texCoords.value = convertTextureAtlas( + attributes.texCoords.value, + attributes.uvRegions.value + ); + } + + const cartesianOrigin = new Vector3(box); + const cartographicOrigin = Ellipsoid.WGS84.cartesianToCartographic( + cartesianOrigin, + new Vector3() + ); + + attributes.positions.value = this._normalizePositions( + positionsValue, + cartesianOrigin, + cartographicOrigin, + modelMatrix + ); + this._createBatchIds(tileContent, featureAttributes); + if (attributes.normals && !this._checkNormals(attributes.normals.value)) { + delete attributes.normals; + } + const indices = + originalIndices || generateSyntheticIndices(positionsValue.length / positions.size); + const meshIndex = gltfBuilder.addMesh({ + attributes, + indices, + material: materialIndex, + mode: 4 + }); + const transformMatrix = this._generateTransformMatrix(cartesianOrigin); + const nodeIndex = gltfBuilder.addNode({meshIndex, matrix: transformMatrix}); + const sceneIndex = gltfBuilder.addScene({nodeIndices: [nodeIndex]}); + gltfBuilder.setDefaultScene(sceneIndex); + + gltfBuilder.createBinaryChunk(); + + const gltfBuffer = encodeSync(gltfBuilder.gltf, GLTFWriter); + + return gltfBuffer; + } + + /** + * Update gltfBuilder with texture from I3S tile + * @param {object} i3sTile - Tile3D object + * @param {GLTFScenegraph} gltfBuilder - gltfScenegraph instance to construct GLTF + * @returns {Promise} - GLTF texture index + */ + async _addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder) { + const {texture, material, attributes} = tileContent; + let textureIndex = null; + let selectedTexture = texture; + if (!texture && material) { + selectedTexture = + material.pbrMetallicRoughness && + material.pbrMetallicRoughness.baseColorTexture && + material.pbrMetallicRoughness.baseColorTexture.texture.source.image; + } + if (selectedTexture) { + const mimeType = this._deduceMimeTypeFromFormat(textureFormat); + const imageIndex = gltfBuilder.addImage(selectedTexture, mimeType); + textureIndex = gltfBuilder.addTexture({imageIndex}); + delete attributes.colors; + } + return textureIndex; + } + + /** + * Generate a positions array which is correct for 3DTiles/GLTF format + * @param {Float64Array} positionsValue - the input geometry positions array + * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system + * @param {number[]} cartographicOrigin - the tile center in the cartographic coordinate system + * @param {number[]} modelMatrix - the model matrix of geometry + * @returns {Float32Array} - the output geometry positions array + */ + _normalizePositions(positionsValue, cartesianOrigin, cartographicOrigin, modelMatrix) { + const newPositionsValue = new Float32Array(positionsValue.length); + for (let index = 0; index < positionsValue.length; index += 3) { + const vertex = positionsValue.subarray(index, index + 3); + const cartesianOriginVector = new Vector3(cartesianOrigin); + let vertexVector = new Vector3(Array.from(vertex)) + .transform(modelMatrix) + .add(cartographicOrigin); + Ellipsoid.WGS84.cartographicToCartesian(vertexVector, scratchVector); + vertexVector = scratchVector.subtract(cartesianOriginVector); + newPositionsValue.set(vertexVector, index); + } + return newPositionsValue; + } + + /** + * Generate the transformation matrix for GLTF node: + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node + * 1. Create the translate transformation from cartesianOrigin (the positions array stores offsets from this cartesianOrigin) + * 2. Create the rotation transformation to rotate model from z-up coordinates (I3S specific) to y-up coordinates (GLTF specific) + * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system + * @returns {Matrix4} - an array of 16 numbers (4x4 matrix) + */ + _generateTransformMatrix(cartesianOrigin) { + const translateOriginMatrix = new Matrix4().translate(cartesianOrigin); + const result = translateOriginMatrix.multiplyLeft(Z_UP_TO_Y_UP_MATRIX); + return result; + } + + /** + * Create _BATCHID attribute + * @param {Object} i3sContent - the source object + * @returns {void} + */ + _createBatchIds(i3sContent, featureAttributes) { + const {featureIds} = i3sContent; + const {OBJECTID: objectIds} = featureAttributes || {}; + if (!featureIds || !objectIds) { + return; + } + + for (let i = 0; i < featureIds.length; i++) { + const featureId = featureIds[i]; + const batchId = objectIds.indexOf(featureId); + featureIds[i] = batchId; + } + + i3sContent.attributes._BATCHID = { + size: 1, + byteOffset: 0, + value: featureIds + }; + } + + /** + * Deduce mime type by format from `textureSetDefinition.formats[0].format` + * https://github.com/Esri/i3s-spec/blob/master/docs/1.7/textureSetDefinitionFormat.cmn.md + * @param {string} format - format name + * @returns {string} mime type. + */ + _deduceMimeTypeFromFormat(format) { + switch (format) { + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'ktx2': + return 'image/ktx2'; + default: + console.warn(`Unexpected texture format in I3S: ${format}`); // eslint-disable-line no-console, no-undef + return 'image/jpeg'; + } + } + + /** + * Convert i3s material to GLTF compatible material + * @param {object} material - i3s material definition + * @param {number | null} textureIndex - texture index in GLTF + * @returns {object} GLTF material + */ + _convertI3sMaterialToGLTFMaterial(material, textureIndex) { + const isTextureIndexExists = textureIndex !== null; + + if (!material) { + material = { + alphaMode: 'OPAQUE', + doubleSided: false, + pbrMetallicRoughness: { + metallicFactor: 0, + roughnessFactor: 1 + } + }; + + if (isTextureIndexExists) { + material.pbrMetallicRoughness.baseColorTexture = { + index: textureIndex, + texCoord: 0 + }; + } else { + material.pbrMetallicRoughness.baseColorFactor = [1, 1, 1, 1]; + } + + return material; + } + + if (textureIndex !== null) { + material = this._setGLTFTexture(material, textureIndex); + } + + return material; + } + + /** + * Set texture properties in material with GLTF textureIndex + * @param {object} materialDefinition - i3s material definition + * @param {number} textureIndex - texture index in GLTF + * @returns {void} + */ + _setGLTFTexture(materialDefinition, textureIndex) { + const material = { + ...materialDefinition, + pbrMetallicRoughness: {...materialDefinition.pbrMetallicRoughness} + }; + // I3SLoader now support loading only one texture. This elseif sequence will assign this texture to one of + // properties defined in materialDefinition + if ( + materialDefinition.pbrMetallicRoughness && + materialDefinition.pbrMetallicRoughness.baseColorTexture + ) { + material.pbrMetallicRoughness.baseColorTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if (materialDefinition.emissiveTexture) { + material.emissiveTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if ( + materialDefinition.pbrMetallicRoughness && + materialDefinition.pbrMetallicRoughness.metallicRoughnessTexture + ) { + material.pbrMetallicRoughness.metallicRoughnessTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if (materialDefinition.normalTexture) { + material.normalTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if (materialDefinition.occlusionTexture) { + material.occlusionTexture = { + index: textureIndex, + texCoord: 0 + }; + } + return material; + } + + /* + * Returns Features length based on attribute array in attribute object. + * @param {Object} attributes + * @returns {Number} Features length . + */ + _getFeaturesLength(attributes) { + if (!attributes) { + return 0; + } + const firstKey = Object.keys(attributes)[0]; + return firstKey ? attributes[firstKey].length : 0; + } + + /* Checks that normals buffer is correct + * @param {TypedArray} normals + * @returns {boolean} true - normals are correct; false - normals are incorrect + */ + _checkNormals(normals) { + // If all normals === 0, the resulting tileset is all in black colors on Cesium + return normals.find((value) => value); + } +} From 11f49e4bd9bb6e17da186d4c47d59f6c02ae60ca Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 9 Sep 2024 16:56:37 +0300 Subject: [PATCH 3/8] b3dm converter --- .../3d-tiles-converter/3d-tiles-converter.ts | 8 +- .../helpers/b3dm-converter.ts | 355 +++++++++++++++++- .../helpers/gltf-converter.ts | 7 - .../helpers/tiles-converter.ts | 354 ----------------- .../helpers/b3dm-converter.spec.js | 20 +- 5 files changed, 365 insertions(+), 379 deletions(-) delete mode 100644 modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts delete mode 100644 modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts diff --git a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts index b5e29c1480..1ebc90ec28 100644 --- a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts @@ -22,9 +22,7 @@ import {TILESET as tilesetTemplate} from './json-templates/tileset'; import {createObbFromMbs} from '../i3s-converter/helpers/coordinate-converter'; import {WorkerFarm} from '@loaders.gl/worker-utils'; import {BROWSER_ERROR_MESSAGE} from '../constants'; -import {I3SAttributesData} from './helpers/tiles-converter'; -import B3dmConverter from './helpers/b3dm-converter'; -import GltfConverter from './helpers/gltf-converter'; +import {GltfConverter, type I3SAttributesData} from './helpers/b3dm-converter'; import {I3STileHeader} from '@loaders.gl/i3s/src/types'; import {getNodeCount, loadFromArchive, loadI3SContent, openSLPK} from './helpers/load-i3s'; import {I3SLoaderOptions} from '@loaders.gl/i3s/src/i3s-loader'; @@ -296,7 +294,9 @@ export default class Tiles3DConverter { }; const converter = - this.options.tilesVersion === '1.0' ? new B3dmConverter() : new GltfConverter(); + this.options.tilesVersion === '1.0' + ? new GltfConverter({tilesVersion: '1.0'}) + : new GltfConverter(); const b3dm = await converter.convert(i3sAttributesData, featureAttributes); await this.conversionDump.addNode(`${sourceChild.id}.${this.fileExt}`, sourceChild.id); diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts index c78dba0a91..c843352b8c 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts @@ -1,7 +1,354 @@ -import {TilesConverter} from './tiles-converter'; +/* eslint-disable no-console */ -export default class B3dmConverter extends TilesConverter { - constructor() { - super({tilesVersion: '1.0'}); +import type {I3STileContent, FeatureAttribute} from '@loaders.gl/i3s'; +import {encodeSync} from '@loaders.gl/core'; +import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf'; +import {Tile3DWriter} from '@loaders.gl/3d-tiles'; +import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; +import {Matrix4, Vector3} from '@math.gl/core'; +import {Ellipsoid} from '@math.gl/geospatial'; +import {convertTextureAtlas} from './texture-atlas'; +import {generateSyntheticIndices} from '../../lib/utils/geometry-utils'; + +const Z_UP_TO_Y_UP_MATRIX = new Matrix4([1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]); +const scratchVector = new Vector3(); +const KHR_MATERIALS_UNLIT = 'KHR_materials_unlit'; +const METALLIC_FACTOR_DEFAULT = 1.0; +const ROUGHNESS_FACTOR_DEFAULT = 1.0; + +export type I3SAttributesData = { + tileContent: I3STileContent; + box: number[]; + textureFormat: string; +}; + +/** + * Converts content of an I3S node to 3D Tiles file content + */ +export class GltfConverter { + // @ts-expect-error + rtcCenter: Float32Array; + i3sTile: any; + tilesVersion: string; + + constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { + this.tilesVersion = options.tilesVersion; + } + + /** + * The starter of content conversion + * @param i3sTile - Tile3D instance for I3S node + * @returns - encoded content + */ + async convert( + i3sAttributesData: I3SAttributesData, + featureAttributes: FeatureAttribute | null = null + ): Promise { + const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes); + const tiles = encodeSync( + { + gltfEncoded: new Uint8Array(gltf), + type: this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF, + featuresLength: this._getFeaturesLength(featureAttributes), + batchTable: featureAttributes + }, + Tile3DWriter + ); + return tiles; + } + + /** + * Build and encode gltf + * @param i3sTile - Tile3D instance for I3S node + * @returns - encoded glb content + */ + // eslint-disable-next-line max-statements + async buildGLTF( + i3sAttributesData: I3SAttributesData, + featureAttributes: FeatureAttribute | null + ): Promise { + const {tileContent, textureFormat, box} = i3sAttributesData; + const {material, attributes, indices: originalIndices, modelMatrix} = tileContent; + const gltfBuilder = new GLTFScenegraph(); + + const textureIndex = await this._addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder); + + // Add KHR_MATERIALS_UNLIT extension in the following cases: + // - metallicFactor or roughnessFactor are set to default values + // - metallicFactor or roughnessFactor are not set + const pbrMetallicRoughness = material?.pbrMetallicRoughness; + if ( + pbrMetallicRoughness && + (pbrMetallicRoughness.metallicFactor === undefined || + pbrMetallicRoughness.metallicFactor === METALLIC_FACTOR_DEFAULT) && + (pbrMetallicRoughness.roughnessFactor === undefined || + pbrMetallicRoughness.roughnessFactor === ROUGHNESS_FACTOR_DEFAULT) + ) { + gltfBuilder.addObjectExtension(material, KHR_MATERIALS_UNLIT, {}); + gltfBuilder.addExtension(KHR_MATERIALS_UNLIT); + } + + const pbrMaterialInfo = this._convertI3sMaterialToGLTFMaterial(material, textureIndex); + const materialIndex = gltfBuilder.addMaterial(pbrMaterialInfo); + + const positions = attributes.positions; + const positionsValue = positions.value; + + if (attributes.uvRegions && attributes.texCoords) { + attributes.texCoords.value = convertTextureAtlas( + attributes.texCoords.value, + attributes.uvRegions.value + ); + } + + const cartesianOrigin = new Vector3(box); + const cartographicOrigin = Ellipsoid.WGS84.cartesianToCartographic( + cartesianOrigin, + new Vector3() + ); + + attributes.positions.value = this._normalizePositions( + positionsValue, + cartesianOrigin, + cartographicOrigin, + modelMatrix + ); + this._createBatchIds(tileContent, featureAttributes); + if (attributes.normals && !this._checkNormals(attributes.normals.value)) { + delete attributes.normals; + } + const indices = + originalIndices || generateSyntheticIndices(positionsValue.length / positions.size); + const meshIndex = gltfBuilder.addMesh({ + attributes, + indices, + material: materialIndex, + mode: 4 + }); + const transformMatrix = this._generateTransformMatrix(cartesianOrigin); + const nodeIndex = gltfBuilder.addNode({meshIndex, matrix: transformMatrix}); + const sceneIndex = gltfBuilder.addScene({nodeIndices: [nodeIndex]}); + gltfBuilder.setDefaultScene(sceneIndex); + + gltfBuilder.createBinaryChunk(); + + const gltfBuffer = encodeSync(gltfBuilder.gltf, GLTFWriter); + + return gltfBuffer; + } + + /** + * Update gltfBuilder with texture from I3S tile + * @param {object} i3sTile - Tile3D object + * @param {GLTFScenegraph} gltfBuilder - gltfScenegraph instance to construct GLTF + * @returns {Promise} - GLTF texture index + */ + async _addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder) { + const {texture, material, attributes} = tileContent; + let textureIndex = null; + let selectedTexture = texture; + if (!texture && material) { + selectedTexture = + material.pbrMetallicRoughness && + material.pbrMetallicRoughness.baseColorTexture && + material.pbrMetallicRoughness.baseColorTexture.texture.source.image; + } + if (selectedTexture) { + const mimeType = this._deduceMimeTypeFromFormat(textureFormat); + const imageIndex = gltfBuilder.addImage(selectedTexture, mimeType); + textureIndex = gltfBuilder.addTexture({imageIndex}); + delete attributes.colors; + } + return textureIndex; + } + + /** + * Generate a positions array which is correct for 3DTiles/GLTF format + * @param {Float64Array} positionsValue - the input geometry positions array + * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system + * @param {number[]} cartographicOrigin - the tile center in the cartographic coordinate system + * @param {number[]} modelMatrix - the model matrix of geometry + * @returns {Float32Array} - the output geometry positions array + */ + _normalizePositions(positionsValue, cartesianOrigin, cartographicOrigin, modelMatrix) { + const newPositionsValue = new Float32Array(positionsValue.length); + for (let index = 0; index < positionsValue.length; index += 3) { + const vertex = positionsValue.subarray(index, index + 3); + const cartesianOriginVector = new Vector3(cartesianOrigin); + let vertexVector = new Vector3(Array.from(vertex)) + .transform(modelMatrix) + .add(cartographicOrigin); + Ellipsoid.WGS84.cartographicToCartesian(vertexVector, scratchVector); + vertexVector = scratchVector.subtract(cartesianOriginVector); + newPositionsValue.set(vertexVector, index); + } + return newPositionsValue; + } + + /** + * Generate the transformation matrix for GLTF node: + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node + * 1. Create the translate transformation from cartesianOrigin (the positions array stores offsets from this cartesianOrigin) + * 2. Create the rotation transformation to rotate model from z-up coordinates (I3S specific) to y-up coordinates (GLTF specific) + * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system + * @returns {Matrix4} - an array of 16 numbers (4x4 matrix) + */ + _generateTransformMatrix(cartesianOrigin) { + const translateOriginMatrix = new Matrix4().translate(cartesianOrigin); + const result = translateOriginMatrix.multiplyLeft(Z_UP_TO_Y_UP_MATRIX); + return result; + } + + /** + * Create _BATCHID attribute + * @param {Object} i3sContent - the source object + * @returns {void} + */ + _createBatchIds(i3sContent, featureAttributes) { + const {featureIds} = i3sContent; + const {OBJECTID: objectIds} = featureAttributes || {}; + if (!featureIds || !objectIds) { + return; + } + + for (let i = 0; i < featureIds.length; i++) { + const featureId = featureIds[i]; + const batchId = objectIds.indexOf(featureId); + featureIds[i] = batchId; + } + + i3sContent.attributes._BATCHID = { + size: 1, + byteOffset: 0, + value: featureIds + }; + } + + /** + * Deduce mime type by format from `textureSetDefinition.formats[0].format` + * https://github.com/Esri/i3s-spec/blob/master/docs/1.7/textureSetDefinitionFormat.cmn.md + * @param {string} format - format name + * @returns {string} mime type. + */ + _deduceMimeTypeFromFormat(format) { + switch (format) { + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'ktx2': + return 'image/ktx2'; + default: + console.warn(`Unexpected texture format in I3S: ${format}`); // eslint-disable-line no-console, no-undef + return 'image/jpeg'; + } + } + + /** + * Convert i3s material to GLTF compatible material + * @param {object} material - i3s material definition + * @param {number | null} textureIndex - texture index in GLTF + * @returns {object} GLTF material + */ + _convertI3sMaterialToGLTFMaterial(material, textureIndex) { + const isTextureIndexExists = textureIndex !== null; + + if (!material) { + material = { + alphaMode: 'OPAQUE', + doubleSided: false, + pbrMetallicRoughness: { + metallicFactor: 0, + roughnessFactor: 1 + } + }; + + if (isTextureIndexExists) { + material.pbrMetallicRoughness.baseColorTexture = { + index: textureIndex, + texCoord: 0 + }; + } else { + material.pbrMetallicRoughness.baseColorFactor = [1, 1, 1, 1]; + } + + return material; + } + + if (textureIndex !== null) { + material = this._setGLTFTexture(material, textureIndex); + } + + return material; + } + + /** + * Set texture properties in material with GLTF textureIndex + * @param {object} materialDefinition - i3s material definition + * @param {number} textureIndex - texture index in GLTF + * @returns {void} + */ + _setGLTFTexture(materialDefinition, textureIndex) { + const material = { + ...materialDefinition, + pbrMetallicRoughness: {...materialDefinition.pbrMetallicRoughness} + }; + // I3SLoader now support loading only one texture. This elseif sequence will assign this texture to one of + // properties defined in materialDefinition + if ( + materialDefinition.pbrMetallicRoughness && + materialDefinition.pbrMetallicRoughness.baseColorTexture + ) { + material.pbrMetallicRoughness.baseColorTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if (materialDefinition.emissiveTexture) { + material.emissiveTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if ( + materialDefinition.pbrMetallicRoughness && + materialDefinition.pbrMetallicRoughness.metallicRoughnessTexture + ) { + material.pbrMetallicRoughness.metallicRoughnessTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if (materialDefinition.normalTexture) { + material.normalTexture = { + index: textureIndex, + texCoord: 0 + }; + } else if (materialDefinition.occlusionTexture) { + material.occlusionTexture = { + index: textureIndex, + texCoord: 0 + }; + } + return material; + } + + /* + * Returns Features length based on attribute array in attribute object. + * @param {Object} attributes + * @returns {Number} Features length . + */ + _getFeaturesLength(attributes) { + if (!attributes) { + return 0; + } + const firstKey = Object.keys(attributes)[0]; + return firstKey ? attributes[firstKey].length : 0; + } + + /* Checks that normals buffer is correct + * @param {TypedArray} normals + * @returns {boolean} true - normals are correct; false - normals are incorrect + */ + _checkNormals(normals) { + // If all normals === 0, the resulting tileset is all in black colors on Cesium + return normals.find((value) => value); } } diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts deleted file mode 100644 index 2900463c91..0000000000 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {TilesConverter} from './tiles-converter'; - -export default class GltfConverter extends TilesConverter { - constructor() { - super({tilesVersion: '1.1'}); - } -} diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts deleted file mode 100644 index f762da1eec..0000000000 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/tiles-converter.ts +++ /dev/null @@ -1,354 +0,0 @@ -/* eslint-disable no-console */ - -import type {I3STileContent, FeatureAttribute} from '@loaders.gl/i3s'; -import {encodeSync} from '@loaders.gl/core'; -import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf'; -import {Tile3DWriter} from '@loaders.gl/3d-tiles'; -import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; -import {Matrix4, Vector3} from '@math.gl/core'; -import {Ellipsoid} from '@math.gl/geospatial'; -import {convertTextureAtlas} from './texture-atlas'; -import {generateSyntheticIndices} from '../../lib/utils/geometry-utils'; - -const Z_UP_TO_Y_UP_MATRIX = new Matrix4([1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]); -const scratchVector = new Vector3(); -const KHR_MATERIALS_UNLIT = 'KHR_materials_unlit'; -const METALLIC_FACTOR_DEFAULT = 1.0; -const ROUGHNESS_FACTOR_DEFAULT = 1.0; - -export type I3SAttributesData = { - tileContent: I3STileContent; - box: number[]; - textureFormat: string; -}; - -/** - * Converts content of an I3S node to 3D Tiles file content - */ -export class TilesConverter { - // @ts-expect-error - rtcCenter: Float32Array; - i3sTile: any; - tilesVersion: string; - - protected constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { - this.tilesVersion = options.tilesVersion; - } - - /** - * The starter of content conversion - * @param i3sTile - Tile3D instance for I3S node - * @returns - encoded content - */ - async convert( - i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null = null - ): Promise { - const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes); - const tiles = encodeSync( - { - gltfEncoded: new Uint8Array(gltf), - type: this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF, - featuresLength: this._getFeaturesLength(featureAttributes), - batchTable: featureAttributes - }, - Tile3DWriter - ); - return tiles; - } - - /** - * Build and encode gltf - * @param i3sTile - Tile3D instance for I3S node - * @returns - encoded glb content - */ - // eslint-disable-next-line max-statements - async buildGLTF( - i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null - ): Promise { - const {tileContent, textureFormat, box} = i3sAttributesData; - const {material, attributes, indices: originalIndices, modelMatrix} = tileContent; - const gltfBuilder = new GLTFScenegraph(); - - const textureIndex = await this._addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder); - - // Add KHR_MATERIALS_UNLIT extension in the following cases: - // - metallicFactor or roughnessFactor are set to default values - // - metallicFactor or roughnessFactor are not set - const pbrMetallicRoughness = material?.pbrMetallicRoughness; - if ( - pbrMetallicRoughness && - (pbrMetallicRoughness.metallicFactor === undefined || - pbrMetallicRoughness.metallicFactor === METALLIC_FACTOR_DEFAULT) && - (pbrMetallicRoughness.roughnessFactor === undefined || - pbrMetallicRoughness.roughnessFactor === ROUGHNESS_FACTOR_DEFAULT) - ) { - gltfBuilder.addObjectExtension(material, KHR_MATERIALS_UNLIT, {}); - gltfBuilder.addExtension(KHR_MATERIALS_UNLIT); - } - - const pbrMaterialInfo = this._convertI3sMaterialToGLTFMaterial(material, textureIndex); - const materialIndex = gltfBuilder.addMaterial(pbrMaterialInfo); - - const positions = attributes.positions; - const positionsValue = positions.value; - - if (attributes.uvRegions && attributes.texCoords) { - attributes.texCoords.value = convertTextureAtlas( - attributes.texCoords.value, - attributes.uvRegions.value - ); - } - - const cartesianOrigin = new Vector3(box); - const cartographicOrigin = Ellipsoid.WGS84.cartesianToCartographic( - cartesianOrigin, - new Vector3() - ); - - attributes.positions.value = this._normalizePositions( - positionsValue, - cartesianOrigin, - cartographicOrigin, - modelMatrix - ); - this._createBatchIds(tileContent, featureAttributes); - if (attributes.normals && !this._checkNormals(attributes.normals.value)) { - delete attributes.normals; - } - const indices = - originalIndices || generateSyntheticIndices(positionsValue.length / positions.size); - const meshIndex = gltfBuilder.addMesh({ - attributes, - indices, - material: materialIndex, - mode: 4 - }); - const transformMatrix = this._generateTransformMatrix(cartesianOrigin); - const nodeIndex = gltfBuilder.addNode({meshIndex, matrix: transformMatrix}); - const sceneIndex = gltfBuilder.addScene({nodeIndices: [nodeIndex]}); - gltfBuilder.setDefaultScene(sceneIndex); - - gltfBuilder.createBinaryChunk(); - - const gltfBuffer = encodeSync(gltfBuilder.gltf, GLTFWriter); - - return gltfBuffer; - } - - /** - * Update gltfBuilder with texture from I3S tile - * @param {object} i3sTile - Tile3D object - * @param {GLTFScenegraph} gltfBuilder - gltfScenegraph instance to construct GLTF - * @returns {Promise} - GLTF texture index - */ - async _addI3sTextureToGLTF(tileContent, textureFormat, gltfBuilder) { - const {texture, material, attributes} = tileContent; - let textureIndex = null; - let selectedTexture = texture; - if (!texture && material) { - selectedTexture = - material.pbrMetallicRoughness && - material.pbrMetallicRoughness.baseColorTexture && - material.pbrMetallicRoughness.baseColorTexture.texture.source.image; - } - if (selectedTexture) { - const mimeType = this._deduceMimeTypeFromFormat(textureFormat); - const imageIndex = gltfBuilder.addImage(selectedTexture, mimeType); - textureIndex = gltfBuilder.addTexture({imageIndex}); - delete attributes.colors; - } - return textureIndex; - } - - /** - * Generate a positions array which is correct for 3DTiles/GLTF format - * @param {Float64Array} positionsValue - the input geometry positions array - * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system - * @param {number[]} cartographicOrigin - the tile center in the cartographic coordinate system - * @param {number[]} modelMatrix - the model matrix of geometry - * @returns {Float32Array} - the output geometry positions array - */ - _normalizePositions(positionsValue, cartesianOrigin, cartographicOrigin, modelMatrix) { - const newPositionsValue = new Float32Array(positionsValue.length); - for (let index = 0; index < positionsValue.length; index += 3) { - const vertex = positionsValue.subarray(index, index + 3); - const cartesianOriginVector = new Vector3(cartesianOrigin); - let vertexVector = new Vector3(Array.from(vertex)) - .transform(modelMatrix) - .add(cartographicOrigin); - Ellipsoid.WGS84.cartographicToCartesian(vertexVector, scratchVector); - vertexVector = scratchVector.subtract(cartesianOriginVector); - newPositionsValue.set(vertexVector, index); - } - return newPositionsValue; - } - - /** - * Generate the transformation matrix for GLTF node: - * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node - * 1. Create the translate transformation from cartesianOrigin (the positions array stores offsets from this cartesianOrigin) - * 2. Create the rotation transformation to rotate model from z-up coordinates (I3S specific) to y-up coordinates (GLTF specific) - * @param {number[]} cartesianOrigin - the tile center in the cartesian coordinate system - * @returns {Matrix4} - an array of 16 numbers (4x4 matrix) - */ - _generateTransformMatrix(cartesianOrigin) { - const translateOriginMatrix = new Matrix4().translate(cartesianOrigin); - const result = translateOriginMatrix.multiplyLeft(Z_UP_TO_Y_UP_MATRIX); - return result; - } - - /** - * Create _BATCHID attribute - * @param {Object} i3sContent - the source object - * @returns {void} - */ - _createBatchIds(i3sContent, featureAttributes) { - const {featureIds} = i3sContent; - const {OBJECTID: objectIds} = featureAttributes || {}; - if (!featureIds || !objectIds) { - return; - } - - for (let i = 0; i < featureIds.length; i++) { - const featureId = featureIds[i]; - const batchId = objectIds.indexOf(featureId); - featureIds[i] = batchId; - } - - i3sContent.attributes._BATCHID = { - size: 1, - byteOffset: 0, - value: featureIds - }; - } - - /** - * Deduce mime type by format from `textureSetDefinition.formats[0].format` - * https://github.com/Esri/i3s-spec/blob/master/docs/1.7/textureSetDefinitionFormat.cmn.md - * @param {string} format - format name - * @returns {string} mime type. - */ - _deduceMimeTypeFromFormat(format) { - switch (format) { - case 'jpg': - return 'image/jpeg'; - case 'png': - return 'image/png'; - case 'ktx2': - return 'image/ktx2'; - default: - console.warn(`Unexpected texture format in I3S: ${format}`); // eslint-disable-line no-console, no-undef - return 'image/jpeg'; - } - } - - /** - * Convert i3s material to GLTF compatible material - * @param {object} material - i3s material definition - * @param {number | null} textureIndex - texture index in GLTF - * @returns {object} GLTF material - */ - _convertI3sMaterialToGLTFMaterial(material, textureIndex) { - const isTextureIndexExists = textureIndex !== null; - - if (!material) { - material = { - alphaMode: 'OPAQUE', - doubleSided: false, - pbrMetallicRoughness: { - metallicFactor: 0, - roughnessFactor: 1 - } - }; - - if (isTextureIndexExists) { - material.pbrMetallicRoughness.baseColorTexture = { - index: textureIndex, - texCoord: 0 - }; - } else { - material.pbrMetallicRoughness.baseColorFactor = [1, 1, 1, 1]; - } - - return material; - } - - if (textureIndex !== null) { - material = this._setGLTFTexture(material, textureIndex); - } - - return material; - } - - /** - * Set texture properties in material with GLTF textureIndex - * @param {object} materialDefinition - i3s material definition - * @param {number} textureIndex - texture index in GLTF - * @returns {void} - */ - _setGLTFTexture(materialDefinition, textureIndex) { - const material = { - ...materialDefinition, - pbrMetallicRoughness: {...materialDefinition.pbrMetallicRoughness} - }; - // I3SLoader now support loading only one texture. This elseif sequence will assign this texture to one of - // properties defined in materialDefinition - if ( - materialDefinition.pbrMetallicRoughness && - materialDefinition.pbrMetallicRoughness.baseColorTexture - ) { - material.pbrMetallicRoughness.baseColorTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if (materialDefinition.emissiveTexture) { - material.emissiveTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if ( - materialDefinition.pbrMetallicRoughness && - materialDefinition.pbrMetallicRoughness.metallicRoughnessTexture - ) { - material.pbrMetallicRoughness.metallicRoughnessTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if (materialDefinition.normalTexture) { - material.normalTexture = { - index: textureIndex, - texCoord: 0 - }; - } else if (materialDefinition.occlusionTexture) { - material.occlusionTexture = { - index: textureIndex, - texCoord: 0 - }; - } - return material; - } - - /* - * Returns Features length based on attribute array in attribute object. - * @param {Object} attributes - * @returns {Number} Features length . - */ - _getFeaturesLength(attributes) { - if (!attributes) { - return 0; - } - const firstKey = Object.keys(attributes)[0]; - return firstKey ? attributes[firstKey].length : 0; - } - - /* Checks that normals buffer is correct - * @param {TypedArray} normals - * @returns {boolean} true - normals are correct; false - normals are incorrect - */ - _checkNormals(normals) { - // If all normals === 0, the resulting tileset is all in black colors on Cesium - return normals.find((value) => value); - } -} diff --git a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js b/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js index ce1a547115..d6a7c45885 100644 --- a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js +++ b/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js @@ -1,7 +1,7 @@ import test from 'tape-promise/tape'; import {Tiles3DLoader} from '@loaders.gl/3d-tiles'; import {loadI3STile} from '@loaders.gl/i3s/test/test-utils/load-utils'; -import B3dmConverter from '../../../src/3d-tiles-converter/helpers/b3dm-converter'; +import {GltfConverter} from '../../../src/3d-tiles-converter/helpers/b3dm-converter'; import {isBrowser, parse} from '@loaders.gl/core'; import {load} from '@loaders.gl/core'; import {I3SAttributeLoader, COORDINATE_SYSTEM} from '@loaders.gl/i3s'; @@ -57,7 +57,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to const i3sContent = tile.content; t.ok(i3sContent); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, @@ -86,7 +86,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should normalise positions corre const i3sContent = tile.content; const originPositions = i3sContent.attributes.positions.value; const cartographicOrigin = i3sContent.cartographicOrigin; - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -121,7 +121,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should add KHR_materials_unlit e i3sContent.material.pbrMetallicRoughness.metallicFactor = 1.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 1.0; - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -153,7 +153,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl i3sContent.material.pbrMetallicRoughness.metallicFactor = 2.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 2.0; - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -178,7 +178,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl test('tile-converter(3d-tiles)#b3dm converter - should convert material', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -205,7 +205,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert material', async test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -246,7 +246,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect nor test('tile-converter(3d-tiles)#b3dm converter - should handle geometry without normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); delete tile.content.attributes.normals; const encodedContent = await b3dmConverter.convert({ @@ -280,7 +280,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to t.equal(tile.header.textureFormat, 'ktx2'); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, @@ -298,7 +298,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should generate batchIds during if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new B3dmConverter(); + const b3dmConverter = new GltfConverter(); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, From 733f4811c8716eff0f11a508d068330dee54a18a Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Wed, 11 Sep 2024 17:43:47 +0300 Subject: [PATCH 4/8] address dev review issues --- .../src/lib/encoders/encode-3d-tile-gltf.ts | 18 -- .../src/lib/encoders/encode-3d-tile.ts | 3 - .../3d-tiles-converter/3d-tiles-converter.ts | 10 +- .../{b3dm-converter.ts => gltf-converter.ts} | 165 +++++++++++++++--- modules/tile-converter/src/converter-cli.ts | 6 +- .../helpers/b3dm-converter.spec.js | 20 +-- 6 files changed, 166 insertions(+), 56 deletions(-) delete mode 100644 modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts rename modules/tile-converter/src/3d-tiles-converter/helpers/{b3dm-converter.ts => gltf-converter.ts} (72%) diff --git a/modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts b/modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts deleted file mode 100644 index 34132dc652..0000000000 --- a/modules/3d-tiles/src/lib/encoders/encode-3d-tile-gltf.ts +++ /dev/null @@ -1,18 +0,0 @@ -// loaders.gl -// SPDX-License-Identifier: MIT AND Apache-2.0 -// Copyright vis.gl contributors - -// 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 {copyBinaryToDataView} from '@loaders.gl/loader-utils'; - -// Procedurally encode the tile array dataView for testing purposes -export function encodeGltf3DTile(tile, dataView, byteOffset, options) { - const gltfEncoded = tile.gltfEncoded; - if (gltfEncoded) { - byteOffset = copyBinaryToDataView(dataView, byteOffset, gltfEncoded, gltfEncoded.byteLength); - } - - return byteOffset; -} diff --git a/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts b/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts index d9b6db27cb..b746df5164 100644 --- a/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts +++ b/modules/3d-tiles/src/lib/encoders/encode-3d-tile.ts @@ -12,7 +12,6 @@ import {encodeComposite3DTile} from './encode-3d-tile-composite'; import {encodeBatchedModel3DTile} from './encode-3d-tile-batched-model'; import {encodeInstancedModel3DTile} from './encode-3d-tile-instanced-model'; import {encodePointCloud3DTile} from './encode-3d-tile-point-cloud'; -import {encodeGltf3DTile} from './encode-3d-tile-gltf'; export default function encode3DTile(tile, options) { const byteLength = encode3DTileToDataView(tile, null, 0, options); @@ -34,8 +33,6 @@ function encode3DTileToDataView(tile, dataView, byteOffset, options) { return encodeBatchedModel3DTile(tile, dataView, byteOffset, options); case TILE3D_TYPE.INSTANCED_3D_MODEL: return encodeInstancedModel3DTile(tile, dataView, byteOffset, options); - case TILE3D_TYPE.GLTF: - return encodeGltf3DTile(tile, dataView, byteOffset, options); default: throw new Error('3D Tiles: unknown tile type'); } diff --git a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts index 1ebc90ec28..c671560ac0 100644 --- a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts @@ -22,7 +22,7 @@ import {TILESET as tilesetTemplate} from './json-templates/tileset'; import {createObbFromMbs} from '../i3s-converter/helpers/coordinate-converter'; import {WorkerFarm} from '@loaders.gl/worker-utils'; import {BROWSER_ERROR_MESSAGE} from '../constants'; -import {GltfConverter, type I3SAttributesData} from './helpers/b3dm-converter'; +import {GltfConverter, type I3SAttributesData} from './helpers/gltf-converter'; import {I3STileHeader} from '@loaders.gl/i3s/src/types'; import {getNodeCount, loadFromArchive, loadI3SContent, openSLPK} from './helpers/load-i3s'; import {I3SLoaderOptions} from '@loaders.gl/i3s/src/i3s-loader'; @@ -72,7 +72,7 @@ export default class Tiles3DConverter { this.workerSource = {}; this.conversionDump = new ConversionDump(); this.progress = new Progress(); - this.fileExt = 'b3dm'; + this.fileExt = ''; } /** @@ -297,7 +297,11 @@ export default class Tiles3DConverter { this.options.tilesVersion === '1.0' ? new GltfConverter({tilesVersion: '1.0'}) : new GltfConverter(); - const b3dm = await converter.convert(i3sAttributesData, featureAttributes); + const b3dm = await converter.convert( + i3sAttributesData, + featureAttributes, + this.attributeStorageInfo + ); await this.conversionDump.addNode(`${sourceChild.id}.${this.fileExt}`, sourceChild.id); await writeFile(this.tilesetPath, new Uint8Array(b3dm), `${sourceChild.id}.${this.fileExt}`); diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts similarity index 72% rename from modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts rename to modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts index c843352b8c..77ce90e392 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/b3dm-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts @@ -1,8 +1,12 @@ -/* eslint-disable no-console */ - -import type {I3STileContent, FeatureAttribute} from '@loaders.gl/i3s'; +import type {I3STileContent, FeatureAttribute, AttributeStorageInfo} from '@loaders.gl/i3s'; import {encodeSync} from '@loaders.gl/core'; -import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf'; +import { + GLTFScenegraph, + GLTFWriter, + createExtStructuralMetadata, + createExtMeshFeatures, + type PropertyAttribute +} from '@loaders.gl/gltf'; import {Tile3DWriter} from '@loaders.gl/3d-tiles'; import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; import {Matrix4, Vector3} from '@math.gl/core'; @@ -30,9 +34,11 @@ export class GltfConverter { rtcCenter: Float32Array; i3sTile: any; tilesVersion: string; + tileType: string; constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { this.tilesVersion = options.tilesVersion; + this.tileType = this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF; } /** @@ -42,19 +48,26 @@ export class GltfConverter { */ async convert( i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null = null + featureAttributes: FeatureAttribute | null = null, + attributeStorageInfo?: AttributeStorageInfo[] | null | undefined ): Promise { - const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes); - const tiles = encodeSync( - { - gltfEncoded: new Uint8Array(gltf), - type: this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF, - featuresLength: this._getFeaturesLength(featureAttributes), - batchTable: featureAttributes - }, - Tile3DWriter - ); - return tiles; + const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes, attributeStorageInfo); + + if (this.tileType === TILE3D_TYPE.BATCHED_3D_MODEL) { + const b3dm = encodeSync( + { + gltfEncoded: new Uint8Array(gltf), + type: 'b3dm', + featuresLength: this._getFeaturesLength(featureAttributes), + batchTable: featureAttributes + }, + Tile3DWriter + ); + return b3dm; + } else if (this.tileType === TILE3D_TYPE.GLTF) { + return gltf; + } + return new ArrayBuffer(0); } /** @@ -62,10 +75,11 @@ export class GltfConverter { * @param i3sTile - Tile3D instance for I3S node * @returns - encoded glb content */ - // eslint-disable-next-line max-statements + // eslint-disable-next-line complexity, max-statements async buildGLTF( i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null + featureAttributes: FeatureAttribute | null, + attributeStorageInfo?: AttributeStorageInfo[] | null | undefined ): Promise { const {tileContent, textureFormat, box} = i3sAttributesData; const {material, attributes, indices: originalIndices, modelMatrix} = tileContent; @@ -113,6 +127,7 @@ export class GltfConverter { cartographicOrigin, modelMatrix ); + this._createBatchIds(tileContent, featureAttributes); if (attributes.normals && !this._checkNormals(attributes.normals.value)) { delete attributes.normals; @@ -125,16 +140,126 @@ export class GltfConverter { material: materialIndex, mode: 4 }); + if (this.tileType === TILE3D_TYPE.GLTF) { + this._createMetadataExtensions( + gltfBuilder, + meshIndex, + featureAttributes, + attributeStorageInfo, + tileContent + ); + } const transformMatrix = this._generateTransformMatrix(cartesianOrigin); const nodeIndex = gltfBuilder.addNode({meshIndex, matrix: transformMatrix}); const sceneIndex = gltfBuilder.addScene({nodeIndices: [nodeIndex]}); gltfBuilder.setDefaultScene(sceneIndex); gltfBuilder.createBinaryChunk(); + return encodeSync(gltfBuilder.gltf, GLTFWriter, {gltfBuilder}); + } - const gltfBuffer = encodeSync(gltfBuilder.gltf, GLTFWriter); + _createMetadataExtensions( + gltfBuilder: GLTFScenegraph, + meshIndex: number, + featureAttributes: FeatureAttribute | null, + attributeStorageInfo: AttributeStorageInfo[] | null | undefined, + tileContent: I3STileContent + ) { + const propertyAttributes = this._createPropertyAttibutes( + featureAttributes, + attributeStorageInfo + ); + const tableIndex = createExtStructuralMetadata(gltfBuilder, propertyAttributes); + + const mesh = gltfBuilder.getMesh(meshIndex); + for (const primitive of mesh.primitives) { + if (tileContent.attributes._BATCHID?.value) { + createExtMeshFeatures( + gltfBuilder, + primitive, + tileContent.attributes._BATCHID.value, + tableIndex + ); + } + } + } - return gltfBuffer; + _createPropertyAttibutes( + featureAttributes: FeatureAttribute | null, + attributeStorageInfo?: AttributeStorageInfo[] | null | undefined + ): PropertyAttribute[] { + if (!featureAttributes || !attributeStorageInfo) { + return []; + } + const propertyAttributeArray: PropertyAttribute[] = []; + for (const attributeName in featureAttributes) { + const propertyAttribute = this._convertAttributeStorageInfoToPropertyAttribute( + attributeName, + attributeStorageInfo, + featureAttributes + ); + if (propertyAttribute) { + propertyAttributeArray.push(propertyAttribute); + } + } + return propertyAttributeArray; + } + + _convertAttributeStorageInfoToPropertyAttribute( + attributeName: string, + attributeStorageInfo: AttributeStorageInfo[], + featureAttributes: FeatureAttribute + ): PropertyAttribute | null { + const attributes = featureAttributes[attributeName]; + const info = attributeStorageInfo.find((e) => e.name === attributeName); + if (!info) { + return null; + } + const attribute = info.attributeValues; + if (!attribute?.valueType) { + return null; + } + let elementType: string; + let componentType: string | undefined; + switch (attribute.valueType.toLowerCase()) { + case 'oid32': + elementType = 'SCALAR'; + componentType = 'UINT32'; + break; + case 'int32': + elementType = 'SCALAR'; + componentType = 'INT32'; + break; + case 'uint32': + elementType = 'SCALAR'; + componentType = 'UINT32'; + break; + case 'int16': + elementType = 'SCALAR'; + componentType = 'INT16'; + break; + case 'uint16': + elementType = 'SCALAR'; + componentType = 'UINT16'; + break; + case 'float64': + elementType = 'SCALAR'; + componentType = 'FLOAT64'; + break; + case 'string': + elementType = 'STRING'; + break; + default: + elementType = ''; + break; + } + const propertyAttribute: PropertyAttribute = { + name: attributeName, + elementType, + componentType, + values: attributes + }; + return propertyAttribute; } /** diff --git a/modules/tile-converter/src/converter-cli.ts b/modules/tile-converter/src/converter-cli.ts index fc721f29d2..134c01e41c 100644 --- a/modules/tile-converter/src/converter-cli.ts +++ b/modules/tile-converter/src/converter-cli.ts @@ -180,7 +180,7 @@ function printHelp(): void { '--tileset [tileset.json file (3DTiles) / http://..../SceneServer/layers/0 resource (I3S)]' ); console.log('--input-type [tileset input type: I3S or 3DTILES]'); - console.log('--tile-version [3dtile version: 1.0 or 1.1, default: 1.1]'); + console.log('--tiles-version [3dtile version: 1.0 or 1.1, default: 1.1]'); console.log( '--egm [location of Earth Gravity Model *.pgm file to convert heights from ellipsoidal to gravity-related format or "None" to not use it. A model file can be loaded from GeographicLib https://geographiclib.sourceforge.io/html/geoid.html], default: "./deps/egm2008-5.zip"' ); @@ -279,12 +279,14 @@ function validateOptions( addHash || (Boolean(value) && Object.values(TILESET_TYPE).includes(value.toUpperCase())) }, tilesVersion: { - getMessage: () => console.log('Incorrect: --tilesVersion [1.0 or 1.1]'), + getMessage: () => + console.log('Incorrect: --tiles-version [1.0 or 1.1] for --input-type "I3S" only'), condition: (value) => addHash || (Boolean(value) && Object.values(['1.0', '1.1']).includes(value) && Boolean(options.inputType === 'I3S')) || + Boolean(options.inputType !== 'I3S') || Boolean(options.analyze) } }; diff --git a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js b/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js index d6a7c45885..d74da61d81 100644 --- a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js +++ b/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js @@ -1,7 +1,7 @@ import test from 'tape-promise/tape'; import {Tiles3DLoader} from '@loaders.gl/3d-tiles'; import {loadI3STile} from '@loaders.gl/i3s/test/test-utils/load-utils'; -import {GltfConverter} from '../../../src/3d-tiles-converter/helpers/b3dm-converter'; +import {GltfConverter} from '../../../src/3d-tiles-converter/helpers/gltf-converter'; import {isBrowser, parse} from '@loaders.gl/core'; import {load} from '@loaders.gl/core'; import {I3SAttributeLoader, COORDINATE_SYSTEM} from '@loaders.gl/i3s'; @@ -57,7 +57,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to const i3sContent = tile.content; t.ok(i3sContent); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, @@ -86,7 +86,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should normalise positions corre const i3sContent = tile.content; const originPositions = i3sContent.attributes.positions.value; const cartographicOrigin = i3sContent.cartographicOrigin; - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -121,7 +121,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should add KHR_materials_unlit e i3sContent.material.pbrMetallicRoughness.metallicFactor = 1.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 1.0; - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -153,7 +153,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl i3sContent.material.pbrMetallicRoughness.metallicFactor = 2.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 2.0; - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -178,7 +178,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl test('tile-converter(3d-tiles)#b3dm converter - should convert material', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -205,7 +205,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert material', async test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -246,7 +246,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect nor test('tile-converter(3d-tiles)#b3dm converter - should handle geometry without normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); delete tile.content.attributes.normals; const encodedContent = await b3dmConverter.convert({ @@ -280,7 +280,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to t.equal(tile.header.textureFormat, 'ktx2'); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, @@ -298,7 +298,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should generate batchIds during if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new GltfConverter(); + const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, From dba69703bd4baceea90b2424f930900c1cadd859 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 12 Sep 2024 16:59:31 +0300 Subject: [PATCH 5/8] address dev review issues --- .../3d-tiles-converter/3d-tiles-converter.ts | 22 ++++++++--------- ...erter.ts => 3d-tiles-content-converter.ts} | 24 +++++++++---------- modules/tile-converter/src/converter-cli.ts | 18 +++++++------- .../helpers/b3dm-converter.spec.js | 20 ++++++++-------- 4 files changed, 42 insertions(+), 42 deletions(-) rename modules/tile-converter/src/3d-tiles-converter/helpers/{gltf-converter.ts => 3d-tiles-content-converter.ts} (96%) diff --git a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts index c671560ac0..642e8e03fc 100644 --- a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts @@ -22,7 +22,10 @@ import {TILESET as tilesetTemplate} from './json-templates/tileset'; import {createObbFromMbs} from '../i3s-converter/helpers/coordinate-converter'; import {WorkerFarm} from '@loaders.gl/worker-utils'; import {BROWSER_ERROR_MESSAGE} from '../constants'; -import {GltfConverter, type I3SAttributesData} from './helpers/gltf-converter'; +import { + Tiles3DContentConverter, + type I3SAttributesData +} from './helpers/3d-tiles-content-converter'; import {I3STileHeader} from '@loaders.gl/i3s/src/types'; import {getNodeCount, loadFromArchive, loadI3SContent, openSLPK} from './helpers/load-i3s'; import {I3SLoaderOptions} from '@loaders.gl/i3s/src/i3s-loader'; @@ -80,7 +83,7 @@ export default class Tiles3DConverter { * @param options * @param options.inputUrl the url to read the tileset from * @param options.outputPath the output filename - * @param options.tilesVersion the version of 3DTiles + * @param options.outputVersion the version of 3DTiles * @param options.tilesetName the output name of the tileset * @param options.egmFilePath location of *.pgm file to convert heights from ellipsoidal to gravity-related format * @param options.maxDepth The max tree depth of conversion @@ -90,7 +93,7 @@ export default class Tiles3DConverter { inputUrl: string; outputPath: string; tilesetName: string; - tilesVersion?: string; + outputVersion?: string; maxDepth?: number; egmFilePath: string; inquirer?: {prompt: PromptModule}; @@ -103,7 +106,7 @@ export default class Tiles3DConverter { const { inputUrl, outputPath, - tilesVersion, + outputVersion, tilesetName, maxDepth, egmFilePath, @@ -111,8 +114,8 @@ export default class Tiles3DConverter { analyze } = options; this.conversionStartTime = process.hrtime(); - this.options = {maxDepth, inquirer, tilesVersion}; - this.fileExt = this.options.tilesVersion === '1.0' ? 'b3dm' : 'glb'; + this.options = {maxDepth, inquirer, outputVersion}; + this.fileExt = this.options.outputVersion === '1.0' ? 'b3dm' : 'glb'; console.log('Loading egm file...'); // eslint-disable-line this.geoidHeightModel = await load(egmFilePath, PGMLoader); @@ -187,7 +190,7 @@ export default class Tiles3DConverter { await this._addChildren(rootNode, rootTile, 1); - const tileset = transform({asset: {version: tilesVersion}, root: rootTile}, tilesetTemplate()); + const tileset = transform({asset: {version: outputVersion}, root: rootTile}, tilesetTemplate()); await writeFile(this.tilesetPath, JSON.stringify(tileset), 'tileset.json'); await this.conversionDump.deleteDumpFile(); @@ -293,10 +296,7 @@ export default class Tiles3DConverter { textureFormat: sourceChild.textureFormat }; - const converter = - this.options.tilesVersion === '1.0' - ? new GltfConverter({tilesVersion: '1.0'}) - : new GltfConverter(); + const converter = new Tiles3DContentConverter({outputVersion: this.options.outputVersion}); const b3dm = await converter.convert( i3sAttributesData, featureAttributes, diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts similarity index 96% rename from modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts rename to modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts index 77ce90e392..07a3899b19 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/gltf-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts @@ -29,16 +29,16 @@ export type I3SAttributesData = { /** * Converts content of an I3S node to 3D Tiles file content */ -export class GltfConverter { +export class Tiles3DContentConverter { // @ts-expect-error rtcCenter: Float32Array; i3sTile: any; - tilesVersion: string; + outputVersion: string; tileType: string; - constructor(options: {tilesVersion: string} = {tilesVersion: '1.1'}) { - this.tilesVersion = options.tilesVersion; - this.tileType = this.tilesVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF; + constructor(options: {outputVersion: string} = {outputVersion: '1.1'}) { + this.outputVersion = options.outputVersion; + this.tileType = this.outputVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF; } /** @@ -64,10 +64,8 @@ export class GltfConverter { Tile3DWriter ); return b3dm; - } else if (this.tileType === TILE3D_TYPE.GLTF) { - return gltf; } - return new ArrayBuffer(0); + return gltf; } /** @@ -210,18 +208,18 @@ export class GltfConverter { attributeStorageInfo: AttributeStorageInfo[], featureAttributes: FeatureAttribute ): PropertyAttribute | null { - const attributes = featureAttributes[attributeName]; + const attributeValues = featureAttributes[attributeName]; const info = attributeStorageInfo.find((e) => e.name === attributeName); if (!info) { return null; } - const attribute = info.attributeValues; - if (!attribute?.valueType) { + const attributeMetadata = info.attributeValues; + if (!attributeMetadata?.valueType) { return null; } let elementType: string; let componentType: string | undefined; - switch (attribute.valueType.toLowerCase()) { + switch (attributeMetadata.valueType.toLowerCase()) { case 'oid32': elementType = 'SCALAR'; componentType = 'UINT32'; @@ -257,7 +255,7 @@ export class GltfConverter { name: attributeName, elementType, componentType, - values: attributes + values: attributeValues }; return propertyAttribute; } diff --git a/modules/tile-converter/src/converter-cli.ts b/modules/tile-converter/src/converter-cli.ts index 134c01e41c..78e57bb367 100644 --- a/modules/tile-converter/src/converter-cli.ts +++ b/modules/tile-converter/src/converter-cli.ts @@ -28,7 +28,7 @@ type TileConversionOptions = { output: string; /** 3DTile version. * Default: version "1.1" */ - tilesVersion?: string; + outputVersion?: string; /** Keep created 3DNodeIndexDocument files on disk instead of memory. This option reduce memory usage but decelerates conversion speed */ instantNodeWriting: boolean; /** Try to merge similar materials to be able to merge meshes into one node (I3S to 3DTiles conversion only) */ @@ -180,7 +180,9 @@ function printHelp(): void { '--tileset [tileset.json file (3DTiles) / http://..../SceneServer/layers/0 resource (I3S)]' ); console.log('--input-type [tileset input type: I3S or 3DTILES]'); - console.log('--tiles-version [3dtile version: 1.0 or 1.1, default: 1.1]'); + console.log( + '--output-version [3dtile version: 1.0 or 1.1, default: 1.1]. This option supports only 1.0/1.1 values for 3DTiles output. I3S output version setting is not supported yet.' + ); console.log( '--egm [location of Earth Gravity Model *.pgm file to convert heights from ellipsoidal to gravity-related format or "None" to not use it. A model file can be loaded from GeographicLib https://geographiclib.sourceforge.io/html/geoid.html], default: "./deps/egm2008-5.zip"' ); @@ -216,7 +218,7 @@ async function convert(options: ValidatedTileConversionOptions) { await tiles3DConverter.convert({ inputUrl: options.tileset, outputPath: options.output, - tilesVersion: options.tilesVersion, + outputVersion: options.outputVersion, tilesetName: options.name, maxDepth: options.maxDepth, egmFilePath: options.egm, @@ -278,9 +280,9 @@ function validateOptions( condition: (value) => addHash || (Boolean(value) && Object.values(TILESET_TYPE).includes(value.toUpperCase())) }, - tilesVersion: { + outputVersion: { getMessage: () => - console.log('Incorrect: --tiles-version [1.0 or 1.1] for --input-type "I3S" only'), + console.log('Incorrect: --output-version [1.0 or 1.1] is for --input-type "I3S" only'), condition: (value) => addHash || (Boolean(value) && @@ -316,7 +318,7 @@ function validateOptions( function parseOptions(args: string[]): TileConversionOptions { const opts: TileConversionOptions = { output: 'data', - tilesVersion: '1.1', + outputVersion: '1.1', instantNodeWriting: false, mergeMaterials: true, egm: join(process.cwd(), 'deps', 'egm2008-5.pgm'), @@ -345,8 +347,8 @@ function parseOptions(args: string[]): TileConversionOptions { case '--output': opts.output = getStringValue(index, args); break; - case '--tiles-version': - opts.tilesVersion = getStringValue(index, args); + case '--output-version': + opts.outputVersion = getStringValue(index, args); break; case '--instant-node-writing': opts.instantNodeWriting = getBooleanValue(index, args); diff --git a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js b/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js index d74da61d81..4d5d63e454 100644 --- a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js +++ b/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js @@ -1,7 +1,7 @@ import test from 'tape-promise/tape'; import {Tiles3DLoader} from '@loaders.gl/3d-tiles'; import {loadI3STile} from '@loaders.gl/i3s/test/test-utils/load-utils'; -import {GltfConverter} from '../../../src/3d-tiles-converter/helpers/gltf-converter'; +import {Tiles3DContentConverter} from '../../../src/3d-tiles-converter/helpers/3d-tiles-content-converter'; import {isBrowser, parse} from '@loaders.gl/core'; import {load} from '@loaders.gl/core'; import {I3SAttributeLoader, COORDINATE_SYSTEM} from '@loaders.gl/i3s'; @@ -57,7 +57,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to const i3sContent = tile.content; t.ok(i3sContent); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, @@ -86,7 +86,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should normalise positions corre const i3sContent = tile.content; const originPositions = i3sContent.attributes.positions.value; const cartographicOrigin = i3sContent.cartographicOrigin; - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -121,7 +121,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should add KHR_materials_unlit e i3sContent.material.pbrMetallicRoughness.metallicFactor = 1.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 1.0; - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -153,7 +153,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl i3sContent.material.pbrMetallicRoughness.metallicFactor = 2.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 2.0; - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: i3sContent, @@ -178,7 +178,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl test('tile-converter(3d-tiles)#b3dm converter - should convert material', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -205,7 +205,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert material', async test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -246,7 +246,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect nor test('tile-converter(3d-tiles)#b3dm converter - should handle geometry without normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); delete tile.content.attributes.normals; const encodedContent = await b3dmConverter.convert({ @@ -280,7 +280,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to t.equal(tile.header.textureFormat, 'ktx2'); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, @@ -298,7 +298,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should generate batchIds during if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new GltfConverter({tilesVersion: '1.0'}); + const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); const encodedContent = await b3dmConverter.convert( { tileContent: tile.content, From 40e7d52f5ae887e68b690eb448ed8d3460ca82c0 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 12 Sep 2024 17:16:03 +0300 Subject: [PATCH 6/8] address dev review issues --- .../3d-tiles-converter/helpers/3d-tiles-content-converter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts index 07a3899b19..18b84f846e 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts @@ -33,12 +33,11 @@ export class Tiles3DContentConverter { // @ts-expect-error rtcCenter: Float32Array; i3sTile: any; - outputVersion: string; tileType: string; constructor(options: {outputVersion: string} = {outputVersion: '1.1'}) { - this.outputVersion = options.outputVersion; - this.tileType = this.outputVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF; + this.tileType = + options.outputVersion === '1.0' ? TILE3D_TYPE.BATCHED_3D_MODEL : TILE3D_TYPE.GLTF; } /** From db83393d21b8131e35156cf5e80822a909eca416 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Fri, 13 Sep 2024 17:11:32 +0300 Subject: [PATCH 7/8] address dev review issues --- .../cli-reference/tile-converter.md | 1 + modules/i3s/src/index.ts | 1 + .../3d-tiles-converter/3d-tiles-converter.ts | 15 +++-- .../helpers/3d-tiles-content-converter.ts | 27 +++++---- .../src/lib/utils/conversion-dump.ts | 6 +- ....js => 3d-tiles-content-converter.spec.js} | 56 +++++++++---------- modules/tile-converter/test/index.js | 2 +- .../test/lib/utils/conversion-dump.spec.ts | 2 +- 8 files changed, 62 insertions(+), 48 deletions(-) rename modules/tile-converter/test/3d-tiles-converter/helpers/{b3dm-converter.spec.js => 3d-tiles-content-converter.spec.js} (81%) diff --git a/docs/modules/tile-converter/cli-reference/tile-converter.md b/docs/modules/tile-converter/cli-reference/tile-converter.md index b67eeaccd6..bb9a1c033e 100644 --- a/docs/modules/tile-converter/cli-reference/tile-converter.md +++ b/docs/modules/tile-converter/cli-reference/tile-converter.md @@ -74,6 +74,7 @@ NodeJS 14 or higher is required. | tileset | \* | \* | "tileset.json" file (3DTiles) / "http://..../SceneServer/layers/0" resource (I3S) | | output | \* | \* | Output folder. This folder will be created by converter if doesn't exist. It is relative to the converter path. Default: "./data" folder | | name | \* | \* | Tileset name. This option is required for naming in the output json resouces and for the output `path/\*.slpk` file naming | +| output-version | | \* | Version of 3D Tiles format. This option is used for I3S to 3DTiles conversion only (optional). Possible values - "1.0", "1.1" (default). More information on the 3D Tiles revisions: https://github.com/CesiumGS/3d-tiles/blob/main/3d-tiles-reference-card-1.1.pdf| | max-depth | \* | \* | Maximal depth of the hierarchical tiles tree traversal, default: infinity | | slpk | \* | | Whether the converter generates \*.slpk (Scene Layer Package) I3S output files | | 7zExe | \* | | location of 7z.exe archiver to create slpk on Windows OS, default: "C:\\Program Files\\7-Zip\\7z.exe" | diff --git a/modules/i3s/src/index.ts b/modules/i3s/src/index.ts index 9756a55c11..7902f62528 100644 --- a/modules/i3s/src/index.ts +++ b/modules/i3s/src/index.ts @@ -53,3 +53,4 @@ export {SLPKArchive} from './lib/parsers/parse-slpk/slpk-archieve'; export {parseSLPKArchive} from './lib/parsers/parse-slpk/parse-slpk'; export {LayerError} from './lib/parsers/parse-arcgis-webscene'; export {customizeColors} from './lib/utils/customize-colors'; +export {type I3STileAttributes} from './lib/parsers/parse-i3s-attribute'; diff --git a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts index 642e8e03fc..056f6970ed 100644 --- a/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts @@ -2,7 +2,8 @@ import type { AttributeStorageInfo, FeatureAttribute, NodeReference, - I3STilesetHeader + I3STilesetHeader, + I3STileAttributes } from '@loaders.gl/i3s'; import type {Tile3DBoundingVolume, Tiles3DTileJSON} from '@loaders.gl/3d-tiles'; @@ -283,7 +284,7 @@ export default class Tiles3DConverter { this.vertexCounter += content?.vertexCount || 0; - let featureAttributes: FeatureAttribute | null = null; + let featureAttributes: I3STileAttributes | null = null; if (this.attributeStorageInfo) { featureAttributes = await this._loadChildAttributes(sourceChild, this.attributeStorageInfo); } @@ -297,14 +298,18 @@ export default class Tiles3DConverter { }; const converter = new Tiles3DContentConverter({outputVersion: this.options.outputVersion}); - const b3dm = await converter.convert( + const contentData = await converter.convert( i3sAttributesData, featureAttributes, this.attributeStorageInfo ); await this.conversionDump.addNode(`${sourceChild.id}.${this.fileExt}`, sourceChild.id); - await writeFile(this.tilesetPath, new Uint8Array(b3dm), `${sourceChild.id}.${this.fileExt}`); + await writeFile( + this.tilesetPath, + new Uint8Array(contentData), + `${sourceChild.id}.${this.fileExt}` + ); await this.conversionDump.updateConvertedNodesDumpFile( `${sourceChild.id}.${this.fileExt}`, sourceChild.id, @@ -438,7 +443,7 @@ export default class Tiles3DConverter { private async _loadChildAttributes( sourceChild: I3STileHeader, attributeStorageInfo: AttributeStorageInfo[] - ): Promise { + ): Promise { const promises: any[] = []; const {attributeUrls = []} = sourceChild; diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts index 18b84f846e..a4d348e9f2 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts @@ -1,4 +1,4 @@ -import type {I3STileContent, FeatureAttribute, AttributeStorageInfo} from '@loaders.gl/i3s'; +import type {I3STileContent, AttributeStorageInfo, I3STileAttributes} from '@loaders.gl/i3s'; import {encodeSync} from '@loaders.gl/core'; import { GLTFScenegraph, @@ -10,6 +10,7 @@ import { import {Tile3DWriter} from '@loaders.gl/3d-tiles'; import {TILE3D_TYPE} from '@loaders.gl/3d-tiles'; import {Matrix4, Vector3} from '@math.gl/core'; +import {isTypedArray} from '@math.gl/types'; import {Ellipsoid} from '@math.gl/geospatial'; import {convertTextureAtlas} from './texture-atlas'; import {generateSyntheticIndices} from '../../lib/utils/geometry-utils'; @@ -47,7 +48,7 @@ export class Tiles3DContentConverter { */ async convert( i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null = null, + featureAttributes: I3STileAttributes | null = null, attributeStorageInfo?: AttributeStorageInfo[] | null | undefined ): Promise { const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes, attributeStorageInfo); @@ -75,7 +76,7 @@ export class Tiles3DContentConverter { // eslint-disable-next-line complexity, max-statements async buildGLTF( i3sAttributesData: I3SAttributesData, - featureAttributes: FeatureAttribute | null, + featureAttributes: I3STileAttributes | null, attributeStorageInfo?: AttributeStorageInfo[] | null | undefined ): Promise { const {tileContent, textureFormat, box} = i3sAttributesData; @@ -155,10 +156,10 @@ export class Tiles3DContentConverter { return encodeSync(gltfBuilder.gltf, GLTFWriter, {gltfBuilder}); } - _createMetadataExtensions( + private _createMetadataExtensions( gltfBuilder: GLTFScenegraph, meshIndex: number, - featureAttributes: FeatureAttribute | null, + featureAttributes: I3STileAttributes | null, attributeStorageInfo: AttributeStorageInfo[] | null | undefined, tileContent: I3STileContent ) { @@ -181,8 +182,8 @@ export class Tiles3DContentConverter { } } - _createPropertyAttibutes( - featureAttributes: FeatureAttribute | null, + private _createPropertyAttibutes( + featureAttributes: I3STileAttributes | null, attributeStorageInfo?: AttributeStorageInfo[] | null | undefined ): PropertyAttribute[] { if (!featureAttributes || !attributeStorageInfo) { @@ -202,10 +203,11 @@ export class Tiles3DContentConverter { return propertyAttributeArray; } - _convertAttributeStorageInfoToPropertyAttribute( + // eslint-disable-next-line complexity + private _convertAttributeStorageInfoToPropertyAttribute( attributeName: string, attributeStorageInfo: AttributeStorageInfo[], - featureAttributes: FeatureAttribute + featureAttributes: I3STileAttributes ): PropertyAttribute | null { const attributeValues = featureAttributes[attributeName]; const info = attributeStorageInfo.find((e) => e.name === attributeName); @@ -254,8 +256,13 @@ export class Tiles3DContentConverter { name: attributeName, elementType, componentType, - values: attributeValues + values: [] }; + if (isTypedArray(attributeValues)) { + propertyAttribute.values = Array.prototype.slice.call(attributeValues); + } else if (attributeValues !== null) { + propertyAttribute.values = attributeValues; + } return propertyAttribute; } diff --git a/modules/tile-converter/src/lib/utils/conversion-dump.ts b/modules/tile-converter/src/lib/utils/conversion-dump.ts index 5db3d1e0d2..b3770ae610 100644 --- a/modules/tile-converter/src/lib/utils/conversion-dump.ts +++ b/modules/tile-converter/src/lib/utils/conversion-dump.ts @@ -22,7 +22,7 @@ export type ConversionDumpOptions = { generateBoundingVolumes: boolean; metadataClass: string; analyze: boolean; - tilesVersion: string; + outputVersion: string; }; type NodeDoneStatus = { @@ -89,7 +89,7 @@ export class ConversionDump { mergeMaterials = true, metadataClass, analyze = false, - tilesVersion = '1.1' + outputVersion = '1.1' } = currentOptions; this.options = { tilesetName, @@ -105,7 +105,7 @@ export class ConversionDump { mergeMaterials, metadataClass, analyze, - tilesVersion + outputVersion }; const dumpFilename = join( diff --git a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js b/modules/tile-converter/test/3d-tiles-converter/helpers/3d-tiles-content-converter.spec.js similarity index 81% rename from modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js rename to modules/tile-converter/test/3d-tiles-converter/helpers/3d-tiles-content-converter.spec.js index 4d5d63e454..9f76cbb459 100644 --- a/modules/tile-converter/test/3d-tiles-converter/helpers/b3dm-converter.spec.js +++ b/modules/tile-converter/test/3d-tiles-converter/helpers/3d-tiles-content-converter.spec.js @@ -51,14 +51,14 @@ const ATTRIBUTES_STORAGE_INFO_STUB = [ ]; const Y_UP_TO_Z_UP_MATRIX = new Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]); -test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to b3dm encoded data', async (t) => { +test('tile-converter(3d-tiles)#content converter - should convert i3s node data to b3dm encoded data', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); const i3sContent = tile.content; t.ok(i3sContent); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert( + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); + const encodedContent = await contentConverter.convert( { tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -78,7 +78,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to } }); -test('tile-converter(3d-tiles)#b3dm converter - should normalise positions correctly', async (t) => { +test('tile-converter(3d-tiles)#content converter - should normalise positions correctly', async (t) => { if (!isBrowser) { const tile = await loadI3STile({ i3s: {coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, decodeTextures: false} @@ -86,9 +86,9 @@ test('tile-converter(3d-tiles)#b3dm converter - should normalise positions corre const i3sContent = tile.content; const originPositions = i3sContent.attributes.positions.value; const cartographicOrigin = i3sContent.cartographicOrigin; - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert({ + const encodedContent = await contentConverter.convert({ tileContent: i3sContent, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -111,7 +111,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should normalise positions corre } }); -test('tile-converter(3d-tiles)#b3dm converter - should add KHR_materials_unlit extension', async (t) => { +test('tile-converter(3d-tiles)#content converter - should add KHR_materials_unlit extension', async (t) => { if (!isBrowser) { const tile = await loadI3STile({ i3s: {coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, decodeTextures: false} @@ -121,9 +121,9 @@ test('tile-converter(3d-tiles)#b3dm converter - should add KHR_materials_unlit e i3sContent.material.pbrMetallicRoughness.metallicFactor = 1.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 1.0; - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert({ + const encodedContent = await contentConverter.convert({ tileContent: i3sContent, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -143,7 +143,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should add KHR_materials_unlit e } }); -test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unlit extension', async (t) => { +test('tile-converter(3d-tiles)#content converter - should NOT add KHR_materials_unlit extension', async (t) => { if (!isBrowser) { const tile = await loadI3STile({ i3s: {coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, decodeTextures: false} @@ -153,9 +153,9 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl i3sContent.material.pbrMetallicRoughness.metallicFactor = 2.0; i3sContent.material.pbrMetallicRoughness.roughnessFactor = 2.0; - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert({ + const encodedContent = await contentConverter.convert({ tileContent: i3sContent, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -175,11 +175,11 @@ test('tile-converter(3d-tiles)#b3dm converter - should NOT add KHR_materials_unl } }); -test('tile-converter(3d-tiles)#b3dm converter - should convert material', async (t) => { +test('tile-converter(3d-tiles)#content converter - should convert material', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert({ + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); + const encodedContent = await contentConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -202,11 +202,11 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert material', async } }); -test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect normals', async (t) => { +test('tile-converter(3d-tiles)#content converter - should not convert incorrect normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert({ + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); + const encodedContent = await contentConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -224,7 +224,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect nor // If all normals are 0, converter should not convert such normals tile.content.attributes.normals.value.fill(0); - const encodedContent2 = await b3dmConverter.convert({ + const encodedContent2 = await contentConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -243,13 +243,13 @@ test('tile-converter(3d-tiles)#b3dm converter - should not convert incorrect nor } }); -test('tile-converter(3d-tiles)#b3dm converter - should handle geometry without normals', async (t) => { +test('tile-converter(3d-tiles)#content converter - should handle geometry without normals', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); delete tile.content.attributes.normals; - const encodedContent = await b3dmConverter.convert({ + const encodedContent = await contentConverter.convert({ tileContent: tile.content, textureFormat: tile.header.textureFormat, box: tile.header.boundingVolume.box @@ -268,7 +268,7 @@ test('tile-converter(3d-tiles)#b3dm converter - should handle geometry without n } }); -test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to b3dm encoded data with ktx2 textures', async (t) => { +test('tile-converter(3d-tiles)#content converter - should convert i3s node data to b3dm encoded data with ktx2 textures', async (t) => { if (!isBrowser) { const _replaceWithKTX2Texture = true; const options = {i3s: {decodeTextures: false, useCompressedTextures: true}}; @@ -280,8 +280,8 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to t.equal(tile.header.textureFormat, 'ktx2'); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert( + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); + const encodedContent = await contentConverter.convert( { tileContent: tile.content, textureFormat: tile.header.textureFormat, @@ -294,12 +294,12 @@ test('tile-converter(3d-tiles)#b3dm converter - should convert i3s node data to } }); -test('tile-converter(3d-tiles)#b3dm converter - should generate batchIds during conversion', async (t) => { +test('tile-converter(3d-tiles)#content converter - should generate batchIds during conversion', async (t) => { if (!isBrowser) { const tile = await loadI3STile({i3s: {decodeTextures: false}}); const attributes = await _loadAttributes(tile, ATTRIBUTES_STORAGE_INFO_STUB); - const b3dmConverter = new Tiles3DContentConverter({tilesVersion: '1.0'}); - const encodedContent = await b3dmConverter.convert( + const contentConverter = new Tiles3DContentConverter({outputVersion: '1.0'}); + const encodedContent = await contentConverter.convert( { tileContent: tile.content, textureFormat: tile.header.textureFormat, diff --git a/modules/tile-converter/test/index.js b/modules/tile-converter/test/index.js index a1b75b9e96..86e496d3fb 100644 --- a/modules/tile-converter/test/index.js +++ b/modules/tile-converter/test/index.js @@ -17,7 +17,7 @@ import './i3s-converter/helpers/progress.spec.ts'; import './i3s-converter/i3s-converter.spec'; import './utils/cli-utils.spec'; -import './3d-tiles-converter/helpers/b3dm-converter.spec'; +import './3d-tiles-converter/helpers/3d-tiles-content-converter.spec'; import './3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.spec'; import './3d-tiles-converter/helpers/texture-atlas.spec'; import './3d-tiles-converter/helpers/load-i3s.spec'; diff --git a/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts b/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts index 50f489ea17..0b398e39ea 100644 --- a/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts +++ b/modules/tile-converter/test/lib/utils/conversion-dump.spec.ts @@ -52,7 +52,7 @@ const testOptions = { generateBoundingVolumes: true, metadataClass: 'testMetadataClass', analyze: true, - tilesVersion: '1.0' + outputVersion: '1.0' }; const testMaterialDefinitions = [ { From a3df0c2dfe3a3404ea495e8ef204f5f3f2654ac0 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 16 Sep 2024 14:55:08 +0300 Subject: [PATCH 8/8] address dev review issues --- .../3d-tiles-converter/helpers/3d-tiles-content-converter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts index a4d348e9f2..b6021ab38c 100644 --- a/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +++ b/modules/tile-converter/src/3d-tiles-converter/helpers/3d-tiles-content-converter.ts @@ -259,7 +259,7 @@ export class Tiles3DContentConverter { values: [] }; if (isTypedArray(attributeValues)) { - propertyAttribute.values = Array.prototype.slice.call(attributeValues); + propertyAttribute.values = Array.from(attributeValues); } else if (attributeValues !== null) { propertyAttribute.values = attributeValues; }