UNPKG

@pnext/three-loader

Version:

Potree loader for ThreeJS, converted and adapted to Typescript.

564 lines (450 loc) 17.8 kB
import { Mesh, Vector2, RawShaderMaterial, InstancedBufferGeometry, BufferGeometry, InstancedBufferAttribute, RGBAIntegerFormat, UnsignedIntType, DataTexture, RGBAFormat, FloatType, RGFormat, BufferAttribute, Matrix4, Vector3, RedIntegerFormat, Camera, Quaternion, Texture, ShaderMaterial, DoubleSide, GLSL3, Object3D, NearestFilter, } from 'three'; import { createSortWorker } from './workers/SortWorker'; import { PointCloudMaterial } from './materials'; const DELAYED_FRAMES = 1; export class SplatsMesh extends Object3D { public mesh: any; public material: ShaderMaterial | null = null; public forceSorting: boolean = true; private nodesAsString: string = ''; private texturePosColor: any; private textureCovariance0: any; private textureCovariance1: any; private textureNode: any; private textureNode2: any; private textureNodeIndices: any; private textureHarmonics1: any; private textureHarmonics2: any; private textureHarmonics3: any; private textureVisibilityNodes: any; private bufferCenters: any; private bufferPositions: any; private bufferScale: any; private bufferOrientation: any; private bufferPosColor: any; private bufferCovariance0: any; private bufferCovariance1: any; private bufferNodes: any; private bufferNodes2: any; private bufferNodesIndices: any; private bufferHarmonics1: any; private bufferHarmonics2: any; private bufferHarmonics3: any; private bufferVisibilityNodes: any; private sorter: any; private lastSortViewDir = new Vector3(0, 0, -1); private sortViewDir = new Vector3(0, 0, -1); private lastSortViewPos = new Vector3(); private sortViewOffset = new Vector3(); private enableSorting = true; private indexesBuffer: any; private textures: Array<Texture> = new Array(); private enabled: boolean = false; private texturesNeedUpdate = false; private instanceCount: number = 0; private debugMode = false; rendererSize = new Vector2(); private harmonicsEnabled: boolean = false; constructor(debug: boolean = false) { super(); this.debugMode = debug; } async initialize(maxPointBudget: number, renderHamonics = false) { this.harmonicsEnabled = renderHamonics; this.sorter = await createSortWorker(maxPointBudget); this.indexesBuffer = new Int32Array(maxPointBudget); let indexesToSort = new Int32Array(maxPointBudget); for (let i = 0; i < maxPointBudget; i++) { this.indexesBuffer[i] = i; indexesToSort[i] = i; } const quadVertices = new Float32Array([-1, -1, 0.0, 1, -1, 0.0, -1, 1, 0.0, 1, 1, 0.0]); const quadIndices = new Uint16Array([0, 1, 2, 2, 1, 3]); //Global mesh used to setup the global rendering of the points let shader = new ShaderMaterial({ glslVersion: GLSL3, vertexShader: require('./materials/shaders/splats.vert').default, fragmentShader: require('./materials/shaders/splats.frag').default, transparent: true, depthTest: true, depthWrite: false, side: DoubleSide, uniforms: { focal: { value: new Vector2(0, 0) }, inverseFocalAdjustment: { value: 1 }, splatScale: { value: 1 }, basisViewport: { value: new Vector2(0, 0) }, covarianceTexture0: { value: null }, covarianceTexture1: { value: null }, posColorTexture: { value: null }, nodeTexture: { value: null }, nodeTexture2: { value: null }, nodeIndicesTexture: { value: null }, indicesTexture: { value: null }, harmonicsTexture1: { value: null }, harmonicsTexture2: { value: null }, harmonicsTexture3: { value: null }, visibleNodes: { value: null }, cameraPosition: { value: new Vector3(0, 0, 0) }, harmonicsDegree: { value: 3 }, renderIds: { value: false }, debugMode: { value: false }, renderOnlyHarmonics: { value: false }, adaptiveSize: { value: true }, harmonicsScale: { value: 4 }, octreeSize: { value: 0 }, fov: { value: 1 }, maxSplatScale: { value: 4 }, screenHeight: { value: 1 }, spacing: { value: 1 }, }, }); this.material = shader; let geom = new InstancedBufferGeometry(); geom.setAttribute('position', new BufferAttribute(quadVertices, 3)); geom.setIndex(new BufferAttribute(quadIndices, 1)); geom.setAttribute('indexes_sorted', new InstancedBufferAttribute(indexesToSort, 1)); this.mesh = new Mesh(geom, shader); this.mesh.frustumCulled = false; this.add(this.mesh); //Create the global textures let size = Math.ceil(Math.sqrt(maxPointBudget)); this.bufferCenters = new Float32Array(size * size * 4); this.bufferPositions = new Float32Array(size * size * 4); this.bufferScale = new Float32Array(size * size * 3); this.bufferOrientation = new Float32Array(size * size * 4); this.bufferPosColor = new Uint32Array(size * size * 4); this.bufferCovariance0 = new Float32Array(size * size * 4); this.bufferCovariance1 = new Float32Array(size * size * 2); this.bufferNodes = new Float32Array(100 * 100 * 4); this.bufferNodes2 = new Uint32Array(100 * 100 * 2); this.bufferNodesIndices = new Uint32Array(size * size); this.bufferVisibilityNodes = new Uint8Array(2048 * 4); //For the harmonics let degree1Size = Math.ceil(Math.sqrt(maxPointBudget * 3)); let degree2Size = Math.ceil(Math.sqrt(maxPointBudget * 5)); let degree3Size = Math.ceil(Math.sqrt(maxPointBudget * 7)); if (this.debugMode) console.log('max texture size: ' + degree3Size + ' point budget: ' + maxPointBudget); this.bufferHarmonics1 = new Uint32Array(degree1Size * degree1Size); this.bufferHarmonics2 = new Uint32Array(degree2Size * degree2Size); this.bufferHarmonics3 = new Uint32Array(degree3Size * degree3Size); //This should be able to save up to 10000 nodes this.textureNode = new DataTexture(this.bufferNodes, 100, 100, RGBAFormat, FloatType); this.textureNode2 = new DataTexture(this.bufferNodes2, 100, 100, RGFormat, UnsignedIntType); this.textureNode2.internalFormat = 'RG32UI'; this.textureNodeIndices = new DataTexture( this.bufferNodesIndices, size, size, RedIntegerFormat, UnsignedIntType, ); this.textureNodeIndices.internalFormat = 'R32UI'; this.textureCovariance0 = new DataTexture( this.bufferCovariance0, size, size, RGBAFormat, FloatType, ); this.textureCovariance1 = new DataTexture( this.bufferCovariance1, size, size, RGFormat, FloatType, ); this.texturePosColor = new DataTexture( this.bufferPosColor, size, size, RGBAIntegerFormat, UnsignedIntType, ); this.texturePosColor.internalFormat = 'RGBA32UI'; this.textureHarmonics1 = new DataTexture( this.bufferHarmonics1, degree1Size, degree1Size, RedIntegerFormat, UnsignedIntType, ); this.textureHarmonics1.internalFormat = 'R32UI'; this.textureHarmonics2 = new DataTexture( this.bufferHarmonics2, degree2Size, degree2Size, RedIntegerFormat, UnsignedIntType, ); this.textureHarmonics2.internalFormat = 'R32UI'; this.textureHarmonics3 = new DataTexture( this.bufferHarmonics3, degree3Size, degree3Size, RedIntegerFormat, UnsignedIntType, ); this.textureHarmonics3.internalFormat = 'R32UI'; this.textureVisibilityNodes = new DataTexture(this.bufferVisibilityNodes, 2048, 1, RGBAFormat); this.textureVisibilityNodes.magFilter = NearestFilter; this.textures.push(this.textureNode); this.textures.push(this.textureNodeIndices); this.textures.push(this.textureCovariance0); this.textures.push(this.textureCovariance1); this.textures.push(this.texturePosColor); this.textures.push(this.textureHarmonics1); this.textures.push(this.textureHarmonics2); this.textures.push(this.textureHarmonics3); this.textures.push(this.textureNode2); this.textures.push(this.textureVisibilityNodes); this.textures.map((text) => (text.needsUpdate = true)); this.material.uniforms['posColorTexture'].value = this.texturePosColor; this.material.uniforms['covarianceTexture0'].value = this.textureCovariance0; this.material.uniforms['covarianceTexture1'].value = this.textureCovariance1; this.material.uniforms['nodeTexture'].value = this.textureNode; this.material.uniforms['nodeTexture2'].value = this.textureNode2; this.material.uniforms['nodeIndicesTexture'].value = this.textureNodeIndices; this.material.uniforms['harmonicsTexture1'].value = this.textureHarmonics1; this.material.uniforms['harmonicsTexture2'].value = this.textureHarmonics2; this.material.uniforms['harmonicsTexture3'].value = this.textureHarmonics3; this.material.uniforms.visibleNodes.value = this.textureVisibilityNodes; this.enabled = true; } renderSplatsIDs(status: boolean) { if (this.material == null) return; this.material.uniforms['renderIds'].value = status; this.material.transparent = !status; } update(mesh: Mesh, camera: Camera, size: Vector2, callback = () => {}) { if (this.material == null) return; this.material.uniforms['cameraPosition'].value = camera.position; let mat = mesh.material as RawShaderMaterial; mat.visible = false; //Passing the uniforms from the point cloud material to the splats material. this.material.uniforms.octreeSize.value = mat.uniforms.octreeSize.value; this.material.uniforms.fov.value = mat.uniforms.fov.value; this.material.uniforms.spacing.value = mat.uniforms.spacing.value; this.material.uniforms.screenHeight.value = mat.uniforms.screenHeight.value; let material = this.material as RawShaderMaterial; material.uniforms.basisViewport.value.set(1.0 / size.x, 1.0 / size.y); const focalLengthX = camera.projectionMatrix.elements[0] * 0.5 * size.x; const focalLengthY = camera.projectionMatrix.elements[5] * 0.5 * size.y; material.uniforms.focal.value.set(focalLengthX, focalLengthY); let instanceCount = 0; let nodesCount = 0; let nodesAsString = ''; let totalMemoryUsed = 0; let totalMemoryInDisplay = 0; mesh.traverse((el) => { let m = el as Mesh; let g = m.geometry as BufferGeometry; instanceCount += g.drawRange.count; }); totalMemoryUsed = instanceCount * (this.harmonicsEnabled ? 236 : 56); mesh.traverseVisible((el) => { nodesAsString += el.name; }); this.forceSorting = false; //Copy the data from the visibility nodes, it uses a separated texture to sync when //it is updated in relationship with the other textures. this.bufferVisibilityNodes.set(mat.uniforms.visibleNodes.value.image.data); if (nodesAsString != this.nodesAsString && this.enableSorting) { this.nodesAsString = nodesAsString; instanceCount = 0; nodesCount = 0; let maxLevel = 0; mesh.traverseVisible((el) => { let m = el as Mesh; let g = m.geometry as BufferGeometry; let pointCloudMaterial = mesh.material as PointCloudMaterial; const vnStart = pointCloudMaterial.visibleNodeTextureOffsets.get(el.name)!; const level = m.name.length - 1; maxLevel = Math.max(maxLevel, level); let nodeInfo = [m.position.x, m.position.y, m.position.z, 1]; let nodeInfo2 = [level, vnStart]; this.bufferNodes.set(nodeInfo, nodesCount * 4); this.bufferNodes2.set(nodeInfo2, nodesCount * 2); this.bufferNodesIndices.set( new Uint32Array(g.drawRange.count).fill(nodesCount), instanceCount, ); //Used for sorting this.bufferCenters.set(g.getAttribute('raw_position').array, instanceCount * 4); //Used for raycasting this.bufferPositions.set(g.getAttribute('centers').array, instanceCount * 4); this.bufferScale.set(g.getAttribute('scale').array, instanceCount * 3); this.bufferOrientation.set(g.getAttribute('orientation').array, instanceCount * 4); //Used for rendering this.bufferCovariance0.set(g.getAttribute('COVARIANCE0').array, instanceCount * 4); this.bufferCovariance1.set(g.getAttribute('COVARIANCE1').array, instanceCount * 2); this.bufferPosColor.set(g.getAttribute('POS_COLOR').array, instanceCount * 4); if (this.harmonicsEnabled) { this.bufferHarmonics1.set(g.getAttribute('HARMONICS1').array, instanceCount * 3); this.bufferHarmonics2.set(g.getAttribute('HARMONICS2').array, instanceCount * 5); this.bufferHarmonics3.set(g.getAttribute('HARMONICS3').array, instanceCount * 7); } instanceCount += g.drawRange.count; nodesCount++; }); totalMemoryInDisplay = instanceCount * (this.harmonicsEnabled ? 236 : 56); if (this.debugMode) { console.log('----------------------------'); console.log('total memory in usage: ' + Math.ceil(totalMemoryUsed / 1000000) + ' MB'); console.log('total memory displayed: ' + Math.ceil(totalMemoryInDisplay / 1000000) + ' MB'); console.log('max level displayed: ' + maxLevel); console.log('----------------------------'); } this.instanceCount = instanceCount; this.texturesNeedUpdate = true; this.forceSorting = true; this.sortSplats(camera, callback); } } defer() { let promise = new Promise((resolve) => { let counter = 0; let frameCounter = () => { let anim = requestAnimationFrame(frameCounter); if (counter == DELAYED_FRAMES) { resolve('true'); cancelAnimationFrame(anim); } counter++; }; frameCounter(); }); return promise; } sortSplats(camera: Camera, callback = () => {}) { if (this.mesh == null || this.instanceCount == 0) return; let mvpMatrix = new Matrix4(); camera.updateMatrixWorld(); mvpMatrix.copy(camera.matrixWorld).invert(); mvpMatrix.premultiply(camera.projectionMatrix); mvpMatrix.multiply(this.mesh.matrixWorld); let angleDiff = 0; let positionDiff = 0; this.sortViewDir.set(0, 0, -1).applyQuaternion(camera.quaternion); angleDiff = this.sortViewDir.dot(this.lastSortViewDir); positionDiff = this.sortViewOffset.copy(camera.position).sub(this.lastSortViewPos).length(); if ((this.forceSorting || angleDiff <= 0.99 || positionDiff >= 1.0) && this.enableSorting) { let sortMessage = { indices: this.indexesBuffer, centers: this.bufferCenters, modelViewProj: mvpMatrix.elements, totalSplats: this.instanceCount, }; this.sorter.postMessage({ sort: sortMessage, }); this.enableSorting = false; this.forceSorting = false; this.sorter.onmessage = async (e: any) => { if (e.data.dataSorted) { if (e.data.dataSorted != null) { let indexAttribute = this.mesh.geometry.getAttribute('indexes_sorted'); indexAttribute.array.set(new Int32Array(e.data.dataSorted), 0); indexAttribute.needsUpdate = true; if (this.texturesNeedUpdate) { this.textures.map((text) => (text.needsUpdate = true)); this.texturesNeedUpdate = false; } this.mesh.geometry.instanceCount = this.instanceCount; this.defer().then((_) => { callback(); this.enableSorting = true; }); } else { this.enableSorting = true; } } }; this.lastSortViewPos.copy(camera.position); this.lastSortViewDir.copy(this.sortViewDir); } } getSplatData(globalID: any, nodeID: any) { if (this.mesh == null) return null; let center = new Vector3(); let offset = new Vector3(); let scale = new Vector3(); let orientation = new Quaternion(); center.x = this.bufferPositions[4 * globalID + 0]; center.y = this.bufferPositions[4 * globalID + 1]; center.z = this.bufferPositions[4 * globalID + 2]; scale.x = this.bufferScale[3 * globalID + 0]; scale.y = this.bufferScale[3 * globalID + 1]; scale.z = this.bufferScale[3 * globalID + 2]; orientation.w = this.bufferOrientation[4 * globalID + 0]; orientation.x = this.bufferOrientation[4 * globalID + 1]; orientation.y = this.bufferOrientation[4 * globalID + 2]; orientation.z = this.bufferOrientation[4 * globalID + 3]; offset.x = this.bufferNodes[4 * nodeID + 0]; offset.y = this.bufferNodes[4 * nodeID + 1]; offset.z = this.bufferNodes[4 * nodeID + 2]; center.add(offset); let result = this.mesh.localToWorld(center); return { position: result, scale, orientation, }; } dispose() { if (!this.enabled) return; //Terminate the sorter this.sorter.terminate(); this.sorter = null; //Removing attributes this.mesh.geometry.dispose(); //Remove the shader this.material?.dispose(); //Removing textures this.textures.map((text) => text.dispose()); this.textures = []; //kill the buffers this.indexesBuffer = null; this.bufferCenters = null; this.bufferPositions = null; this.bufferScale = null; this.bufferOrientation = null; this.bufferPosColor = null; this.bufferCovariance0 = null; this.bufferCovariance1 = null; this.bufferNodes = null; this.bufferNodesIndices = null; //kill the mesh this.mesh = null; this.enabled = false; } get splatsEnabled(): boolean { return this.enabled; } }