UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

430 lines (426 loc) 15.8 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Las } from 'copc'; import { Box3, Float32BufferAttribute, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, IntType, Uint16BufferAttribute, Uint32BufferAttribute, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Vector3 } from 'three'; import { GlobalCache } from '../core/Cache'; import CoordinateSystem from '../core/geographic/CoordinateSystem'; import OperationCounter from '../core/OperationCounter'; import Fetcher from '../utils/Fetcher'; import { defined, nonNull } from '../utils/tsutils'; import { getLazPerf } from './las/config'; import LASWorkerPool from './las/LASWorkerPool'; import { readColor, readPosition, readScalarAttribute } from './las/readers'; import { createLasView } from './las/worker'; import { PointCloudSourceBase } from './PointCloudSource'; import { EXPOSED_ATTRIBUTES, processAttributes, processLazAttributes } from './potree/attributes'; import { readBinFile } from './potree/bin'; import { toBox3 } from './potree/BoundingBox'; import PotreeWorkerPool from './potree/PotreeWorkerPool'; // Create an A(xis)A(ligned)B(ounding)B(ox) for the child `childIndex` of one aabb. // (PotreeConverter protocol builds implicit octree hierarchy by applying the same // subdivision algo recursively) function createChildAABB(aabb, childIndex) { // Code taken from potree let { min } = aabb; let { max } = aabb; const dHalfLength = new Vector3().copy(max).sub(min).multiplyScalar(0.5); const xHalfLength = new Vector3(dHalfLength.x, 0, 0); const yHalfLength = new Vector3(0, dHalfLength.y, 0); const zHalfLength = new Vector3(0, 0, dHalfLength.z); const cmin = min; const cmax = new Vector3().add(min).add(dHalfLength); if (childIndex === 1) { min = new Vector3().copy(cmin).add(zHalfLength); max = new Vector3().copy(cmax).add(zHalfLength); } else if (childIndex === 3) { min = new Vector3().copy(cmin).add(zHalfLength).add(yHalfLength); max = new Vector3().copy(cmax).add(zHalfLength).add(yHalfLength); } else if (childIndex === 0) { min = cmin; max = cmax; } else if (childIndex === 2) { min = new Vector3().copy(cmin).add(yHalfLength); max = new Vector3().copy(cmax).add(yHalfLength); } else if (childIndex === 5) { min = new Vector3().copy(cmin).add(zHalfLength).add(xHalfLength); max = new Vector3().copy(cmax).add(zHalfLength).add(xHalfLength); } else if (childIndex === 7) { min = new Vector3().copy(cmin).add(dHalfLength); max = new Vector3().copy(cmax).add(dHalfLength); } else if (childIndex === 4) { min = new Vector3().copy(cmin).add(xHalfLength); max = new Vector3().copy(cmax).add(xHalfLength); } else if (childIndex === 6) { min = new Vector3().copy(cmin).add(xHalfLength).add(yHalfLength); max = new Vector3().copy(cmax).add(xHalfLength).add(yHalfLength); } return new Box3(min, max); } function createBufferAttribute(buf, attribute) { if (attribute.interpretation === 'color') { return new Uint8ClampedBufferAttribute(new Uint8ClampedArray(buf), 3, true); } let normalized = false; if ('normalized' in attribute) { normalized = attribute.normalized; } let result; switch (attribute.size) { case 1: if (attribute.type === 'signed') { result = new Int8BufferAttribute(new Int8Array(buf), attribute.dimension, normalized); } else { result = new Uint8BufferAttribute(new Uint8Array(buf), attribute.dimension, normalized); } break; case 2: if (attribute.type === 'signed') { result = new Int16BufferAttribute(new Int16Array(buf), attribute.dimension, normalized); } else { result = new Uint16BufferAttribute(new Uint16Array(buf), attribute.dimension, normalized); } break; case 4: if (attribute.type === 'signed') { result = new Int32BufferAttribute(new Int32Array(buf), attribute.dimension, normalized); } else if (attribute.type === 'unsigned') { result = new Uint32BufferAttribute(new Uint32Array(buf), attribute.dimension, normalized); } else { result = new Float32BufferAttribute(new Float32Array(buf), attribute.dimension, normalized); } break; } if (attribute.type !== 'float') { result.gpuType = IntType; } return result; } /** * Parse a .hrc file and returns the root node of the hierarchy. */ async function parseIndexFile(sourceId, metadata, node) { const url = `${node.baseUrl}/r${node.id}.hrc`; const buf = await Fetcher.arrayBuffer(url); const dataView = new DataView(buf); const stack = []; let offset = 0; const id = nonNull(node.id); node.childrenBitField = dataView.getUint8(0); offset += 1; node.pointCount = dataView.getUint32(1, true); offset += 4; node.children = undefined; stack.push(node); while (stack.length && offset < buf.byteLength) { const snode = nonNull(stack.shift()); // look up 8 children for (let i = 0; i < 8; i++) { // does snode have a #i child ? if (snode.childrenBitField & 1 << i && offset + 5 <= buf.byteLength) { const c = dataView.getUint8(offset); offset += 1; let n = dataView.getUint32(offset, true); offset += 4; if (n === 0) { n = node.pointCount; } const childname = snode.id + i; const volume = createChildAABB(snode.volume, i); let url_1 = nonNull(node.baseUrl); if (childname.length % metadata.hierarchyStepSize === 0) { const myname = childname.substring(id.length); url_1 = `${node.baseUrl}/${myname}`; } const depth = snode.depth + 1; const item = { pointCount: n, childrenBitField: c, children: undefined, id: childname, sourceId, baseUrl: url_1, volume, geometricError: metadata.spacing / 2 ** depth, depth, hasData: true, center: volume.getCenter(new Vector3()), parent: snode }; if (snode.children == null) { snode.children = [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]; } nonNull(snode.children)[i] = item; stack.push(item); } } } return node; } /** * Reads Potree datasets. * * ## Supported formats * * This source currently reads legacy Potree datasets (a `cloud.js` files and multiple `.hrc` and * data files). Data files may either be in the BIN format or LAZ files. * * LAZ decompression is done in background threads using workers. If you wish to disable workers * (for a noticeable cost in performance), you can set {@link PotreeSourceOptions.enableWorkers} to * `false` in constructor options. * * Note: this source uses the **laz-perf** package to perform decoding of point cloud data. This * package uses WebAssembly. If you wish to override the path to the required .wasm file, use * {@link sources.las.config.setLazPerfPath | setLazPerfPath()} before using this source. * The default path is {@link sources.las.config.DEFAULT_LAZPERF_PATH | DEFAULT_LAZPERF_PATH}. */ export default class PotreeSource extends PointCloudSourceBase { type = 'PotreeSource'; isPotreeSource = true; _opCounter = new OperationCounter(); /** Available after initialization. */ _datasetInfo = null; get progress() { return this._opCounter.progress; } get loading() { return this._opCounter.loading; } constructor(options) { super(); this._opCounter.addEventListener('changed', () => this.dispatchEvent({ type: 'progress' })); const opts = nonNull(options, 'options is undefined'); this._options = { enableWorkers: opts.enableWorkers ?? true, url: defined(opts, 'url') }; } async initializeOnce() { this._opCounter.increment(); const metadata = await Fetcher.json(this._options.url).finally(() => this._opCounter.decrement()); const sanitizedMetadata = { version: defined(metadata, 'version'), octreeDir: defined(metadata, 'octreeDir'), points: metadata.points, hierarchyStepSize: defined(metadata, 'hierarchyStepSize'), boundingBox: defined(metadata, 'boundingBox'), tightBoundingBox: metadata.tightBoundingBox, pointAttributes: defined(metadata, 'pointAttributes'), scale: defined(metadata, 'scale'), spacing: defined(metadata, 'spacing'), projection: metadata.projection }; let dataFilesExtension; let pointByteSize = 0; let attributes = []; if (metadata.pointAttributes === 'LAZ') { dataFilesExtension = 'laz'; attributes = processLazAttributes(metadata.tightBoundingBox ?? metadata.boundingBox); } else { const result = processAttributes(metadata.pointAttributes); dataFilesExtension = 'bin'; pointByteSize = result.pointByteSize; attributes = result.attributes; } this._datasetInfo = { metadata: sanitizedMetadata, pointByteSize, attributes, dataFilesExtension }; return this; } async readLazFile(buffer, node, attributes) { const compressed = new Uint8Array(buffer); const header = Las.Header.parse(compressed); let decompressed; if (this._options.enableWorkers === false) { const lp = await getLazPerf(); decompressed = await Las.PointData.decompressFile(compressed, lp); } else { const lazPool = await LASWorkerPool.get(); const response = await lazPool.queue('DecodeLazFile', { buffer: compressed.buffer }, [compressed.buffer]); decompressed = new Uint8Array(response); } const view = createLasView(decompressed, header); const position = readPosition(view, node.volume.min, 1, null); const attributeBuffers = attributes.map(attribute => { if (attribute.interpretation === 'color') { const colorBuffer = readColor(view, 1, true, null); return createBufferAttribute(colorBuffer, attribute); } else { const scalarBuffer = readScalarAttribute(view, attribute, 1, null); return createBufferAttribute(scalarBuffer, attribute); } }); return { positionBuffer: { array: position.buffer, dimension: 3, normalized: false }, localBoundingBox: position.localBoundingBox, attributeBuffers: attributeBuffers.map(attributeBuffer => ({ array: attributeBuffer.array, dimension: attributeBuffer.itemSize, normalized: attributeBuffer.normalized })) }; } async readBinFileSync(buffer, pointByteSize, positionAttribute, attributes) { return readBinFile(buffer, pointByteSize, positionAttribute, attributes); } async readBinFile(buffer, pointByteSize, positionAttribute, attributes) { if (this._options.enableWorkers === false) { return this.readBinFileSync(buffer, pointByteSize, positionAttribute, attributes); } else { const potreePool = await PotreeWorkerPool.get(); return potreePool.queue('ReadBinFile', { buffer, info: { pointByteSize, positionAttribute, attributes } }, [buffer]).then(msg => { const parseResult = { positionBuffer: msg.position, attributeBuffers: msg.attributes }; return parseResult; }); } } async fetchDataFile(url) { let result; const cached = GlobalCache.get(url); if (cached != null) { result = cached; } else { result = await Fetcher.arrayBuffer(url); GlobalCache.set(url, result, { size: result.byteLength }); } return result; } async getNodeData(params) { const { metadata, dataFilesExtension, pointByteSize, attributes } = nonNull(this._datasetInfo, 'not initialized'); const node = params.node; // Query HRC if we don't have children metadata yet. if (node.childrenBitField && node.children == null) { parseIndexFile(this.id, metadata, node); } const url = `${node.baseUrl}/r${node.id}.${dataFilesExtension}`; const signal = params.signal; this._opCounter.increment(); let buffer = await this.fetchDataFile(url).finally(() => this._opCounter.decrement()); if (this._options.enableWorkers) { // We have to make a copy because this buffer might be returned more than once by the // queue. However since it's going to be transferred to workers, the second time // we try to transfer it, it will fail as it will already be transferred. buffer = buffer.slice(0); } signal?.throwIfAborted(); let result; let scale = undefined; this._opCounter.increment(); const paramsAttributes = params.attributes ?? []; switch (dataFilesExtension) { case 'bin': { scale = new Vector3(metadata.scale, metadata.scale, metadata.scale); const potreeAttrs = attributes; result = await this.readBinFile(buffer, pointByteSize, nonNull(potreeAttrs.find(a => a.name === 'POSITION_CARTESIAN')), paramsAttributes.map(attribute => nonNull(potreeAttrs.find(a => a.name === attribute.name)))).finally(() => this._opCounter.decrement()); } break; case 'laz': { result = await this.readLazFile(buffer, node, paramsAttributes.map(attribute => nonNull(attributes.find(a => a.name === attribute.name)))).finally(() => this._opCounter.decrement()); } break; default: throw new Error('not supported data file extension: ' + dataFilesExtension); } signal?.throwIfAborted(); const positionBuffer = new Float32BufferAttribute(result.positionBuffer.array, 3, false); const attributeBuffers = paramsAttributes.map((paramAttribute, index) => { const attributeBuffer = result.attributeBuffers[index]; if (attributeBuffer != null) { return createBufferAttribute(attributeBuffer.array, paramAttribute); } }); const localBoundingBox = new Box3().setFromBufferAttribute(positionBuffer); return { origin: node.volume.min, scale, localBoundingBox, position: positionBuffer, pointCount: positionBuffer.count, attributes: attributeBuffers }; } async getHierarchy() { this._opCounter.increment(); const metadata = nonNull(this._datasetInfo?.metadata, 'not initialized'); const base = this._options.url.replace('cloud.js', ''); const baseUrl = `${base}/${metadata.octreeDir}/r`; const volume = toBox3(metadata.boundingBox); const root = await parseIndexFile(this.id, metadata, { sourceId: this.id, id: '', baseUrl, volume, hasData: true, depth: 0, geometricError: metadata.spacing, center: volume.getCenter(new Vector3()) }).finally(() => this._opCounter.decrement()); return root; } dispose() { // Nothing to do } getMemoryUsage() { // Nothing to do } getMetadata() { const { metadata, attributes } = nonNull(this._datasetInfo, 'not initialized'); const { lx, ly, lz, ux, uy, uz } = metadata.tightBoundingBox ?? metadata.boundingBox; const proj = metadata.projection; return Promise.resolve({ volume: new Box3().setFromArray([lx, ly, lz, ux, uy, uz]), attributes: attributes.filter(att => EXPOSED_ATTRIBUTES.has(att.name)), pointCount: metadata.points, crs: proj != null && proj.length > 0 ? new CoordinateSystem({ definition: proj, name: `potree:${this.id}` }) : CoordinateSystem.unknown }); } }