Skip to content

Commit

Permalink
feat(3dtiles): add support for binary batch table
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
`C3DTBatchTable` constructor signature has changed from
C3DTBatchTable(buffer, binaryLength, batchLength, registeredExtensions) to
C3DTBatchTable(buffer, jsonLength, binaryLength, batchLength, registeredExtensions)
  • Loading branch information
jailln committed Oct 26, 2022
1 parent 48a24fa commit 47325ab
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 37 deletions.
70 changes: 51 additions & 19 deletions src/Core/3DTiles/C3DTBatchTable.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import utf8Decoder from 'Utils/Utf8Decoder';
import binaryPropertyAccessor from './utils/BinaryPropertyAccessor';
import C3DTilesTypes from './C3DTilesTypes';

/** @classdesc
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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;
}
}
}

Expand Down
104 changes: 104 additions & 0 deletions src/Core/3DTiles/utils/BinaryPropertyAccessor.js
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 3 additions & 2 deletions src/Parser/B3dmParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({}));
Expand Down
5 changes: 1 addition & 4 deletions test/unit/3dtiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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());
Expand Down
44 changes: 44 additions & 0 deletions test/unit/3dtilesbinarypropertyaccessor.js
Original file line number Diff line number Diff line change
@@ -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));
}
});
});
5 changes: 1 addition & 4 deletions test/unit/camera.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down
5 changes: 1 addition & 4 deletions test/unit/globeview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 1 addition & 4 deletions test/unit/lasparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/';
Expand Down
15 changes: 15 additions & 0 deletions test/unit/utils.js
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 47325ab

Please sign in to comment.