@loaders.gl/3d-tiles
Version:
3D Tiles, an open standard for streaming massive heterogeneous 3D geospatial datasets.
169 lines (168 loc) • 8.85 kB
JavaScript
// 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 { Vector3, Matrix3, Matrix4, Quaternion } from '@math.gl/core';
import { Ellipsoid } from '@math.gl/geospatial';
import { GL } from '@loaders.gl/math'; // 'math.gl/geometry';
import Tile3DFeatureTable from "../classes/tile-3d-feature-table.js";
import Tile3DBatchTable from "../classes/tile-3d-batch-table.js";
import { parse3DTileHeaderSync } from "./helpers/parse-3d-tile-header.js";
import { parse3DTileTablesHeaderSync, parse3DTileTablesSync } from "./helpers/parse-3d-tile-tables.js";
import { parse3DTileGLTFViewSync, extractGLTF } from "./helpers/parse-3d-tile-gltf-view.js";
export async function parseInstancedModel3DTile(tile, arrayBuffer, byteOffset, options, context) {
byteOffset = parseInstancedModel(tile, arrayBuffer, byteOffset, options, context);
await extractGLTF(tile, tile.gltfFormat || 0, options, context);
return byteOffset;
}
function parseInstancedModel(tile, arrayBuffer, byteOffset, options, context) {
byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset);
if (tile.version !== 1) {
throw new Error(`Instanced 3D Model version ${tile.version} is not supported`);
}
byteOffset = parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset);
const view = new DataView(arrayBuffer);
tile.gltfFormat = view.getUint32(byteOffset, true);
byteOffset += 4;
// PARSE FEATURE TABLE
byteOffset = parse3DTileTablesSync(tile, arrayBuffer, byteOffset, options);
byteOffset = parse3DTileGLTFViewSync(tile, arrayBuffer, byteOffset, options);
// TODO - Is the feature table sometimes optional or can check be moved into table header parser?
if (!tile?.header?.featureTableJsonByteLength || tile.header.featureTableJsonByteLength === 0) {
throw new Error('i3dm parser: featureTableJsonByteLength is zero.');
}
const featureTable = new Tile3DFeatureTable(tile.featureTableJson, tile.featureTableBinary);
const instancesLength = featureTable.getGlobalProperty('INSTANCES_LENGTH');
featureTable.featuresLength = instancesLength;
if (!Number.isFinite(instancesLength)) {
throw new Error('i3dm parser: INSTANCES_LENGTH must be defined');
}
tile.eastNorthUp = featureTable.getGlobalProperty('EAST_NORTH_UP');
tile.rtcCenter = featureTable.getGlobalProperty('RTC_CENTER', GL.FLOAT, 3);
const batchTable = new Tile3DBatchTable(tile.batchTableJson, tile.batchTableBinary, instancesLength);
extractInstancedAttributes(tile, featureTable, batchTable, instancesLength);
return byteOffset;
}
// eslint-disable-next-line max-statements, complexity
function extractInstancedAttributes(tile, featureTable, batchTable, instancesLength) {
const instances = new Array(instancesLength);
const instancePosition = new Vector3();
const instanceNormalRight = new Vector3();
const instanceNormalUp = new Vector3();
const instanceNormalForward = new Vector3();
const instanceRotation = new Matrix3();
const instanceQuaternion = new Quaternion();
const instanceScale = new Vector3();
const instanceTranslationRotationScale = {};
const instanceTransform = new Matrix4();
const scratch1 = [];
const scratch2 = [];
const scratch3 = [];
const scratch4 = [];
for (let i = 0; i < instancesLength; i++) {
let position;
// Get the instance position
if (featureTable.hasProperty('POSITION')) {
position = featureTable.getProperty('POSITION', GL.FLOAT, 3, i, instancePosition);
}
else if (featureTable.hasProperty('POSITION_QUANTIZED')) {
position = featureTable.getProperty('POSITION_QUANTIZED', GL.UNSIGNED_SHORT, 3, i, instancePosition);
const quantizedVolumeOffset = featureTable.getGlobalProperty('QUANTIZED_VOLUME_OFFSET', GL.FLOAT, 3);
if (!quantizedVolumeOffset) {
throw new Error('i3dm parser: QUANTIZED_VOLUME_OFFSET must be defined for quantized positions.');
}
const quantizedVolumeScale = featureTable.getGlobalProperty('QUANTIZED_VOLUME_SCALE', GL.FLOAT, 3);
if (!quantizedVolumeScale) {
throw new Error('i3dm parser: QUANTIZED_VOLUME_SCALE must be defined for quantized positions.');
}
const MAX_UNSIGNED_SHORT = 65535.0;
for (let j = 0; j < 3; j++) {
position[j] =
(position[j] / MAX_UNSIGNED_SHORT) * quantizedVolumeScale[j] + quantizedVolumeOffset[j];
}
}
if (!position) {
throw new Error('i3dm: POSITION or POSITION_QUANTIZED must be defined for each instance.');
}
instancePosition.copy(position);
// @ts-expect-error
instanceTranslationRotationScale.translation = instancePosition;
// Get the instance rotation
tile.normalUp = featureTable.getProperty('NORMAL_UP', GL.FLOAT, 3, i, scratch1);
tile.normalRight = featureTable.getProperty('NORMAL_RIGHT', GL.FLOAT, 3, i, scratch2);
const hasCustomOrientation = false;
if (tile.normalUp) {
if (!tile.normalRight) {
throw new Error('i3dm: Custom orientation requires both NORMAL_UP and NORMAL_RIGHT.');
}
// Vector3.unpack(normalUp, 0, instanceNormalUp);
// Vector3.unpack(normalRight, 0, instanceNormalRight);
tile.hasCustomOrientation = true;
}
else {
tile.octNormalUp = featureTable.getProperty('NORMAL_UP_OCT32P', GL.UNSIGNED_SHORT, 2, i, scratch1);
tile.octNormalRight = featureTable.getProperty('NORMAL_RIGHT_OCT32P', GL.UNSIGNED_SHORT, 2, i, scratch2);
if (tile.octNormalUp) {
if (!tile.octNormalRight) {
throw new Error('i3dm: oct-encoded orientation requires NORMAL_UP_OCT32P and NORMAL_RIGHT_OCT32P');
}
throw new Error('i3dm: oct-encoded orientation not implemented');
/*
AttributeCompression.octDecodeInRange(octNormalUp[0], octNormalUp[1], 65535, instanceNormalUp);
AttributeCompression.octDecodeInRange(octNormalRight[0], octNormalRight[1], 65535, instanceNormalRight);
hasCustomOrientation = true;
*/
}
else if (tile.eastNorthUp) {
Ellipsoid.WGS84.eastNorthUpToFixedFrame(instancePosition, instanceTransform);
instanceTransform.getRotationMatrix3(instanceRotation);
}
else {
instanceRotation.identity();
}
}
if (hasCustomOrientation) {
instanceNormalForward.copy(instanceNormalRight).cross(instanceNormalUp).normalize();
instanceRotation.setColumn(0, instanceNormalRight);
instanceRotation.setColumn(1, instanceNormalUp);
instanceRotation.setColumn(2, instanceNormalForward);
}
instanceQuaternion.fromMatrix3(instanceRotation);
// @ts-expect-error
instanceTranslationRotationScale.rotation = instanceQuaternion;
// Get the instance scale
instanceScale.set(1.0, 1.0, 1.0);
const scale = featureTable.getProperty('SCALE', GL.FLOAT, 1, i, scratch3);
if (Number.isFinite(scale)) {
instanceScale.multiplyByScalar(scale);
}
const nonUniformScale = featureTable.getProperty('SCALE_NON_UNIFORM', GL.FLOAT, 3, i, scratch1);
if (nonUniformScale) {
instanceScale.scale(nonUniformScale);
}
// @ts-expect-error
instanceTranslationRotationScale.scale = instanceScale;
// Get the batchId
let batchId = featureTable.getProperty('BATCH_ID', GL.UNSIGNED_SHORT, 1, i, scratch4);
if (batchId === undefined) {
// If BATCH_ID semantic is undefined, batchId is just the instance number
batchId = i;
}
// @ts-expect-error
const rotationMatrix = new Matrix4().fromQuaternion(instanceTranslationRotationScale.rotation);
// Create the model matrix and the instance
instanceTransform.identity();
// @ts-expect-error
instanceTransform.translate(instanceTranslationRotationScale.translation);
instanceTransform.multiplyRight(rotationMatrix);
// @ts-expect-error
instanceTransform.scale(instanceTranslationRotationScale.scale);
const modelMatrix = instanceTransform.clone();
instances[i] = {
modelMatrix,
batchId
};
}
tile.instances = instances;
}