Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(converter): support writing 3DTILES of version 1.1 #3054

Merged
merged 12 commits into from
Sep 16, 2024
2 changes: 1 addition & 1 deletion modules/gltf/src/glb-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const GLBWriter = {
} as const satisfies WriterWithEncoder<GLB, never, GLBWriterOptions>;

function encodeSync(glb, options) {
const {byteOffset = 0} = options || {};
const {byteOffset = 0} = options ?? {};

// Calculate length and allocate buffer
const byteLength = encodeGLBSync(glb, null, byteOffset, options);
Expand Down
43 changes: 32 additions & 11 deletions modules/tile-converter/src/3d-tiles-converter/3d-tiles-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 B3dmConverter, {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';
Expand Down Expand Up @@ -59,6 +59,7 @@ export default class Tiles3DConverter {
};
conversionDump: ConversionDump;
progress: Progress;
fileExt: string;

constructor() {
this.options = {};
Expand All @@ -71,13 +72,15 @@ export default class Tiles3DConverter {
this.workerSource = {};
this.conversionDump = new ConversionDump();
this.progress = new Progress();
this.fileExt = '';
}

/**
* Convert i3s format data to 3dTiles
* @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
Expand All @@ -87,6 +90,7 @@ export default class Tiles3DConverter {
inputUrl: string;
outputPath: string;
tilesetName: string;
tilesVersion?: string;
belom88 marked this conversation as resolved.
Show resolved Hide resolved
maxDepth?: number;
egmFilePath: string;
inquirer?: {prompt: PromptModule};
Expand All @@ -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);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -279,13 +293,20 @@ export default class Tiles3DConverter {
textureFormat: sourceChild.textureFormat
};

const b3dmConverter = new B3dmConverter();
const b3dm = await b3dmConverter.convert(i3sAttributesData, featureAttributes);
const converter =
this.options.tilesVersion === '1.0'
? new GltfConverter({tilesVersion: '1.0'})
: new GltfConverter();
belom88 marked this conversation as resolved.
Show resolved Hide resolved
const b3dm = await converter.convert(
belom88 marked this conversation as resolved.
Show resolved Hide resolved
i3sAttributesData,
featureAttributes,
this.attributeStorageInfo
);

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
);
Expand Down Expand Up @@ -379,7 +400,7 @@ export default class Tiles3DConverter {
geometricError: convertScreenThresholdToGeometricError(sourceChild),
children: [],
content: {
uri: `${sourceChild.id}.b3dm`,
uri: `${sourceChild.id}.${this.fileExt}`,
boundingVolume
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type {I3STileContent} 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';
import {Ellipsoid} from '@math.gl/geospatial';
import {convertTextureAtlas} from './texture-atlas';
Expand All @@ -20,12 +27,19 @@ export type I3SAttributesData = {
};

/**
* Converts content of an I3S node to *.b3dm's file content
* Converts content of an I3S node to 3D Tiles file content
*/
export default class B3dmConverter {
export class GltfConverter {
// @ts-expect-error
belom88 marked this conversation as resolved.
Show resolved Hide resolved
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;
belom88 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* The starter of content conversion
Expand All @@ -34,30 +48,38 @@ export default class B3dmConverter {
*/
async convert(
i3sAttributesData: I3SAttributesData,
featureAttributes: any = null
featureAttributes: FeatureAttribute | null = null,
attributeStorageInfo?: AttributeStorageInfo[] | null | undefined
belom88 marked this conversation as resolved.
Show resolved Hide resolved
): Promise<ArrayBuffer> {
const gltf = await this.buildGLTF(i3sAttributesData, featureAttributes);
const b3dm = encodeSync(
{
gltfEncoded: new Uint8Array(gltf),
type: 'b3dm',
featuresLength: this._getFeaturesLength(featureAttributes),
batchTable: featureAttributes
},
Tile3DWriter
);
return b3dm;
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);
}
belom88 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Build and encode gltf
* @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: any
featureAttributes: FeatureAttribute | null,
attributeStorageInfo?: AttributeStorageInfo[] | null | undefined
): Promise<ArrayBuffer> {
const {tileContent, textureFormat, box} = i3sAttributesData;
const {material, attributes, indices: originalIndices, modelMatrix} = tileContent;
Expand Down Expand Up @@ -105,6 +127,7 @@ export default class B3dmConverter {
cartographicOrigin,
modelMatrix
);

this._createBatchIds(tileContent, featureAttributes);
if (attributes.normals && !this._checkNormals(attributes.normals.value)) {
delete attributes.normals;
Expand All @@ -117,16 +140,126 @@ export default class B3dmConverter {
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});
}

_createMetadataExtensions(
gltfBuilder: GLTFScenegraph,
belom88 marked this conversation as resolved.
Show resolved Hide resolved
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
);
}
}
}

const gltfBuffer = encodeSync(gltfBuilder.gltf, GLTFWriter);
_createPropertyAttibutes(
featureAttributes: FeatureAttribute | null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_createPropertyAttibutes(
private _createPropertyAttibutes(

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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;
}

return gltfBuffer;
_convertAttributeStorageInfoToPropertyAttribute(
attributeName: string,
belom88 marked this conversation as resolved.
Show resolved Hide resolved
attributeStorageInfo: AttributeStorageInfo[],
featureAttributes: FeatureAttribute
): PropertyAttribute | null {
const attributes = featureAttributes[attributeName];
const info = attributeStorageInfo.find((e) => e.name === attributeName);
belom88 marked this conversation as resolved.
Show resolved Hide resolved
if (!info) {
return null;
}
const attribute = info.attributeValues;
if (!attribute?.valueType) {
belom88 marked this conversation as resolved.
Show resolved Hide resolved
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;
}

/**
Expand Down
Loading
Loading