diff --git a/src/Core/3DTiles/C3DTBatchTable.js b/src/Core/3DTiles/C3DTBatchTable.js index 74d502a10e..a584a10a77 100644 --- a/src/Core/3DTiles/C3DTBatchTable.js +++ b/src/Core/3DTiles/C3DTBatchTable.js @@ -1,4 +1,5 @@ import utf8Decoder from 'Utils/Utf8Decoder'; +import binaryPropertyAccessor from './utils/BinaryPropertyAccessor'; import C3DTilesTypes from './C3DTilesTypes'; /** @classdesc @@ -17,41 +18,67 @@ import C3DTilesTypes from './C3DTilesTypes'; */ class C3DTBatchTable { /** - * @param {ArrayBuffer} buffer - batch table buffer to parse. - * @param {ArrayBuffer} binaryLength - the length of the binary part of - * the batch table (not supported yet) + * @param {ArrayBuffer} buffer - batch table buffer to parse + * @param {number} jsonLength - batch table json part length + * @param {number} binaryLength - batch table binary part length * @param {number} batchLength - the length of the batch. * @param {Object} registeredExtensions - extensions registered to the layer */ - constructor(buffer, binaryLength, batchLength, registeredExtensions) { + constructor(buffer, jsonLength, binaryLength, batchLength, registeredExtensions) { + if (arguments.length === 4 && + typeof batchLength === 'object' && + !Array.isArray(batchLength) && + batchLength !== null) { + console.warn('You most likely used a deprecated constructor of C3DTBatchTable.'); + } + if (jsonLength + binaryLength !== buffer.byteLength) { + console.error('3DTiles batch table json length and binary length are not consistent with total buffer' + + ' length. The batch table may be wrong.'); + } + this.type = C3DTilesTypes.batchtable; this.batchLength = batchLength; - // Parse Batch table content - let jsonBuffer = buffer; - // Batch table has a json part and can have a binary part (not supported yet) + const jsonBuffer = buffer.slice(0, jsonLength); + const jsonContent = JSON.parse(utf8Decoder.decode(new Uint8Array(jsonBuffer))); + if (binaryLength > 0) { - console.warn('Binary batch table content not supported yet.'); - jsonBuffer = buffer.slice(0, buffer.byteLength - binaryLength); - } + const binaryBuffer = buffer.slice(jsonLength, jsonLength + binaryLength); - // Parse JSON content - const content = utf8Decoder.decode(new Uint8Array(jsonBuffer)); - const json = JSON.parse(content); + for (const propKey in jsonContent) { + if (!Object.prototype.hasOwnProperty.call(jsonContent, propKey)) { + continue; + } + const propVal = jsonContent[propKey]; + // Batch table entries that have already been parsed from the JSON buffer have an array of values. + if (Array.isArray(propVal)) { + continue; + } + if (typeof propVal?.byteOffset !== 'undefined' && + typeof propVal?.componentType !== 'undefined' && + typeof propVal?.type !== 'undefined') { + jsonContent[propKey] = binaryPropertyAccessor(binaryBuffer, this.batchLength, propVal.byteOffset, + propVal.componentType, propVal.type); + } else { + console.error('Invalid 3D Tiles batch table property that is neither a JSON array nor a valid ' + + 'accessor to a binary body'); + } + } + } // Separate the content and the possible extensions // When an extension is found, we call its parser and append the // returned object to batchTable.extensions // Extensions must be registered in the layer (see an example of this in // 3dtiles_hierarchy.html) - if (json.extensions) { + if (jsonContent.extensions) { this.extensions = - registeredExtensions.parseExtensions(json.extensions, this.type); - delete json.extensions; + registeredExtensions.parseExtensions(jsonContent.extensions, this.type); + delete jsonContent.extensions; } // Store batch table json content - this.content = json; + this.content = jsonContent; } /** @@ -86,8 +113,13 @@ class C3DTBatchTable { for (const property in this.content) { // check that the property is not inherited from prototype chain if (Object.prototype.hasOwnProperty.call(this.content, property)) { - featureDisplayableInfo.batchTable[property] = - this.content[property][batchID]; + const val = this.content[property][batchID]; + // Property value may be a threejs vector (see 3D Tiles spec and BinaryPropertyAccessor.js) + if (val && (val.isVector2 || val.isVector3 || val.isVector4)) { + featureDisplayableInfo.batchTable[property] = val.toArray(); + } else { + featureDisplayableInfo.batchTable[property] = val; + } } } diff --git a/src/Core/3DTiles/utils/BinaryPropertyAccessor.js b/src/Core/3DTiles/utils/BinaryPropertyAccessor.js new file mode 100644 index 0000000000..62a6f79d2a --- /dev/null +++ b/src/Core/3DTiles/utils/BinaryPropertyAccessor.js @@ -0,0 +1,104 @@ +import { Vector2, Vector3, Vector4 } from 'three'; + +/** + * @enum {Object} componentTypeBytesSize - Size in byte of a component type. + */ +const componentTypeBytesSize = { + BYTE: 1, + UNSIGNED_BYTE: 1, + SHORT: 2, + UNSIGNED_SHORT: 2, + INT: 4, + UNSIGNED_INT: 4, + FLOAT: 4, + DOUBLE: 8, +}; + +/** + * @enum {Object} componentTypeConstructor - TypedArray constructor for each 3D Tiles binary componentType + */ +const componentTypeConstructor = { + BYTE: Int8Array, + UNSIGNED_BYTE: Uint8Array, + SHORT: Int16Array, + UNSIGNED_SHORT: Uint16Array, + INT: Int32Array, + UNSIGNED_INT: Uint32Array, + FLOAT: Float32Array, + DOUBLE: Float64Array, +}; + + +/** + * @enum {Object} typeComponentsNumber - Number of components for a given type. + */ +const typeComponentsNumber = { + SCALAR: 1, + VEC2: 2, + VEC3: 3, + VEC4: 4, +}; + +/** + * @enum {Object} typeConstructor - constructor for types (only for vectors since scalar will be converted to a single + * value) + */ +const typeConstructor = { + // SCALAR: no constructor, just create a value (int, float, etc. depending on componentType) + VEC2: Vector2, + VEC3: Vector3, + VEC4: Vector4, +}; + +/** + * Parses a 3D Tiles binary property. Used for batch table and feature table parsing. See the 3D Tiles spec for more + * information on how these values are encoded: + * [3D Tiles spec](https://github.com/CesiumGS/3d-tiles/blob/main/specification/TileFormats/BatchTable/README.md#binary-body)) + * @param {ArrayBuffer} buffer The buffer to parse values from. + * @param {Number} batchLength number of objects in the batch (= number of elements to parse). + * @param {Number} byteOffset the offset in bytes into the buffer. + * @param {String} componentType the type of component to parse (one of componentTypeBytesSize keys) + * @param {String} type the type of element to parse (one of typeComponentsNumber keys) + * @returns {Array} an array of values parsed from the buffer. An array of componentType if type is SCALAR. An array + * of Threejs Vector2, Vector3 or Vector4 if type is VEC2, VEC3 or VEC4 respectively. + */ +function binaryPropertyAccessor(buffer, batchLength, byteOffset, componentType, type) { + if (!buffer) { + throw new Error('Buffer is mandatory to parse binary property.'); + } + if (typeof batchLength === 'undefined' || batchLength === null) { + throw new Error('batchLength is mandatory to parse binary property.'); + } + if (typeof byteOffset === 'undefined' || byteOffset === null) { + throw new Error('byteOffset is mandatory to parse binary property.'); + } + if (!componentTypeBytesSize[componentType]) { + throw new Error(`Uknown component type: ${componentType}. Cannot access binary property.`); + } + if (!typeComponentsNumber[type]) { + throw new Error(`Uknown type: ${type}. Cannot access binary property.`); + } + + const typeNb = typeComponentsNumber[type]; + const elementsNb = batchLength * typeNb; // Number of elements to parse in the buffer + + const typedArray = new componentTypeConstructor[componentType](buffer, byteOffset, elementsNb); + + if (type === 'SCALAR') { + return Array.from(typedArray); + } else { + // return an array of threejs vectors, depending on type (see typeConstructor) + const array = []; + // iteration step of 2, 3 or 4, depending on the type (VEC2, VEC3 or VEC4) + for (let i = 0; i <= typedArray.length - typeNb; i += typeNb) { + const vector = new typeConstructor[type](); + // Create a vector from an array, starting at the offset i and takes the right number of elements depending + // on its type (Vector2, Vector3, Vector 4) + vector.fromArray(typedArray, i); + array.push(vector); + } + return array; + } +} + +export default binaryPropertyAccessor; diff --git a/src/Parser/B3dmParser.js b/src/Parser/B3dmParser.js index c4f5ff8e89..42f4719415 100644 --- a/src/Parser/B3dmParser.js +++ b/src/Parser/B3dmParser.js @@ -141,8 +141,9 @@ export default { // sizeBegin is an index to the beginning of the batch table const sizeBegin = headerByteLength + b3dmHeader.FTJSONLength + b3dmHeader.FTBinaryLength; - const BTBuffer = buffer.slice(sizeBegin, b3dmHeader.BTJSONLength + sizeBegin); - promises.push(new C3DTBatchTable(BTBuffer, + const BTBuffer = buffer.slice(sizeBegin, sizeBegin + b3dmHeader.BTJSONLength + + b3dmHeader.BTBinaryLength); + promises.push(new C3DTBatchTable(BTBuffer, b3dmHeader.BTJSONLength, b3dmHeader.BTBinaryLength, FTJSON.BATCH_LENGTH, options.registeredExtensions)); } else { promises.push(Promise.resolve({})); diff --git a/test/unit/3dtiles.js b/test/unit/3dtiles.js index 619b950f20..61fb2f33c9 100644 --- a/test/unit/3dtiles.js +++ b/test/unit/3dtiles.js @@ -6,6 +6,7 @@ import Coordinates from 'Core/Geographic/Coordinates'; import { computeNodeSSE } from 'Process/3dTilesProcessing'; import { configureTile } from 'Provider/3dTilesProvider'; import C3DTileset from '../../src/Core/3DTiles/C3DTileset'; +import { compareWithEpsilon } from './utils'; function tilesetWithRegion(transformMatrix) { const tileset = { @@ -56,10 +57,6 @@ function tilesetWithSphere(transformMatrix) { return tileset; } -function compareWithEpsilon(a, b, epsilon) { - return a - epsilon < b && a + epsilon > b; -} - describe('Distance computation using boundingVolume.region', function () { const camera = new Camera('EPSG:4978', 100, 100); camera.camera3D.position.copy(new Coordinates('EPSG:4326', 0, 0, 10000).as('EPSG:4978').toVector3()); diff --git a/test/unit/3dtilesbinarypropertyaccessor.js b/test/unit/3dtilesbinarypropertyaccessor.js new file mode 100644 index 0000000000..c91e3fe85c --- /dev/null +++ b/test/unit/3dtilesbinarypropertyaccessor.js @@ -0,0 +1,44 @@ +import assert from 'assert'; +import { Vector2 } from 'three'; +import binaryPropertyAccessor from 'Core/3DTiles/utils/BinaryPropertyAccessor'; +import { compareArrayWithEpsilon } from './utils'; + +describe('3D Tiles Binary Property Accessor', function () { + it('Should parse float scalar binary array', function () { + const refArray = [3.5, 2.1, -1.5]; + const typedArray = new Float32Array(refArray); + const buffer = typedArray.buffer; + const batchLength = 3; + const byteOffset = 0; + const componentType = 'FLOAT'; + const type = 'SCALAR'; + + var parsedArray = binaryPropertyAccessor(buffer, batchLength, byteOffset, componentType, type); + + assert.ok(compareArrayWithEpsilon(parsedArray, refArray, 0.001)); + }); + + it('Should parse unsigned short int vector2 binary array', function () { + const refArray = [14, 12, 3, 5, 108, 500]; + const typedArray = new Uint16Array(refArray); + const buffer = typedArray.buffer; + const batchLength = 3; + const byteOffset = 0; + const componentType = 'UNSIGNED_SHORT'; + const type = 'VEC2'; + + const parsedArray = binaryPropertyAccessor(buffer, batchLength, byteOffset, componentType, type); + + // Create expected array (array of THREE.Vector2s) + const expectedArray = []; + for (let i = 0; i <= refArray.length - 2; i += 2) { + const vec2 = new Vector2(); + expectedArray.push(vec2.fromArray(refArray, i)); + } + + // Convert each vector2 to Array and compare them. + for (let i = 0; i < parsedArray.length; i++) { + assert.ok(compareArrayWithEpsilon(parsedArray[i].toArray(), expectedArray[i].toArray(), 0.001)); + } + }); +}); diff --git a/test/unit/camera.js b/test/unit/camera.js index 7c56e8232f..a2738ee4fe 100644 --- a/test/unit/camera.js +++ b/test/unit/camera.js @@ -1,10 +1,7 @@ import assert from 'assert'; import Camera, { CAMERA_TYPE } from 'Renderer/Camera'; import Coordinates from 'Core/Geographic/Coordinates'; - -function compareWithEpsilon(a, b, epsilon) { - return a - epsilon < b && a + epsilon > b; -} +import { compareWithEpsilon } from './utils'; describe('camera', function () { it('should set good aspect in camera3D', function () { diff --git a/test/unit/globeview.js b/test/unit/globeview.js index ea8a891baa..062293fb22 100644 --- a/test/unit/globeview.js +++ b/test/unit/globeview.js @@ -7,10 +7,7 @@ import Extent from 'Core/Geographic/Extent'; import Renderer from './bootstrap'; import CameraUtils from '../../src/Utils/CameraUtils'; import OBB from '../../src/Renderer/OBB'; - -function compareWithEpsilon(a, b, epsilon) { - return a - epsilon < b && a + epsilon > b; -} +import { compareWithEpsilon } from './utils'; describe('GlobeView', function () { const renderer = new Renderer(); diff --git a/test/unit/lasparser.js b/test/unit/lasparser.js index e1a86304cd..629b40425f 100644 --- a/test/unit/lasparser.js +++ b/test/unit/lasparser.js @@ -2,15 +2,12 @@ import assert from 'assert'; import HttpsProxyAgent from 'https-proxy-agent'; import LASParser from 'Parser/LASParser'; import Fetcher from 'Provider/Fetcher'; +import { compareWithEpsilon } from './utils'; describe('LASParser', function () { let lasData; let lazData; - function compareWithEpsilon(a, b, epsilon) { - return a - epsilon < b && a + epsilon > b; - } - before(async () => { const networkOptions = process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}; const baseurl = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/'; diff --git a/test/unit/utils.js b/test/unit/utils.js new file mode 100644 index 0000000000..d758be33b5 --- /dev/null +++ b/test/unit/utils.js @@ -0,0 +1,15 @@ +export function compareWithEpsilon(a, b, epsilon) { + return a - epsilon < b && a + epsilon > b; +} + +export function compareArrayWithEpsilon(arr1, arr2, epsilon) { + if (arr1.length !== arr2.length) { + return false; + } + for (let i = 0; i < arr1.length; i++) { + if (!compareWithEpsilon(arr1[i], arr2[i], epsilon)) { + return false; + } + } + return true; +}