diff --git a/package-lock.json b/package-lock.json index 089a02d0..f49c039b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/parser": "^8.53.0", "babel-loader": "^10.0.0", + "brotli": "^1.3.3", "circular-dependency-plugin": "^5.2.2", "commitizen": "^4.3.1", "css-loader": "^7.1.2", @@ -4640,6 +4641,16 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", diff --git a/package.json b/package.json index 75d4946c..3ec4cc05 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/parser": "^8.53.0", "babel-loader": "^10.0.0", + "brotli": "^1.3.3", "circular-dependency-plugin": "^5.2.2", "commitizen": "^4.3.1", "css-loader": "^7.1.2", diff --git a/src/loading2/brotli-decoder.ts b/src/loading2/brotli-decoder.ts new file mode 100644 index 00000000..63157e47 --- /dev/null +++ b/src/loading2/brotli-decoder.ts @@ -0,0 +1,132 @@ +import { BufferAttribute, BufferGeometry } from 'three'; +import { XhrRequest } from '../loading/types'; +import { DecodedGeometry, GeometryDecoder } from './geometry-decoder'; +import { OctreeGeometryNode } from './octree-geometry-node'; +import { LoadingContext, Metadata } from './octree-loader'; +import { WorkerType } from './worker-pool'; + +// Buffer files for DEFAULT encoding +export const HIERARCHY_FILE = 'hierarchy.bin'; +export const OCTREE_FILE = 'octree.bin'; + +export class BrotliDecoder implements GeometryDecoder { + public readonly workerType = WorkerType.DECODER_WORKER_BROTLI; + private _metadata: Metadata; + + constructor( + public metadata: Metadata, + private context: LoadingContext, + ) { + this._metadata = metadata; + } + + async decode(node: OctreeGeometryNode, worker: Worker): Promise { + const { byteOffset, byteSize } = node; + + if (byteOffset === undefined || byteSize === undefined) { + throw new Error('byteOffset and byteSize are required'); + } + + if (byteSize === BigInt(0)) { + // If bytesize is zero, skip the worker step and return empty DecodedGeometry + return { + buffer: new ArrayBuffer(0), + geometry: new BufferGeometry(), + data: { + tightBoundingBox: { min: [0, 0, 0], max: [0, 0, 0] }, + }, + }; + } + + const urlOctree = await this.getUrl(this.octreePath); + + const first = byteOffset; + const last = byteOffset + byteSize - BigInt(1); + + const headers = { Range: `bytes=${first}-${last}` }; + const response = await this.xhrRequest(urlOctree, { headers }); + + const buffer = await response.arrayBuffer(); + + const pointAttributes = node.octreeGeometry.pointAttributes; + const scale = node.octreeGeometry.scale; + + const box = node.boundingBox; + const min = node.octreeGeometry.offset.clone().add(box.min); + const size = box.max.clone().sub(box.min); + const max = min.clone().add(size); + const numPoints = node.numPoints; + + const offset = this._metadata.offset; + + const message = { + name: node.name, + buffer: buffer, + pointAttributes: pointAttributes, + scale: scale, + min: min, + max: max, + size: size, + offset: offset, + numPoints: numPoints, + }; + + worker.postMessage(message, [message.buffer]); + + const doneEvent = await new Promise>((res) => { + worker.onmessage = res; + }); + + return this.readSuccessMessage(doneEvent, buffer); + } + + private get getUrl() { + return this.context.getUrl; + } + + private get xhrRequest(): XhrRequest { + return this.context.xhrRequest; + } + + private get octreePath() { + return this.context.octreePath; + } + + private readSuccessMessage(e: MessageEvent, buffer: ArrayBuffer) { + const data = e.data; + const buffers = data.attributeBuffers; + const geometry = new BufferGeometry(); + + for (const property in buffers) { + const buffer = buffers[property].buffer; + + if (property === 'position') { + geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); + } else if (property === 'rgba') { + geometry.setAttribute('rgba', new BufferAttribute(new Uint8Array(buffer), 4, true)); + } else if (property === 'NORMAL') { + geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); + } else if (property === 'INDICES') { + const bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4); + bufferAttribute.normalized = true; + geometry.setAttribute('indices', bufferAttribute); + } else { + const bufferAttribute: BufferAttribute & { + potree?: object; + } = new BufferAttribute(new Float32Array(buffer), 1); + + const batchAttribute = buffers[property].attribute; + bufferAttribute.potree = { + offset: buffers[property].offset, + scale: buffers[property].scale, + preciseBuffer: buffers[property].preciseBuffer, + range: batchAttribute.range, + }; + + geometry.setAttribute(property, bufferAttribute); + } + } + + return { data, buffer, geometry }; + } +} diff --git a/src/loading2/brotli-decoder.worker.js b/src/loading2/brotli-decoder.worker.js new file mode 100644 index 00000000..46ee0ee2 --- /dev/null +++ b/src/loading2/brotli-decoder.worker.js @@ -0,0 +1,287 @@ +/** + * Adapted from original Potree Brotli worker: https://github.com/potree/potree/blob/master/src/modules/loader/2.0/DecoderWorker_brotli.js + */ + +import { PointAttribute, PointAttributeTypes } from './point-attributes.ts'; + +const decompress = require('brotli/decompress'); + +const typedArrayMapping = { + int8: Int8Array, + int16: Int16Array, + int32: Int32Array, + int64: Float64Array, + uint8: Uint8Array, + uint16: Uint16Array, + uint32: Uint32Array, + uint64: Float64Array, + float: Float32Array, + double: Float64Array, +}; + +function dealign24b(mortoncode) { + // see https://stackoverflow.com/questions/45694690/how-i-can-remove-all-odds-bits-in-c + + // input alignment of desired bits + // ..a..b..c..d..e..f..g..h..i..j..k..l..m..n..o..p + let x = mortoncode; + + // ..a..b..c..d..e..f..g..h..i..j..k..l..m..n..o..p ..a..b..c..d..e..f..g..h..i..j..k..l..m..n..o..p + // ..a.....c.....e.....g.....i.....k.....m.....o... .....b.....d.....f.....h.....j.....l.....n.....p + // ....a.....c.....e.....g.....i.....k.....m.....o. .....b.....d.....f.....h.....j.....l.....n.....p + x = ((x & 0b001000001000001000001000) >> 2) | ((x & 0b000001000001000001000001) >> 0); + // ....ab....cd....ef....gh....ij....kl....mn....op ....ab....cd....ef....gh....ij....kl....mn....op + // ....ab..........ef..........ij..........mn...... ..........cd..........gh..........kl..........op + // ........ab..........ef..........ij..........mn.. ..........cd..........gh..........kl..........op + x = ((x & 0b000011000000000011000000) >> 4) | ((x & 0b000000000011000000000011) >> 0); + // ........abcd........efgh........ijkl........mnop ........abcd........efgh........ijkl........mnop + // ........abcd....................ijkl............ ....................efgh....................mnop + // ................abcd....................ijkl.... ....................efgh....................mnop + x = ((x & 0b000000001111000000000000) >> 8) | ((x & 0b000000000000000000001111) >> 0); + // ................abcdefgh................ijklmnop ................abcdefgh................ijklmnop + // ................abcdefgh........................ ........................................ijklmnop + // ................................abcdefgh........ ........................................ijklmnop + x = ((x & 0b000000000000000000000000) >> 16) | ((x & 0b000000000000000011111111) >> 0); + + // sucessfully realigned! + //................................abcdefghijklmnop + + return x; +} + +onmessage = function (event) { + let { pointAttributes, scale, min, size, offset, numPoints } = event.data; + + let buffer = decompress(new Uint8Array(event.data.buffer), event.data.buffer.byteLength); + let view = new DataView(buffer.buffer); + + let attributeBuffers = {}; + + let gridSize = 32; + let grid = new Uint32Array(gridSize ** 3); + let toIndex = (x, y, z) => { + // min is already subtracted + let dx = (gridSize * x) / size.x; + let dy = (gridSize * y) / size.y; + let dz = (gridSize * z) / size.z; + + let ix = Math.min(parseInt(dx), gridSize - 1); + let iy = Math.min(parseInt(dy), gridSize - 1); + let iz = Math.min(parseInt(dz), gridSize - 1); + + let index = ix + iy * gridSize + iz * gridSize * gridSize; + + return index; + }; + + let numOccupiedCells = 0; + let byteOffset = 0; + + let tightBoxMin = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]; + let tightBoxMax = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY]; + + for (let pointAttribute of pointAttributes.attributes) { + if (['POSITION_CARTESIAN', 'position'].includes(pointAttribute.name)) { + let buff = new ArrayBuffer(numPoints * 4 * 3); + let positions = new Float32Array(buff); + + for (let j = 0; j < numPoints; j++) { + let mc_0 = view.getUint32(byteOffset + 4, true); + let mc_1 = view.getUint32(byteOffset + 0, true); + let mc_2 = view.getUint32(byteOffset + 12, true); + let mc_3 = view.getUint32(byteOffset + 8, true); + + byteOffset += 16; + + let X = + dealign24b((mc_3 & 0x00ffffff) >>> 0) | + (dealign24b(((mc_3 >>> 24) | (mc_2 << 8)) >>> 0) << 8); + + let Y = + dealign24b((mc_3 & 0x00ffffff) >>> 1) | + (dealign24b(((mc_3 >>> 24) | (mc_2 << 8)) >>> 1) << 8); + + let Z = + dealign24b((mc_3 & 0x00ffffff) >>> 2) | + (dealign24b(((mc_3 >>> 24) | (mc_2 << 8)) >>> 2) << 8); + + if (mc_1 != 0 || mc_2 != 0) { + X = + X | + (dealign24b((mc_1 & 0x00ffffff) >>> 0) << 16) | + (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 0) << 24); + + Y = + Y | + (dealign24b((mc_1 & 0x00ffffff) >>> 1) << 16) | + (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 1) << 24); + + Z = + Z | + (dealign24b((mc_1 & 0x00ffffff) >>> 2) << 16) | + (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 2) << 24); + } + + let x = parseInt(X) * scale[0] + offset[0] - min.x; + let y = parseInt(Y) * scale[1] + offset[1] - min.y; + let z = parseInt(Z) * scale[2] + offset[2] - min.z; + + tightBoxMin[0] = Math.min(tightBoxMin[0], x); + tightBoxMin[1] = Math.min(tightBoxMin[1], y); + tightBoxMin[2] = Math.min(tightBoxMin[2], z); + + tightBoxMax[0] = Math.max(tightBoxMax[0], x); + tightBoxMax[1] = Math.max(tightBoxMax[1], y); + tightBoxMax[2] = Math.max(tightBoxMax[2], z); + + let index = toIndex(x, y, z); + let count = grid[index]++; + if (count === 0) { + numOccupiedCells++; + } + + positions[3 * j + 0] = x; + positions[3 * j + 1] = y; + positions[3 * j + 2] = z; + } + + attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; + } else if (['RGBA', 'rgba'].includes(pointAttribute.name)) { + let buff = new ArrayBuffer(numPoints * 4); + let colors = new Uint8Array(buff); + + for (let j = 0; j < numPoints; j++) { + let mc_0 = view.getUint32(byteOffset + 4, true); + let mc_1 = view.getUint32(byteOffset + 0, true); + byteOffset += 8; + + let r = + dealign24b((mc_1 & 0x00ffffff) >>> 0) | + (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 0) << 8); + + let g = + dealign24b((mc_1 & 0x00ffffff) >>> 1) | + (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 1) << 8); + + let b = + dealign24b((mc_1 & 0x00ffffff) >>> 2) | + (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 2) << 8); + + colors[4 * j + 0] = r > 255 ? r / 256 : r; + colors[4 * j + 1] = g > 255 ? g / 256 : g; + colors[4 * j + 2] = b > 255 ? b / 256 : b; + } + + attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; + } else { + let buff = new ArrayBuffer(numPoints * 4); + let f32 = new Float32Array(buff); + + let TypedArray = typedArrayMapping[pointAttribute.type.name]; + let preciseBuffer = new TypedArray(numPoints); + + let [offset, scale] = [0, 1]; + + const getterMap = { + int8: view.getInt8, + int16: view.getInt16, + int32: view.getInt32, + // 'int64': view.getInt64, + uint8: view.getUint8, + uint16: view.getUint16, + uint32: view.getUint32, + // 'uint64': view.getUint64, + float: view.getFloat32, + double: view.getFloat64, + }; + const getter = getterMap[pointAttribute.type.name].bind(view); + + // compute offset and scale to pack larger types into 32 bit floats + if (pointAttribute.type.size > 4) { + let [amin, amax] = pointAttribute.range; + offset = amin; + scale = 1 / (amax - amin); + } + + for (let j = 0; j < numPoints; j++) { + let value = getter(byteOffset, true); + byteOffset += pointAttribute.byteSize; + + f32[j] = (value - offset) * scale; + preciseBuffer[j] = value; + } + + attributeBuffers[pointAttribute.name] = { + buffer: buff, + preciseBuffer: preciseBuffer, + attribute: pointAttribute, + offset: offset, + scale: scale, + }; + } + } + + let occupancy = parseInt(numPoints / numOccupiedCells); + + { + // add indices + let buff = new ArrayBuffer(numPoints * 4); + let indices = new Uint32Array(buff); + + for (let i = 0; i < numPoints; i++) { + indices[i] = i; + } + + attributeBuffers['INDICES'] = { buffer: buff, attribute: PointAttribute.INDICES }; + } + + { + // handle attribute vectors + let vectors = pointAttributes.vectors; + + for (let vector of vectors) { + let { name, attributes } = vector; + let numVectorElements = attributes.length; + let buffer = new ArrayBuffer(numVectorElements * numPoints * 4); + let f32 = new Float32Array(buffer); + + let iElement = 0; + for (let sourceName of attributes) { + let sourceBuffer = attributeBuffers[sourceName]; + let { offset, scale } = sourceBuffer; + let view = new DataView(sourceBuffer.buffer); + + const getter = view.getFloat32.bind(view); + + for (let j = 0; j < numPoints; j++) { + let value = getter(j * 4, true); + + f32[j * numVectorElements + iElement] = value / scale + offset; + } + + iElement++; + } + + let vecAttribute = new PointAttribute(name, PointAttributeTypes.DATA_TYPE_FLOAT, 3); + + attributeBuffers[name] = { + buffer: buffer, + attribute: vecAttribute, + }; + } + } + + let message = { + buffer: buffer, + attributeBuffers: attributeBuffers, + density: occupancy, + tightBoundingBox: { min: tightBoxMin, max: tightBoxMax }, + }; + + let transferables = []; + for (let property in message.attributeBuffers) { + transferables.push(message.attributeBuffers[property].buffer); + } + + postMessage(message, transferables); +}; diff --git a/src/loading2/octree-loader.ts b/src/loading2/octree-loader.ts index 12b0a116..34305efc 100644 --- a/src/loading2/octree-loader.ts +++ b/src/loading2/octree-loader.ts @@ -1,5 +1,6 @@ import { Box3, Sphere, Vector3 } from 'three'; import { GetUrlFn, XhrRequest } from '../loading/types'; +import { BrotliDecoder } from './brotli-decoder'; import { Decoder } from './decoder'; import { GeometryDecoder } from './geometry-decoder'; import { GltfDecoder } from './gltf-decoder'; @@ -25,12 +26,16 @@ export class NodeLoader { public metadata: Metadata, private loadingContext: LoadingContext, ) { - if (this.metadata.encoding !== 'GLTF') { - this.decoder = new Decoder(metadata, loadingContext); - } else if (metadata.attributes.some((attr) => attr.name === 'sh_band_0')) { - this.decoder = new GltfSplatDecoder(metadata, loadingContext); + if (this.metadata.encoding === 'GLTF') { + if (metadata.attributes.some((attr) => attr.name === 'sh_band_0')) { + this.decoder = new GltfSplatDecoder(metadata, loadingContext); + } else { + this.decoder = new GltfDecoder(metadata, loadingContext); + } + } else if (this.metadata.encoding === 'BROTLI') { + this.decoder = new BrotliDecoder(metadata, loadingContext); } else { - this.decoder = new GltfDecoder(metadata, loadingContext); + this.decoder = new Decoder(metadata, loadingContext); } } diff --git a/src/loading2/worker-pool.ts b/src/loading2/worker-pool.ts index 467ce752..720f535e 100644 --- a/src/loading2/worker-pool.ts +++ b/src/loading2/worker-pool.ts @@ -2,6 +2,7 @@ export enum WorkerType { DECODER_WORKER = 'DECODER_WORKER', DECODER_WORKER_GLTF = 'DECODER_WORKER_GLTF', + DECODER_WORKER_BROTLI = 'DECODER_WORKER_BROTLI', DECODER_WORKER_SPLATS = 'DECODER_WORKER_SPLATS', } @@ -18,6 +19,10 @@ function createWorker(type: WorkerType): Worker { const DecoderWorker_GLTF = require('./gltf-decoder.worker.js').default; return new DecoderWorker_GLTF(); } + case WorkerType.DECODER_WORKER_BROTLI: { + const DecoderWorker_Brotli = require('./brotli-decoder.worker.js').default; + return new DecoderWorker_Brotli(); + } case WorkerType.DECODER_WORKER_SPLATS: { const DecoderWorker_GLTF = require('./gltf-splats-decoder.worker.js').default; return new DecoderWorker_GLTF(); @@ -32,6 +37,7 @@ export class WorkerPool { private workers: { [key in WorkerType]: Worker[] } = { DECODER_WORKER: [], DECODER_WORKER_GLTF: [], + DECODER_WORKER_BROTLI: [], DECODER_WORKER_SPLATS: [], };