@loaders.gl/3d-tiles
Version:
3D Tiles, an open standard for streaming massive heterogeneous 3D geospatial datasets.
520 lines (456 loc) • 19.3 kB
text/typescript
// 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 {DracoLoader} from '@loaders.gl/draco';
import {LoaderContext, parseFromContext} from '@loaders.gl/loader-utils';
import {GL} from '@loaders.gl/math';
import {Vector3} from '@math.gl/core';
import Tile3DFeatureTable from '../classes/tile-3d-feature-table';
import Tile3DBatchTable from '../classes/tile-3d-batch-table';
import {parse3DTileHeaderSync} from './helpers/parse-3d-tile-header';
import {parse3DTileTablesHeaderSync, parse3DTileTablesSync} from './helpers/parse-3d-tile-tables';
import {normalize3DTileColorAttribute} from './helpers/normalize-3d-tile-colors';
import {normalize3DTileNormalAttribute} from './helpers/normalize-3d-tile-normals';
import {normalize3DTilePositionAttribute} from './helpers/normalize-3d-tile-positions';
import {Tiles3DLoaderOptions} from '../../tiles-3d-loader';
import {Tiles3DTileContent} from '../../types';
export async function parsePointCloud3DTile(
tile: Tiles3DTileContent,
arrayBuffer: ArrayBuffer,
byteOffset: number,
options?: Tiles3DLoaderOptions,
context?: LoaderContext
): Promise<number> {
byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset);
byteOffset = parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset);
byteOffset = parse3DTileTablesSync(tile, arrayBuffer, byteOffset, options);
initializeTile(tile);
const {featureTable, batchTable} = parsePointCloudTables(tile);
await parseDraco(tile, featureTable, batchTable, options, context);
parsePositions(tile, featureTable, options);
// @ts-expect-error TODO - do we need to assert on the batch table?
parseColors(tile, featureTable, batchTable);
parseNormals(tile, featureTable);
return byteOffset;
}
function initializeTile(tile: Tiles3DTileContent): void {
// Initialize point cloud tile defaults
tile.attributes = {
positions: null,
colors: null,
normals: null,
batchIds: null
};
tile.isQuantized = false;
tile.isTranslucent = false;
tile.isRGB565 = false;
tile.isOctEncoded16P = false;
}
function parsePointCloudTables(tile: Tiles3DTileContent): {
featureTable: Tile3DFeatureTable;
batchTable: Tile3DBatchTable | null;
} {
const featureTable = new Tile3DFeatureTable(tile.featureTableJson, tile.featureTableBinary);
const pointsLength = featureTable.getGlobalProperty('POINTS_LENGTH');
if (!Number.isFinite(pointsLength)) {
throw new Error('POINTS_LENGTH must be defined');
}
featureTable.featuresLength = pointsLength;
tile.featuresLength = pointsLength;
tile.pointsLength = pointsLength;
tile.pointCount = pointsLength;
tile.rtcCenter = featureTable.getGlobalProperty('RTC_CENTER', GL.FLOAT, 3);
const batchTable = parseBatchIds(tile, featureTable);
return {featureTable, batchTable};
}
function parsePositions(
tile: Tiles3DTileContent,
featureTable: Tile3DFeatureTable,
options: Tiles3DLoaderOptions | undefined
): void {
tile.attributes = tile.attributes || {
positions: null,
colors: null,
normals: null,
batchIds: null
};
if (!tile.attributes.positions) {
if (featureTable.hasProperty('POSITION')) {
tile.attributes.positions = featureTable.getPropertyArray('POSITION', GL.FLOAT, 3);
} else if (featureTable.hasProperty('POSITION_QUANTIZED')) {
const positions = featureTable.getPropertyArray('POSITION_QUANTIZED', GL.UNSIGNED_SHORT, 3);
tile.isQuantized = true;
tile.quantizedRange = (1 << 16) - 1;
tile.quantizedVolumeScale = featureTable.getGlobalProperty(
'QUANTIZED_VOLUME_SCALE',
GL.FLOAT,
3
);
if (!tile.quantizedVolumeScale) {
throw new Error('QUANTIZED_VOLUME_SCALE must be defined for quantized positions.');
}
tile.quantizedVolumeOffset = featureTable.getGlobalProperty(
'QUANTIZED_VOLUME_OFFSET',
GL.FLOAT,
3
);
if (!tile.quantizedVolumeOffset) {
throw new Error('QUANTIZED_VOLUME_OFFSET must be defined for quantized positions.');
}
tile.attributes.positions = normalize3DTilePositionAttribute(tile, positions, options);
}
}
if (!tile.attributes.positions) {
throw new Error('Either POSITION or POSITION_QUANTIZED must be defined.');
}
}
function parseColors(
tile: Tiles3DTileContent,
featureTable: Tile3DFeatureTable,
batchTable: Tile3DBatchTable
): void {
tile.attributes = tile.attributes || {
positions: null,
colors: null,
normals: null,
batchIds: null
};
if (!tile.attributes.colors) {
let colors = null;
if (featureTable.hasProperty('RGBA')) {
colors = featureTable.getPropertyArray('RGBA', GL.UNSIGNED_BYTE, 4);
tile.isTranslucent = true;
} else if (featureTable.hasProperty('RGB')) {
colors = featureTable.getPropertyArray('RGB', GL.UNSIGNED_BYTE, 3);
} else if (featureTable.hasProperty('RGB565')) {
colors = featureTable.getPropertyArray('RGB565', GL.UNSIGNED_SHORT, 1);
tile.isRGB565 = true;
}
tile.attributes.colors = normalize3DTileColorAttribute(tile, colors, batchTable);
}
if (featureTable.hasProperty('CONSTANT_RGBA')) {
tile.constantRGBA = featureTable.getGlobalProperty('CONSTANT_RGBA', GL.UNSIGNED_BYTE, 4);
}
}
function parseNormals(tile: Tiles3DTileContent, featureTable: Tile3DFeatureTable): void {
tile.attributes = tile.attributes || {
positions: null,
colors: null,
normals: null,
batchIds: null
};
if (!tile.attributes.normals) {
let normals = null;
if (featureTable.hasProperty('NORMAL')) {
normals = featureTable.getPropertyArray('NORMAL', GL.FLOAT, 3);
} else if (featureTable.hasProperty('NORMAL_OCT16P')) {
normals = featureTable.getPropertyArray('NORMAL_OCT16P', GL.UNSIGNED_BYTE, 2);
tile.isOctEncoded16P = true;
}
tile.attributes.normals = normalize3DTileNormalAttribute(tile, normals);
}
}
function parseBatchIds(
tile: Tiles3DTileContent,
featureTable: Tile3DFeatureTable
): Tile3DBatchTable | null {
let batchTable: Tile3DBatchTable | null = null;
if (!tile.batchIds && featureTable.hasProperty('BATCH_ID')) {
tile.batchIds = featureTable.getPropertyArray('BATCH_ID', GL.UNSIGNED_SHORT, 1);
if (tile.batchIds) {
const batchFeatureLength = featureTable.getGlobalProperty('BATCH_LENGTH');
if (!batchFeatureLength) {
throw new Error('Global property: BATCH_LENGTH must be defined when BATCH_ID is defined.');
}
const {batchTableJson, batchTableBinary} = tile;
batchTable = new Tile3DBatchTable(batchTableJson, batchTableBinary, batchFeatureLength);
}
}
return batchTable;
}
// eslint-disable-next-line complexity
async function parseDraco(
tile: Tiles3DTileContent,
featureTable: Tile3DFeatureTable,
batchTable,
options?: Tiles3DLoaderOptions,
context?: LoaderContext
) {
let dracoBuffer;
let dracoFeatureTableProperties;
let dracoBatchTableProperties;
const batchTableDraco =
tile.batchTableJson &&
tile.batchTableJson.extensions &&
tile.batchTableJson.extensions['3DTILES_draco_point_compression'];
if (batchTableDraco) {
dracoBatchTableProperties = batchTableDraco.properties;
}
const featureTableDraco = featureTable.getExtension('3DTILES_draco_point_compression');
if (featureTableDraco) {
dracoFeatureTableProperties = featureTableDraco.properties;
const dracoByteOffset = featureTableDraco.byteOffset;
const dracoByteLength = featureTableDraco.byteLength;
if (!dracoFeatureTableProperties || !Number.isFinite(dracoByteOffset) || !dracoByteLength) {
throw new Error('Draco properties, byteOffset, and byteLength must be defined');
}
dracoBuffer = (tile.featureTableBinary || []).slice(
dracoByteOffset,
dracoByteOffset + dracoByteLength
);
tile.hasPositions = Number.isFinite(dracoFeatureTableProperties.POSITION);
tile.hasColors =
Number.isFinite(dracoFeatureTableProperties.RGB) ||
Number.isFinite(dracoFeatureTableProperties.RGBA);
tile.hasNormals = Number.isFinite(dracoFeatureTableProperties.NORMAL);
tile.hasBatchIds = Number.isFinite(dracoFeatureTableProperties.BATCH_ID);
tile.isTranslucent = Number.isFinite(dracoFeatureTableProperties.RGBA);
}
if (!dracoBuffer) {
return true;
}
const dracoData = {
buffer: dracoBuffer,
properties: {...dracoFeatureTableProperties, ...dracoBatchTableProperties},
featureTableProperties: dracoFeatureTableProperties,
batchTableProperties: dracoBatchTableProperties,
dequantizeInShader: false
};
return await loadDraco(tile, dracoData, options, context);
}
// eslint-disable-next-line complexity, max-statements
export async function loadDraco(
tile: Tiles3DTileContent,
dracoData,
options?: Tiles3DLoaderOptions,
context?: LoaderContext
): Promise<void> {
if (!context) {
return;
}
const dracoOptions = {
...options,
draco: {
...options?.draco,
extraAttributes: dracoData.batchTableProperties || {}
}
};
// The entire tileset might be included, too expensive to serialize
delete dracoOptions['3d-tiles'];
const data = await parseFromContext(dracoData.buffer, DracoLoader, dracoOptions, context);
const decodedPositions = data.attributes.POSITION && data.attributes.POSITION.value;
const decodedColors = data.attributes.COLOR_0 && data.attributes.COLOR_0.value;
const decodedNormals = data.attributes.NORMAL && data.attributes.NORMAL.value;
const decodedBatchIds = data.attributes.BATCH_ID && data.attributes.BATCH_ID.value;
// @ts-expect-error
const isQuantizedDraco = decodedPositions && data.attributes.POSITION.value.quantization;
// @ts-expect-error
const isOctEncodedDraco = decodedNormals && data.attributes.NORMAL.value.quantization;
if (isQuantizedDraco) {
// Draco quantization range == quantized volume scale - size in meters of the quantized volume
// Internal quantized range is the range of values of the quantized data, e.g. 255 for 8-bit, 1023 for 10-bit, etc
// @ts-expect-error This doesn't look right
const quantization = data.POSITION.data.quantization;
const range = quantization.range;
tile.quantizedVolumeScale = new Vector3(range, range, range);
tile.quantizedVolumeOffset = new Vector3(quantization.minValues);
tile.quantizedRange = (1 << quantization.quantizationBits) - 1.0;
tile.isQuantizedDraco = true;
}
if (isOctEncodedDraco) {
// @ts-expect-error This doesn't look right
tile.octEncodedRange = (1 << data.NORMAL.data.quantization.quantizationBits) - 1.0;
tile.isOctEncodedDraco = true;
}
// Extra batch table attributes
const batchTableAttributes = {};
if (dracoData.batchTableProperties) {
for (const attributeName of Object.keys(dracoData.batchTableProperties)) {
if (data.attributes[attributeName] && data.attributes[attributeName].value) {
batchTableAttributes[attributeName.toLowerCase()] = data.attributes[attributeName].value;
}
}
}
tile.attributes = {
// @ts-expect-error
positions: decodedPositions,
// @ts-expect-error
colors: normalize3DTileColorAttribute(tile, decodedColors, undefined),
// @ts-expect-error
normals: decodedNormals,
// @ts-expect-error
batchIds: decodedBatchIds,
...batchTableAttributes
};
}
// TODO - this is the remaining code from Cesium's parser
/*
const batchTable = new Tile3DBatchTable(tile);
// parseDracoBuffer(tile, featureTable, batchTable);
if (!tile.attributes.positions) {
throw new Error('Either POSITION or POSITION_QUANTIZED must be defined.');
}
}
/*
if (!tile.attributes.positions) {
if (featureTable.hasProperty('POSITION')) {
tile.attributes.positions = featureTable.getPropertyArray('POSITION', GL.FLOAT, 3);
} else if (featureTable.hasProperty('POSITION_QUANTIZED')) {
tile.attributes.positions = featureTable.getPropertyArray('POSITION_QUANTIZED', GL.UNSIGNED_SHORT, 3);
if (!tile.colors) {
if (featureTable.hasProperty('RGBA')) {
tile.colors = featureTable.getPropertyArray('RGBA', GL.UNSIGNED_BYTE, 4);
tile.isTranslucent = true;
} else if (featureTable.hasProperty('RGB')) {
tile.colors = featureTable.getPropertyArray('RGB', GL.UNSIGNED_BYTE, 3);
} else if (featureTable.hasPropertry('RGB565')) {
tile.colors = featureTable.getPropertyArray('RGB565', GL.UNSIGNED_SHORT, 1);
tile.isRGB565 = true;
}
}
if (!tile.attributes.normals) {
if (featureTable.getPropertry('NORMAL')) {
tile.attributes.normals = featureTable.getPropertyArray('NORMAL', GL.FLOAT, 3);
} else if (featureTable.getProperty('NORMAL_OCT16P')) {
tile.attributes.normals = featureTable.getPropertyArray('NORMAL_OCT16P', GL.UNSIGNED_BYTE, 2);
tile.isOctEncoded16P = true;
}
}
if (!tile.batchIds) {
if (featureTable.hasProperty('BATCH_ID')) {
tile.batchIds = featureTable.getPropertyArray('BATCH_ID', GL.UNSIGNED_SHORT, 1);
}
}
if (!tile.attributes.positions) {
throw new Error('Either POSITION or POSITION_QUANTIZED must be defined.');
}
if (featureTable.getPropertry('CONSTANT_RGBA')) {
tile.constantRGBA = featureTable.getGlobalProperty('CONSTANT_RGBA', GL.UNSIGNED_BYTE, 4);
}
if (tile.batchIds) {
const batchLength = featureTable.getGlobalProperty('BATCH_LENGTH');
if (!defined(batchLength)) {
throw new Error('Global property: BATCH_LENGTH must be defined when BATCH_ID is defined.');
}
if (defined(batchTableBinary)) {
// Copy the batchTableBinary section and let the underlying ArrayBuffer be freed
batchTableBinary = new Uint8Array(batchTableBinary);
}
if (defined(pointCloud._batchTableLoaded)) {
pointCloud._batchTableLoaded(batchLength, batchTableJson, batchTableBinary);
}
}
// If points are not batched and there are per-point properties, use these properties for styling purposes
var styleableProperties;
if (!hasBatchIds && defined(batchTableBinary)) {
tile.styleableProperties = Cesium3DTileBatchTable.getBinaryProperties(
pointsLength,
batchTableJson,
batchTableBinary
);
}
tile.draco = draco;
}
// Separate parsing and decoding of Draco
export function parseDracoBuffer(tile, featureTable, batchTable) {
let dracoBuffer;
let dracoFeatureTableProperties;
let dracoBatchTableProperties;
const batchTableDraco = batchTable.getExtension('3DTILES_draco_point_compression');
if (batchTableDraco) {
dracoBatchTableProperties = batchTableDraco.properties;
}
const featureTableDraco = featureTable.getExtension('3DTILES_draco_point_compression');
if (featureTableDraco) {
dracoFeatureTableProperties = featureTableDraco.properties;
const dracoByteOffset = featureTableDraco.byteOffset;
const dracoByteLength = featureTableDraco.byteLength;
if (!dracoFeatureTableProperties || !dracoByteOffset || !dracoByteLength) {
throw new Error('Draco properties, byteOffset, and byteLength must be defined');
}
dracoBuffer = arraySlice(
featureTableBinary,
dracoByteOffset,
dracoByteOffset + dracoByteLength
);
tile.hasPositions = dracoFeatureTableProperties.POSITION;
tile.hasColors = dracoFeatureTableProperties.RGB || dracoFeatureTableProperties.RGBA;
tile.hasNormals = dracoFeatureTableProperties.NORMAL;
tile.hasBatchIds = dracoFeatureTableProperties.BATCH_ID;
tile.isTranslucent = dracoFeatureTableProperties.RGBA;
}
if (dracoBuffer) {
tile.draco = {
buffer: dracoBuffer,
properties: {...dracoFeatureTableProperties, ...dracoBatchTableProperties},
featureTableProperties: dracoFeatureTableProperties,
batchTableProperties: dracoBatchTableProperties,
dequantizeInShader: false
};
tile.decodingState = DECODING_STATE.NEEDS_DECODE;
}
}
/*
function decodeDraco(tile, context) {
if (tile.decodingState === DECODING_STATE.READY) {
return false;
}
if (tile.decodingState === DECODING_STATE.NEEDS_DECODE) {
var parsedContent = tile._parsedContent;
var draco = parsedContent.draco;
var decodePromise = DracoLoader.decodePointCloud(draco, context);
if (defined(decodePromise)) {
tile.decodingState = DECODING_STATE.DECODING;
decodePromise.then(function(result) {
tile.decodingState = DECODING_STATE.READY;
var decodedPositions = defined(result.POSITION) ? result.POSITION.array : undefined;
var decodedRgb = defined(result.RGB) ? result.RGB.array : undefined;
var decodedRgba = defined(result.RGBA) ? result.RGBA.array : undefined;
var decodedNormals = defined(result.NORMAL) ? result.NORMAL.array : undefined;
var decodedBatchIds = defined(result.BATCH_ID) ? result.BATCH_ID.array : undefined;
var isQuantizedDraco = defined(decodedPositions) && defined(result.POSITION.data.quantization);
var isOctEncodedDraco = defined(decodedNormals) && defined(result.NORMAL.data.quantization);
if (isQuantizedDraco) {
// Draco quantization range == quantized volume scale - size in meters of the quantized volume
// Internal quantized range is the range of values of the quantized data, e.g. 255 for 8-bit, 1023 for 10-bit, etc
var quantization = result.POSITION.data.quantization;
var range = quantization.range;
tile._quantizedVolumeScale = Cartesian3.fromElements(range, range, range);
tile._quantizedVolumeOffset = Cartesian3.unpack(quantization.minValues);
tile._quantizedRange = (1 << quantization.quantizationBits) - 1.0;
tile._isQuantizedDraco = true;
}
if (isOctEncodedDraco) {
tile._octEncodedRange = (1 << result.NORMAL.data.quantization.quantizationBits) - 1.0;
tile._isOctEncodedDraco = true;
}
var styleableProperties = parsedContent.styleableProperties;
var batchTableProperties = draco.batchTableProperties;
for (var name in batchTableProperties) {
if (batchTableProperties.hasOwnProperty(name)) {
var property = result[name];
if (!defined(styleableProperties)) {
styleableProperties = {};
}
styleableProperties[name] = {
typedArray : property.array,
componentCount : property.data.componentsPerAttribute
};
}
}
parsedContent.positions = defaultValue(decodedPositions, parsedContent.positions);
parsedContent.colors = defaultValue(defaultValue(decodedRgba, decodedRgb), parsedContent.colors);
parsedContent.normals = defaultValue(decodedNormals, parsedContent.normals);
parsedContent.batchIds = defaultValue(decodedBatchIds, parsedContent.batchIds);
parsedContent.styleableProperties = styleableProperties;
}).otherwise(function(error) {
tile.decodingState = DECODING_STATE.FAILED;
tile._readyPromise.reject(error);
});
}
}
return true;
}
*/