UNPKG

@pnext/three-loader

Version:

Potree loader for ThreeJS, converted and adapted to Typescript.

495 lines (405 loc) 15 kB
import { Box3, Sphere, Vector3 } from 'three'; import { GetUrlFn, XhrRequest } from '../loading/types'; import { Decoder } from './decoder'; import { GeometryDecoder } from './geometry-decoder'; import { GltfDecoder } from './gltf-decoder'; import { GltfSplatDecoder } from './gltf-splats-decoder'; import { OctreeGeometry } from './octree-geometry'; import { OctreeGeometryNode } from './octree-geometry-node'; import { PointAttribute, PointAttributes, PointAttributeTypes } from './point-attributes'; import { buildUrl, extractBasePath } from './utils'; import { WorkerPool, WorkerType } from './worker-pool'; // Buffer files for DEFAULT encoding export const HIERARCHY_FILE = 'hierarchy.bin'; export const OCTREE_FILE = 'octree.bin'; // Default buffer files for GLTF encoding export const GLTF_COLORS_FILE = 'colors.glbin'; export const GLTF_POSITIONS_FILE = 'positions.glbin'; export class NodeLoader { private readonly decoder: GeometryDecoder; constructor( public url: string, 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); } else { this.decoder = new GltfDecoder(metadata, loadingContext); } } async load(node: OctreeGeometryNode) { if (node.loaded || node.loading) { return; } node.loading = true; node.octreeGeometry.numNodesLoading++; let worker: Worker | undefined; try { if (node.nodeType === 2) { await this.loadHierarchy(node); } const { byteOffset, byteSize } = node; if (byteOffset === undefined || byteSize === undefined) { throw new Error('byteOffset and byteSize are required'); } worker = this.workerPool.getWorker(this.workerType); const loaded = await this.decoder.decode(node, worker); if (!loaded) { return; } const { geometry, data } = loaded; node.density = data.density; node.geometry = geometry; node.loaded = true; node.octreeGeometry.needsUpdate = true; node.tightBoundingBox = this.getTightBoundingBox(data.tightBoundingBox); } catch (e) { node.loaded = false; } finally { node.loading = false; node.octreeGeometry.numNodesLoading--; if (worker) { this.workerPool.returnWorker(this.workerType, worker); } } } private get workerPool() { return this.loadingContext.workerPool; } private get getUrl() { return this.loadingContext.getUrl; } private get hierarchyPath() { return this.loadingContext.hierarchyPath; } private get workerType(): WorkerType { return this.decoder.workerType; } private parseHierarchy(node: OctreeGeometryNode, buffer: ArrayBuffer) { const view = new DataView(buffer); const bytesPerNode = 22; const numNodes = buffer.byteLength / bytesPerNode; const octree = node.octreeGeometry; const nodes: OctreeGeometryNode[] = new Array(numNodes); nodes[0] = node; let nodePos = 1; for (let i = 0; i < numNodes; i++) { const current = nodes[i]; const type = view.getUint8(i * bytesPerNode + 0); const childMask = view.getUint8(i * bytesPerNode + 1); const numPoints = view.getUint32(i * bytesPerNode + 2, true); const byteOffset = view.getBigInt64(i * bytesPerNode + 6, true); const byteSize = view.getBigInt64(i * bytesPerNode + 14, true); if (current.nodeType === 2) { // replace proxy with real node current.byteOffset = byteOffset; current.byteSize = byteSize; current.numPoints = numPoints; } else if (type === 2) { // load proxy current.hierarchyByteOffset = byteOffset; current.hierarchyByteSize = byteSize; current.numPoints = numPoints; } else { // load real node current.byteOffset = byteOffset; current.byteSize = byteSize; current.numPoints = numPoints; } current.nodeType = type; if (current.nodeType === 2) { continue; } for (let childIndex = 0; childIndex < 8; childIndex++) { const childExists = ((1 << childIndex) & childMask) !== 0; if (!childExists) { continue; } const childName = current.name + childIndex; const childAABB = createChildAABB(current.boundingBox, childIndex); const child = new OctreeGeometryNode(childName, octree, childAABB); child.name = childName; child.spacing = current.spacing / 2; child.level = current.level + 1; (current.children as any)[childIndex] = child; child.parent = current; nodes[nodePos] = child; nodePos++; } } } private async loadHierarchy(node: OctreeGeometryNode) { const { hierarchyByteOffset, hierarchyByteSize } = node; if (hierarchyByteOffset === undefined || hierarchyByteSize === undefined) { throw new Error( `hierarchyByteOffset and hierarchyByteSize are undefined for node ${node.name}`, ); } const hierarchyUrl = await this.getUrl(this.hierarchyPath); const first = hierarchyByteOffset; const last = first + hierarchyByteSize - BigInt(1); const headers = { Range: `bytes=${first}-${last}` }; const response = await fetch(hierarchyUrl, { headers }); const buffer = await response.arrayBuffer(); this.parseHierarchy(node, buffer); } private getTightBoundingBox({ min, max }: { min: number[]; max: number[] }): Box3 { const box = new Box3(new Vector3().fromArray(min), new Vector3().fromArray(max)); box.max.sub(box.min); box.min.set(0, 0, 0); return box; } } const tmpVec3 = new Vector3(); function createChildAABB(aabb: Box3, index: number) { const min = aabb.min.clone(); const max = aabb.max.clone(); const size = tmpVec3.subVectors(max, min); if ((index & 0b0001) > 0) { min.z += size.z / 2; } else { max.z -= size.z / 2; } if ((index & 0b0010) > 0) { min.y += size.y / 2; } else { max.y -= size.y / 2; } if ((index & 0b0100) > 0) { min.x += size.x / 2; } else { max.x -= size.x / 2; } return new Box3(min, max); } const typenameTypeattributeMap = { double: PointAttributeTypes.DATA_TYPE_DOUBLE, float: PointAttributeTypes.DATA_TYPE_FLOAT, int8: PointAttributeTypes.DATA_TYPE_INT8, uint8: PointAttributeTypes.DATA_TYPE_UINT8, int16: PointAttributeTypes.DATA_TYPE_INT16, uint16: PointAttributeTypes.DATA_TYPE_UINT16, int32: PointAttributeTypes.DATA_TYPE_INT32, uint32: PointAttributeTypes.DATA_TYPE_UINT32, int64: PointAttributeTypes.DATA_TYPE_INT64, uint64: PointAttributeTypes.DATA_TYPE_UINT64, }; type AttributeType = keyof typeof typenameTypeattributeMap; // A buffer view carries information on how to extract attribute data from a binary buffer. // For the majority of cases byteLength and byteOffset will not be needed because matic will // always upload single attribute buffer files. However, to be prepared for potential future // support of combined buffers byteLength and byteOffset are present to understand where to // find the data inside the buffer. type BufferView = { byteLength: number; byteOffset: number; // The uri points to the particular source file and allows for arbitrary buffernames // when using metadata with gltf encoding. When using PotreeConverter 2 to generate the metadata // the uri can be ignored. It will default to the naming convention of the potree v2 format. uri: string; }; export interface Attribute { name: string; description: string; size: number; numElements: number; type: AttributeType; min: number[]; max: number[]; bufferView: BufferView; } export interface Metadata { version: string; name: string; description: string; points: number; projection: string; hierarchy: { firstChunkSize: number; stepSize: number; depth: number; }; offset: [number, number, number]; scale: [number, number, number]; spacing: number; boundingBox: { min: [number, number, number]; max: [number, number, number]; }; encoding: string; attributes: Attribute[]; } export interface LoadingContext { workerPool: WorkerPool; basePath: string; hierarchyPath: string; octreePath: string; gltfColorsPath: string; gltfPositionsPath: string; harmonicsEnabled: boolean; getUrl: GetUrlFn; } export class OctreeLoader implements LoadingContext { workerPool: WorkerPool = new WorkerPool(); basePath = ''; hierarchyPath = ''; octreePath = ''; gltfColorsPath = ''; gltfPositionsPath = ''; harmonicsEnabled: boolean = false; getUrl: GetUrlFn; constructor(getUrl: GetUrlFn, url: string, loadHarmonics: boolean = false) { this.getUrl = getUrl; this.basePath = extractBasePath(url); this.hierarchyPath = buildUrl(this.basePath, HIERARCHY_FILE); this.octreePath = buildUrl(this.basePath, OCTREE_FILE); this.harmonicsEnabled = loadHarmonics; // We default to the known naming convention for glTF datasets this.gltfColorsPath = buildUrl(this.basePath, GLTF_COLORS_FILE); this.gltfPositionsPath = buildUrl(this.basePath, GLTF_POSITIONS_FILE); } static parseAttributes(jsonAttributes: Attribute[]) { const attributes = new PointAttributes(); const replacements: { [key: string]: string } = { rgb: 'rgba' }; for (const jsonAttribute of jsonAttributes) { const { name, numElements, min, max, bufferView } = jsonAttribute; const type = typenameTypeattributeMap[jsonAttribute.type]; const potreeAttributeName = replacements[name] ? replacements[name] : name; const attribute = new PointAttribute(potreeAttributeName, type, numElements); if (bufferView) { attribute.uri = bufferView.uri; } if (numElements === 1 && min && max) { attribute.range = [min[0], max[0]]; } else { attribute.range = [min, max]; } if (name === 'gps-time') { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909 if (typeof attribute.range[0] === 'number' && attribute.range[0] === attribute.range[1]) { attribute.range[1] += 1; } } attribute.initialRange = attribute.range; attributes.add(attribute); } { const hasNormals = attributes.attributes.find((a) => a.name === 'NormalX') !== undefined && attributes.attributes.find((a) => a.name === 'NormalY') !== undefined && attributes.attributes.find((a) => a.name === 'NormalZ') !== undefined; if (hasNormals) { const vector = { name: 'NORMAL', attributes: ['NormalX', 'NormalY', 'NormalZ'], }; attributes.addVector(vector); } } return attributes; } async load(url: string, xhrRequest: XhrRequest) { const metadata = await this.fetchMetadata(url, xhrRequest); const attributes = OctreeLoader.parseAttributes(metadata.attributes); this.applyCustomBufferURI(metadata.encoding, attributes); const loader = this.createLoader(url, metadata); const boundingBox = this.createBoundingBox(metadata); const offset = this.getOffset(boundingBox); const octree = this.initializeOctree(loader, url, metadata, boundingBox, offset, attributes); const root = this.initializeRootNode(octree, boundingBox, metadata); octree.root = root; loader.load(root); return { geometry: octree }; } private async fetchMetadata(url: string, xhrRequest: XhrRequest): Promise<Metadata> { const response = await xhrRequest(url); return response.json(); } private applyCustomBufferURI(encoding: string, attributes: any) { // Only datasets with GLTF encoding support custom buffer URIs - // as opposed to datasets with DEFAULT encoding coming from PotreeConverter if (encoding === 'GLTF') { this.gltfPositionsPath = attributes.getAttribute('position')?.uri ?? this.gltfPositionsPath; this.gltfColorsPath = attributes.getAttribute('rgba')?.uri ?? this.gltfColorsPath; } } private createLoader(url: string, metadata: Metadata): NodeLoader { return new NodeLoader(url, metadata, this); } private createBoundingBox(metadata: Metadata): Box3 { const min = new Vector3(...metadata.boundingBox.min); const max = new Vector3(...metadata.boundingBox.max); const boundingBox = new Box3(min, max); return boundingBox; } private getOffset(boundingBox: Box3): Vector3 { const offset = boundingBox.min.clone(); boundingBox.min.sub(offset); boundingBox.max.sub(offset); return offset; } private initializeOctree( loader: NodeLoader, url: string, metadata: Metadata, boundingBox: Box3, offset: Vector3, attributes: any, ): OctreeGeometry { const octree = new OctreeGeometry(loader, boundingBox); octree.url = url; octree.spacing = metadata.spacing; octree.scale = metadata.scale; octree.projection = metadata.projection; octree.boundingBox = boundingBox; octree.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); octree.tightBoundingSphere = boundingBox.getBoundingSphere(new Sphere()); octree.tightBoundingBox = this.getTightBoundingBox(metadata); octree.offset = offset; octree.pointAttributes = attributes; return octree; } private initializeRootNode( octree: OctreeGeometry, boundingBox: Box3, metadata: Metadata, ): OctreeGeometryNode { const root = new OctreeGeometryNode('r', octree, boundingBox); root.level = 0; root.nodeType = 2; root.hierarchyByteOffset = BigInt(0); root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize); root.spacing = octree.spacing; root.byteOffset = BigInt(0); return root; } getTightBoundingBox(metadata: Metadata): Box3 { const positionAttribute = metadata.attributes.find((attr) => attr.name === 'position'); if (!positionAttribute || !positionAttribute.min || !positionAttribute.max) { console.warn( 'Position attribute (min, max) not found. Falling back to boundingBox for tightBoundingBox', ); return new Box3( new Vector3(...metadata.boundingBox.min), new Vector3(...metadata.boundingBox.max), ); } const offset = metadata.boundingBox.min; const tightBoundingBox = new Box3( new Vector3( positionAttribute.min[0] - offset[0], positionAttribute.min[1] - offset[1], positionAttribute.min[2] - offset[2], ), new Vector3( positionAttribute.max[0] - offset[0], positionAttribute.max[1] - offset[1], positionAttribute.max[2] - offset[2], ), ); return tightBoundingBox; } }