UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

442 lines (439 loc) 20.3 kB
import { Las } from 'copc'; import { Box3, Float32BufferAttribute, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, IntType, Uint16BufferAttribute, Uint32BufferAttribute, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Vector3 } from 'three'; import { GlobalCache } from '../core/Cache'; import OperationCounter from '../core/OperationCounter'; import { DefaultQueue } from '../core/RequestQueue'; import Fetcher from '../utils/Fetcher'; import { defined, nonNull } from '../utils/tsutils'; import WorkerPool from '../utils/WorkerPool'; import { getLazPerf } from './las/config'; import createWorker from './las/createWorker'; import { readColor, readPosition, readScalarAttribute } from './las/readers'; import { PointCloudSourceBase } from './PointCloudSource'; import { EXPOSED_ATTRIBUTES, processAttributes, processLazAttributes } from './potree/attributes'; import { readBinFile } from './potree/bin'; import { toBox3 } from './potree/BoundingBox'; let potreePool = null; let lazPool = null; // 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; } function createPotreeWorker() { const worker = new Worker(URL.createObjectURL(new Blob([atob('InVzZSBzdHJpY3QiOygoKT0+e2Z1bmN0aW9uIHAoZSxuKXtyZXR1cm57cmVxdWVzdElkOmUsZXJyb3I6biBpbnN0YW5jZW9mIEVycm9yP24ubWVzc2FnZToidW5rbm93biBlcnJvciJ9fXZhciBkPWU9Pih7bWluOjAsbWF4OjIqKmUtMX0pLGg9e21pbjowLG1heDoxfSxfPXttaW46MCxtYXg6MjU1fSxsPXttaW46MCxtYXg6NjU1MzZ9LEE9e1g6e21pbjp2b2lkIDAsbWF4OnZvaWQgMH0sWTp7bWluOnZvaWQgMCxtYXg6dm9pZCAwfSxaOnttaW46dm9pZCAwLG1heDp2b2lkIDB9LEludGVuc2l0eTpsLFJldHVybk51bWJlcjpkKDMpLE51bWJlck9mUmV0dXJuczpkKDMpLFNjYW5EaXJlY3Rpb25GbGFnOmgsRWRnZU9mRmxpZ2h0TGluZTpoLENsYXNzaWZpY2F0aW9uOl8sU2NhbkFuZ2xlOnttaW46LTkwLG1heDo5MH0sU2NhbkFuZ2xlUmFuazp7bWluOi05MCxtYXg6OTB9LFVzZXJEYXRhOl8sUG9pbnRTb3VyY2VJZDpsLFNjYW5DaGFubmVsOmwsR3BzVGltZTp7bWluOjAsbWF4Ojk5OTl9LFJlZDpsLEdyZWVuOmwsQmx1ZTpsLFNjYW5uZXJDaGFubmVsOmQoMiksSW5mcmFyZWQ6bH07dmFyIHM9ZnVuY3Rpb24oZSl7cmV0dXJuIGVbZS5VaW50OD0wXT0iVWludDgiLGVbZS5VaW50MTY9MV09IlVpbnQxNiIsZVtlLlVpbnQzMj0yXT0iVWludDMyIixlW2UuRmxvYXQ9M109IkZsb2F0IixlW2UuRG91YmxlPTRdPSJEb3VibGUiLGV9KHN8fHt9KTtmdW5jdGlvbiB5KGUpe3N3aXRjaChlKXtjYXNlIHMuVWludDg6Y2FzZSBzLlVpbnQxNjpjYXNlIHMuVWludDMyOnJldHVybiAwO2Nhc2Ugcy5GbG9hdDpjYXNlIHMuRG91YmxlOnJldHVybn19ZnVuY3Rpb24gYyhlLG4sbyxyLHQsaSl7cmV0dXJue3R5cGU6ZSxkaW1lbnNpb246bixub3JtYWxpemVkOm8/PyExLGludGVycHJldGF0aW9uOnI/PyJ1bmtub3duIixtaW46dD8/eShlKSxtYXg6aX19dmFyIEw9e1BPU0lUSU9OX0NBUlRFU0lBTjpjKHMuRmxvYXQsMyksQ09MT1JfUEFDS0VEOmMocy5VaW50OCw0LCEwLCJjb2xvciIpLFJHQkFfUEFDS0VEOmMocy5VaW50OCw0LCEwLCJjb2xvciIpLFJHQl9QQUNLRUQ6YyhzLlVpbnQ4LDMsITAsImNvbG9yIiksTk9STUFMX0ZMT0FUUzpjKHMuRmxvYXQsMyksTk9STUFMOmMocy5GbG9hdCwzKSxOT1JNQUxfU1BIRVJFTUFQUEVEOmMocy5VaW50OCwyKSxOT1JNQUxfT0NUMTY6YyhzLlVpbnQ4LDIpLElOVEVOU0lUWTpjKHMuVWludDE2LDEpLENMQVNTSUZJQ0FUSU9OOmMocy5VaW50OCwxLCExLCJjbGFzc2lmaWNhdGlvbiIpLFJFVFVSTl9OVU1CRVI6YyhzLlVpbnQ4LDEpLE5VTUJFUl9PRl9SRVRVUk5TOmMocy5VaW50OCwxKSxTT1VSQ0VfSUQ6YyhzLlVpbnQxNiwxKSxHUFNfVElNRTpjKHMuRG91YmxlLDEpLFNQQUNJTkc6YyhzLkZsb2F0LDEpLElORElDRVM6YyhzLlVpbnQzMiwxKX07dmFyIFM9KGUsbixvKT0+e2xldCByO3N3aXRjaChlLnR5cGUpe2Nhc2Ugcy5VaW50ODpyPSh0LGkpPT50LmdldFVpbnQ4KGkpO2JyZWFrO2Nhc2Ugcy5VaW50MTY6cj0odCxpKT0+dC5nZXRVaW50MTYoaSwhMCk7YnJlYWs7Y2FzZSBzLlVpbnQzMjpyPSh0LGkpPT50LmdldFVpbnQzMihpLCEwKTticmVhaztjYXNlIHMuRmxvYXQ6cj0odCxpKT0+dC5nZXRGbG9hdDMyKGksITApO2JyZWFrO2Nhc2Ugcy5Eb3VibGU6cj0odCxpKT0+dC5nZXRGbG9hdDY0KGksITApO2JyZWFrfXJldHVybih0LGksYSk9PnthW2ldPXIodCxpKm8rbil9fSxOPShlLG4sbyk9Pih0LGksYSk9PntsZXQgdT1pKm8rbixmPXQuZ2V0VWludDMyKHUrMCo0LCEwKSxtPXQuZ2V0VWludDMyKHUrMSo0LCEwKSxnPXQuZ2V0VWludDMyKHUrMio0LCEwKTthW2kqMyswXT1mLGFbaSozKzFdPW0sYVtpKjMrMl09Z30sVD0oZSxuLG8pPT4ocix0LGkpPT57bGV0IGE9dCpvK24sdT1yLmdldFVpbnQ4KGErMCksZj1yLmdldFVpbnQ4KGErMSksbT1yLmdldFVpbnQ4KGErMik7aVt0KjMrMF09dSxpW3QqMysxXT1mLGlbdCozKzJdPW19O2Z1bmN0aW9uIEUoZSxuKXtsZXR7bmFtZTpvLG9mZnNldDpyLHBvdHJlZUF0dHJpYnV0ZTp0fT1lO2lmKG89PT0iUE9TSVRJT05fQ0FSVEVTSUFOIilyZXR1cm4gTih0LHIsbik7c3dpdGNoKGUuaW50ZXJwcmV0YXRpb24pe2Nhc2UiY29sb3IiOnJldHVybiBUKHQscixuKX1yZXR1cm4gUyh0LHIsbil9ZnVuY3Rpb24gdyhlLG4sbyxyKXtsZXQgdD1yKm87c3dpdGNoKGUpe2Nhc2Uic2lnbmVkIjpzd2l0Y2gobil7Y2FzZSAxOnJldHVybiBuZXcgSW50OEFycmF5KHQpO2Nhc2UgMjpyZXR1cm4gbmV3IEludDE2QXJyYXkodCk7Y2FzZSA0OnJldHVybiBuZXcgSW50MzJBcnJheSh0KX1icmVhaztjYXNlInVuc2lnbmVkIjpzd2l0Y2gobil7Y2FzZSAxOnJldHVybiBuZXcgVWludDhBcnJheSh0KTtjYXNlIDI6cmV0dXJuIG5ldyBVaW50MTZBcnJheSh0KTtjYXNlIDQ6cmV0dXJuIG5ldyBVaW50MzJBcnJheSh0KX1icmVhaztjYXNlImZsb2F0IjpyZXR1cm4gbmV3IEZsb2F0MzJBcnJheSh0KX19ZnVuY3Rpb24gUihlLG4sbyxyKXtsZXQgdD13KGUudHlwZSxlLnNpemUsZS5kaW1lbnNpb24sciksaT1FKGUsbyk7Zm9yKGxldCBhPTA7YTxyO2ErKylpKG4sYSx0KTtyZXR1cm57YXJyYXk6dC5idWZmZXIsZGltZW5zaW9uOmUuZGltZW5zaW9uLG5vcm1hbGl6ZWQ6ZS5ub3JtYWxpemVkfX1mdW5jdGlvbiBVKGUsbixvLHIpe2xldCB0PW5ldyBEYXRhVmlldyhlKSxpPU1hdGguZmxvb3IoZS5ieXRlTGVuZ3RoL24pLGE9UihvLHQsbixpKSx1O3JldHVybiByIT1udWxsJiYodT1SKHIsdCxuLGkpKSx7cG9zaXRpb25CdWZmZXI6YSxhdHRyaWJ1dGVCdWZmZXI6dX19ZnVuY3Rpb24gSShlKXt0cnl7bGV0e2J1ZmZlcjpuLGluZm86b309ZS5wYXlsb2FkLHI9VShuLG8ucG9pbnRCeXRlU2l6ZSxvLnBvc2l0aW9uQXR0cmlidXRlLG8ub3B0aW9uYWxBdHRyaWJ1dGUpLHQ9e3JlcXVlc3RJZDplLmlkLHBheWxvYWQ6e3Bvc2l0aW9uOnIucG9zaXRpb25CdWZmZXIsYXR0cmlidXRlOnIuYXR0cmlidXRlQnVmZmVyfX0saT1yLnBvc2l0aW9uQnVmZmVyLmFycmF5LGE9ci5hdHRyaWJ1dGVCdWZmZXI/LmFycmF5LHU9W2ldO2EmJnUucHVzaChhKSxwb3N0TWVzc2FnZSh0LHt0cmFuc2Zlcjp1fSl9Y2F0Y2gobil7cG9zdE1lc3NhZ2UocChlLmlkLG4pKX19b25tZXNzYWdlPWU9PntsZXQgbj1lLmRhdGE7c3dpdGNoKG4udHlwZSl7Y2FzZSJSZWFkQmluRmlsZSI6SShuKTticmVha319O30pKCk7Cg==')], {type: "text/javascript"})), { type: 'module' }); return worker; } /** * 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, optionalAttribute) { 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 { if (lazPool == null) { lazPool = new WorkerPool({ createWorker }); } const response = await lazPool.queue('DecodeLazFile', { buffer: compressed.buffer }, [compressed.buffer]); decompressed = new Uint8Array(response); } const view = Las.View.create(decompressed, header); const position = readPosition(view, node.volume.min, 1, null); let attributeBuffer = undefined; if (optionalAttribute != null) { if (optionalAttribute.interpretation === 'color') { const colorBuffer = readColor(view, 1, true, null); attributeBuffer = createBufferAttribute(colorBuffer, optionalAttribute); } else { const scalarBuffer = readScalarAttribute(view, optionalAttribute, 1, null); attributeBuffer = createBufferAttribute(scalarBuffer, optionalAttribute); } } return { positionBuffer: { array: position.buffer, dimension: 3, normalized: false }, localBoundingBox: position.localBoundingBox, attributeBuffer: attributeBuffer ? { array: attributeBuffer.array, dimension: attributeBuffer.itemSize, normalized: attributeBuffer.normalized } : undefined }; } async readBinFileSync(buffer, pointByteSize, positionAttribute, optionalAttribute) { return readBinFile(buffer, pointByteSize, positionAttribute, optionalAttribute); } async readBinFile(buffer, pointByteSize, positionAttribute, optionalAttribute) { if (this._options.enableWorkers === false) { return this.readBinFileSync(buffer, pointByteSize, positionAttribute, optionalAttribute); } else { if (potreePool == null) { potreePool = new WorkerPool({ createWorker: createPotreeWorker }); } return potreePool.queue('ReadBinFile', { buffer, info: { pointByteSize, positionAttribute, optionalAttribute } }, [buffer]).then(msg => { const parseResult = { positionBuffer: msg.position, attributeBuffer: msg.attribute }; 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 DefaultQueue.enqueue({ id: url, request: () => this.fetchDataFile(url), priority: node.depth, shouldExecute: () => signal ? !signal.aborted : true }).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(); 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')), params.attribute?.name != null ? nonNull(potreeAttrs.find(a => a.name === params.attribute?.name)) : undefined).finally(() => this._opCounter.decrement()); } break; case 'laz': { result = await this.readLazFile(buffer, node, params.attribute?.name != null ? nonNull(attributes.find(a => a.name === params.attribute?.name)) : undefined).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); let attribute = undefined; if (params.attribute != null && result.attributeBuffer != null) { attribute = createBufferAttribute(result.attributeBuffer.array, params.attribute); } const localBoundingBox = new Box3().setFromBufferAttribute(positionBuffer); return { origin: node.volume.min, scale, localBoundingBox, position: positionBuffer, pointCount: positionBuffer.count, attribute }; } 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 ? { definition: proj, name: `potree:${this.id}` } : undefined }); } }