├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── dev.vite.config.ts ├── dist ├── potree-loader.es.js ├── potree-loader.es.js.map ├── potree-loader.umd.js └── potree-loader.umd.js.map ├── example ├── main.css ├── main.ts ├── tsconfig.json └── viewer.ts ├── index.html ├── package.json ├── src ├── constants.ts ├── dem-node.ts ├── features.ts ├── index.ts ├── loading │ ├── binary-loader.ts │ ├── index.ts │ ├── load-poc.ts │ └── types.ts ├── loading2 │ ├── OctreeGeometry.ts │ ├── OctreeGeometryNode.ts │ ├── OctreeLoader.ts │ ├── PointAttributes.ts │ ├── WorkerPool.ts │ ├── brotli-decoder.worker.js │ ├── decoder.worker.js │ ├── libs │ │ └── brotli │ │ │ ├── BUILD │ │ │ ├── LICENSE │ │ │ ├── WORKSPACE │ │ │ ├── decode.js │ │ │ ├── decode.min.js │ │ │ ├── decode_test.js │ │ │ └── polyfill.js │ └── load-octree.ts ├── materials │ ├── blur-material.ts │ ├── classification.ts │ ├── clipping.ts │ ├── color-encoding.ts │ ├── enums.ts │ ├── gradients │ │ ├── grayscale.ts │ │ ├── index.ts │ │ ├── inferno.ts │ │ ├── plasma.ts │ │ ├── rainbow.ts │ │ ├── spectral.ts │ │ ├── vidris.ts │ │ └── yellow-green.ts │ ├── index.ts │ ├── point-cloud-material.ts │ ├── shaders │ │ ├── blur.frag │ │ ├── blur.vert │ │ ├── edl.frag │ │ ├── edl.vert │ │ ├── normalize.frag │ │ ├── normalize.vert │ │ ├── pointcloud.frag │ │ └── pointcloud.vert │ ├── texture-generation.ts │ └── types.ts ├── point-attributes.ts ├── point-cloud-octree-geometry-node.ts ├── point-cloud-octree-geometry.ts ├── point-cloud-octree-node.ts ├── point-cloud-octree-picker.ts ├── point-cloud-octree.ts ├── point-cloud-tree.ts ├── potree.ts ├── type-predicates.ts ├── types.ts ├── utils │ ├── binary-heap.d.ts │ ├── binary-heap.js │ ├── bounds.ts │ ├── box3-helper.ts │ ├── lru.ts │ ├── math.ts │ └── utils.ts ├── version.ts └── workers │ ├── GreyhoundBinaryDecoderWorker.js │ ├── LASDecoderWorker.js │ ├── LazLoaderWorker.js │ ├── binary-decoder-worker-internal.ts │ ├── binary-decoder.worker.js │ └── custom-array-view.ts ├── tsconfig.json ├── tslint.json ├── vite.config.ts └── yarn.lock /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for your interest! In an effort to make it easier to respond to your problem we would like 2 | for you to consider a few things: 3 | 4 | * Can you try to phrase it as a task? 5 | Instead of "What unit is used in the cut view?" ask for "Clarify unit in cut view". 6 | 7 | * Can you make a screenshot or a screencast? 8 | Some graphic errors are hard to describe, a screenshot helps to get your problem accross. 9 | A service like [Gyazo Gif](https://gyazo.com/download) makes it possible to keep the link up for a long time. 10 | Please don't link to a url that could be gone in a month or two (like dropbox). 11 | 12 | * Is all information accessible for us? 13 | A cryptic error message like "Doesn't work on my computer" will not help us much. It would be awesome if 14 | you could provide a permalink or code sample to the problematic section. What OS, potree and browser version you are 15 | you using? How can we reproduce the issue? Logs can help too and 16 | [Gists](https://help.github.com/articles/creating-gists/) are a good idea as well. 17 | 18 | Thank you for your consideration, please delete this text after you read it. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .vscode/ 3 | .idea/ 4 | /build 5 | /pointclouds/alszeile 6 | /pointclouds/D 7 | /pointclouds/wrneustadt 8 | /test 9 | docs/.doc.md.html 10 | docs/.file_format.md.html 11 | docs/.how_does_loading_nodes_work.md.html 12 | docs/.how_to_create_your_own_pointcloud_loader.md.html 13 | docs/.marking_your_own_pointcloud_loader.md.html 14 | docs/.materials_and_rendermodes.md.html 15 | examples/index.html 16 | node_modules 17 | yarn-error.log 18 | size-plugin.json 19 | .parcel-cache 20 | clouds -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ========================= 2 | == PNext Three Loader == 3 | ========================= 4 | 5 | Copyright 2018 PNext 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 8 | associated documentation files (the "Software"), to deal in the Software without restriction, 9 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 10 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial 14 | portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 17 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 19 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | ============ 23 | == POTREE == 24 | ============ 25 | 26 | http://potree.org 27 | 28 | Copyright (c) 2011-2017, Markus Schütz 29 | All rights reserved. 30 | 31 | Redistribution and use in source and binary forms, with or without 32 | modification, are permitted provided that the following conditions are met: 33 | 34 | 1. Redistributions of source code must retain the above copyright notice, this 35 | list of conditions and the following disclaimer. 36 | 2. Redistributions in binary form must reproduce the above copyright notice, 37 | this list of conditions and the following disclaimer in the documentation 38 | and/or other materials provided with the distribution. 39 | 40 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 41 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 42 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 43 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 44 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 45 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 46 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 47 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 48 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 49 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 50 | 51 | The views and conclusions contained in the software and documentation are those 52 | of the authors and should not be interpreted as representing official policies, 53 | either expressed or implied, of the FreeBSD Project. 54 | 55 | 56 | ===================== 57 | == PLASIO / LASLAZ == 58 | ===================== 59 | 60 | http://plas.io/ 61 | 62 | The MIT License (MIT) 63 | 64 | Copyright (c) 2014 Uday Verma, uday.karan@gmail.com 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This is a fork of [three-loader](https://github.com/pnext/three-loader), which itself is a modularized version of [Potree](http://potree.org/). This fork is updated to support the new PotreeConverter 2.0 format, updated to use WebGL2, and has the bundler changed to Vite. The API is identical to the original version. I am mainly using this for a [heritage archive project](https://thelostmetropolis.org). 4 | 5 | # Known issues 6 | - Warning about THREE being imported twice 7 | 8 | # 9 | 10 | # Usage 11 | 12 | ```typescript 13 | import { Scene } from 'three'; 14 | import { PointCloudOctree, Potree } from 'potree-loader'; 15 | 16 | const scene = new Scene(); 17 | // Manages the necessary state for loading/updating one or more point clouds. 18 | const potree = new Potree(); 19 | // Show at most 2 million points. 20 | potree.pointBudget = 2_000_000; 21 | // List of point clouds which we loaded and need to update. 22 | const pointClouds: PointCloudOctree[] = []; 23 | 24 | potree 25 | .loadPointCloud( 26 | // The name of the point cloud which is to be loaded. 27 | 'metadata.json', 28 | // Given the relative URL of a file, should return a full URL (e.g. signed). 29 | relativeUrl => `${baseUrl}${relativeUrl}`, 30 | ) 31 | .then(pco => { 32 | pointClouds.push(pco); 33 | scene.add(pco); // Add the loaded point cloud to your ThreeJS scene. 34 | 35 | // The point cloud comes with a material which can be customized directly. 36 | // Here we just set the size of the points. 37 | pco.material.size = 1.0; 38 | }); 39 | 40 | function update() { 41 | // This is where most of the potree magic happens. It updates the visiblily of the octree nodes 42 | // based on the camera frustum and it triggers any loads/unloads which are necessary to keep the 43 | // number of visible points in check. 44 | potree.updatePointClouds(pointClouds, camera, renderer); 45 | 46 | // Render your scene as normal 47 | renderer.clear(); 48 | renderer.render(scene, camera); 49 | } 50 | ``` 51 | 52 | # Todo 53 | - [ ] Types! 54 | - [ ] Async / await 55 | - [ ] Occlusion culling 56 | -------------------------------------------------------------------------------- /dev.vite.config.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiukaheng/potree-loader/6069bb29d81cda981db52a050a6c3d23ac7fd056/dev.vite.config.ts -------------------------------------------------------------------------------- /example/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .container { 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | .btn-container { 12 | position: absolute; 13 | bottom: 20px; 14 | left: 20px; 15 | } 16 | 17 | .btn-container button { 18 | margin-right: 10px; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | } 24 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | import { PointCloudOctree } from '../src'; 3 | import { Viewer } from './viewer'; 4 | 5 | import "./main.css" 6 | import { PointCloudMaterial } from '../src/materials'; 7 | 8 | const targetEl = document.createElement('div'); 9 | targetEl.className = 'container'; 10 | document.body.appendChild(targetEl); 11 | 12 | const viewer = new Viewer(); 13 | viewer.initialize(targetEl); 14 | const camera = viewer.camera; 15 | camera.far = 1000; 16 | camera.updateProjectionMatrix(); 17 | camera.position.set(0, 0, 10); 18 | camera.lookAt(new Vector3()); 19 | 20 | let pointCloud: PointCloudOctree | undefined; 21 | let loaded: boolean = false; 22 | 23 | const unloadBtn = document.createElement('button'); 24 | unloadBtn.textContent = 'Unload'; 25 | unloadBtn.addEventListener('click', () => { 26 | if (!loaded) { 27 | return; 28 | } 29 | 30 | viewer.unload(); 31 | loaded = false; 32 | pointCloud = undefined; 33 | }); 34 | 35 | viewer 36 | .load( 37 | 'metadata.json', 38 | 'https://static.thelostmetropolis.org/BigShotCleanV2/', 39 | ) 40 | .then(pco => { 41 | pointCloud = pco; 42 | pointCloud.material.size = 1.0; 43 | pointCloud.material.shape = 2; 44 | pointCloud.material.inputColorEncoding = 1; 45 | pointCloud.material.outputColorEncoding = 1; 46 | pointCloud.position.set(0, -2, 1) 47 | pointCloud.scale.set(.1, .1, .1); 48 | viewer.add(pco); 49 | }) 50 | .catch(err => console.error(err)); 51 | 52 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "sourceMap": true, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "alwaysStrict": true, 15 | "target": "es6", 16 | "experimentalDecorators": true, 17 | "typeRoots": ["../node_modules/@types"], 18 | "lib": ["es2017", "dom"], 19 | "plugins": [] 20 | }, 21 | "include": ["**/*.ts"], 22 | "exclude": ["build", "node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /example/viewer.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, Scene, WebGLRenderer } from 'three'; 2 | import { PointCloudOctree, Potree } from '../src'; 3 | 4 | // tslint:disable-next-line:no-duplicate-imports 5 | import * as THREE from 'three'; 6 | // const OrbitControls = require('three-orbit-controls')(THREE); 7 | 8 | export class Viewer { 9 | /** 10 | * The element where we will insert our canvas. 11 | */ 12 | private targetEl: HTMLElement | undefined; 13 | /** 14 | * The ThreeJS renderer used to render the scene. 15 | */ 16 | private renderer = new WebGLRenderer(); 17 | /** 18 | * Our scene which will contain the point cloud. 19 | */ 20 | scene: Scene = new Scene(); 21 | /** 22 | * The camera used to view the scene. 23 | */ 24 | camera: PerspectiveCamera = new PerspectiveCamera(45, NaN, 0.1, 1000); 25 | /** 26 | * Controls which update the position of the camera. 27 | */ 28 | cameraControls!: any; 29 | /** 30 | * Out potree instance which handles updating point clouds, keeps track of loaded nodes, etc. 31 | */ 32 | private potree = new Potree(); 33 | /** 34 | * Array of point clouds which are in the scene and need to be updated. 35 | */ 36 | private pointClouds: PointCloudOctree[] = []; 37 | /** 38 | * The time (milliseconds) when `loop()` was last called. 39 | */ 40 | private prevTime: number | undefined; 41 | /** 42 | * requestAnimationFrame handle we can use to cancel the viewer loop. 43 | */ 44 | private reqAnimationFrameHandle: number | undefined; 45 | 46 | /** 47 | * Initializes the viewer into the specified element. 48 | * 49 | * @param targetEl 50 | * The element into which we should add the canvas where we will render the scene. 51 | */ 52 | initialize(targetEl: HTMLElement): void { 53 | if (this.targetEl || !targetEl) { 54 | return; 55 | } 56 | 57 | this.targetEl = targetEl; 58 | targetEl.appendChild(this.renderer.domElement); 59 | 60 | // this.cameraControls = new OrbitControls(this.camera, this.targetEl); 61 | 62 | this.resize(); 63 | window.addEventListener('resize', this.resize); 64 | 65 | requestAnimationFrame(this.loop); 66 | } 67 | 68 | /** 69 | * Performs any cleanup necessary to destroy/remove the viewer from the page. 70 | */ 71 | destroy(): void { 72 | if (this.targetEl) { 73 | this.targetEl.removeChild(this.renderer.domElement); 74 | this.targetEl = undefined; 75 | } 76 | 77 | window.removeEventListener('resize', this.resize); 78 | 79 | // TODO: clean point clouds or other objects added to the scene. 80 | 81 | if (this.reqAnimationFrameHandle !== undefined) { 82 | cancelAnimationFrame(this.reqAnimationFrameHandle); 83 | } 84 | } 85 | 86 | /** 87 | * Loads a point cloud into the viewer and returns it. 88 | * 89 | * @param fileName 90 | * The name of the point cloud which is to be loaded. 91 | * @param baseUrl 92 | * The url where the point cloud is located and from where we should load the octree nodes. 93 | */ 94 | load(fileName: string, baseUrl: string): Promise { 95 | return this.potree.loadPointCloud( 96 | // The file name of the point cloud which is to be loaded. 97 | fileName, 98 | // Given the relative URL of a file, should return a full URL. 99 | url => `${baseUrl}${url}`, 100 | ); 101 | } 102 | 103 | add(pco: PointCloudOctree): void { 104 | this.scene.add(pco); 105 | this.pointClouds.push(pco); 106 | } 107 | 108 | unload(): void { 109 | this.pointClouds.forEach(pco => { 110 | this.scene.remove(pco); 111 | pco.dispose(); 112 | }); 113 | 114 | this.pointClouds = []; 115 | } 116 | 117 | /** 118 | * Updates the point clouds, cameras or any other objects which are in the scene. 119 | * 120 | * @param dt 121 | * The time, in milliseconds, since the last update. 122 | */ 123 | update(_: number): void { 124 | // Alternatively, you could use Three's OrbitControls or any other 125 | // camera control system. 126 | // this.cameraControls.update(); 127 | 128 | // This is where most of the potree magic happens. It updates the 129 | // visiblily of the octree nodes based on the camera frustum and it 130 | // triggers any loads/unloads which are necessary to keep the number 131 | // of visible points in check. 132 | this.potree.updatePointClouds(this.pointClouds, this.camera, this.renderer); 133 | } 134 | 135 | /** 136 | * Renders the scene into the canvas. 137 | */ 138 | render(): void { 139 | this.renderer.clear(); 140 | this.renderer.render(this.scene, this.camera); 141 | } 142 | 143 | /** 144 | * The main loop of the viewer, called at 60FPS, if possible. 145 | */ 146 | loop = (time: number): void => { 147 | this.reqAnimationFrameHandle = requestAnimationFrame(this.loop); 148 | 149 | const prevTime = this.prevTime; 150 | this.prevTime = time; 151 | if (prevTime === undefined) { 152 | return; 153 | } 154 | 155 | this.update(time - prevTime); 156 | this.render(); 157 | }; 158 | 159 | /** 160 | * Triggered anytime the window gets resized. 161 | */ 162 | resize = () => { 163 | if (!this.targetEl) { 164 | return; 165 | } 166 | 167 | const { width, height } = this.targetEl.getBoundingClientRect(); 168 | this.camera.aspect = width / height; 169 | this.camera.updateProjectionMatrix(); 170 | this.renderer.setSize(width, height); 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "potree-loader", 3 | "version": "1.10.4", 4 | "description": "WebGL point cloud viewer", 5 | "keywords": [ 6 | "point", 7 | "cloud", 8 | "pointcloud", 9 | "octree", 10 | "entwine", 11 | "viewer", 12 | "threejs", 13 | "webgl", 14 | "browser", 15 | "tool" 16 | ], 17 | "author": "Heng", 18 | "license": "BSD-2-CLAUSE", 19 | "repository": "https://github.com/shiukaheng/potree-loader", 20 | "dependencies": { 21 | "proj4": "^2.7.5", 22 | "vite": "^2.8.6" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "main": "./dist/potree-loader.umd.js", 28 | "module": "./dist/potree-loader.es.js", 29 | "types": "./dist/potree-loader.d.ts", 30 | "scripts": { 31 | "start": "vite", 32 | "build": "vite build" 33 | }, 34 | "devDependencies": { 35 | "@types/three": "^0.138.0", 36 | "concurrently": "^7.0.0", 37 | "path": "^0.12.7", 38 | "three": "^0.138.3", 39 | "typescript": ">=3.0.0", 40 | "vite-plugin-glsl": "^0.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { Color, Vector4 } from 'three'; 2 | 3 | export const DEFAULT_RGB_BRIGHTNESS = 0; 4 | export const DEFAULT_RGB_CONTRAST = 0; 5 | export const DEFAULT_RGB_GAMMA = 1; 6 | export const DEFAULT_MAX_POINT_SIZE = 50; 7 | export const DEFAULT_MIN_NODE_PIXEL_SIZE = 50; 8 | export const DEFAULT_MIN_POINT_SIZE = 2; 9 | export const DEFAULT_PICK_WINDOW_SIZE = 15; 10 | export const DEFAULT_POINT_BUDGET = 1_000_000; 11 | export const MAX_LOADS_TO_GPU = 2; 12 | export const MAX_NUM_NODES_LOADING = 4; 13 | export const PERSPECTIVE_CAMERA = 'PerspectiveCamera'; 14 | export const COLOR_BLACK = new Color(0, 0, 0); 15 | export const DEFAULT_HIGHLIGHT_COLOR = new Vector4(1, 0, 0, 1); 16 | -------------------------------------------------------------------------------- /src/dem-node.ts: -------------------------------------------------------------------------------- 1 | import { Box3, Vector2, Vector3 } from 'three'; 2 | 3 | export class DEMNode { 4 | level: number; 5 | data: Float32Array; 6 | children: DEMNode[]; 7 | mipMap: Float32Array[]; 8 | mipMapNeedsUpdate: boolean = true; 9 | 10 | constructor(public name: string, public box: Box3, public tileSize: number) { 11 | this.level = this.name.length - 1; 12 | this.data = new Float32Array(tileSize * tileSize); 13 | this.data.fill(-Infinity); 14 | this.children = []; 15 | 16 | this.mipMap = [this.data]; 17 | } 18 | 19 | createMipMap() { 20 | this.mipMap = [this.data]; 21 | 22 | let sourceSize = this.tileSize; 23 | let mipSize = sourceSize / 2; 24 | let mipSource = this.data; 25 | while (mipSize > 1) { 26 | const mipData = new Float32Array(mipSize * mipSize); 27 | 28 | for (let i = 0; i < mipSize; i++) { 29 | for (let j = 0; j < mipSize; j++) { 30 | const h00 = mipSource[2 * i + 0 + 2 * j * sourceSize]; 31 | const h01 = mipSource[2 * i + 0 + 2 * j * sourceSize + sourceSize]; 32 | const h10 = mipSource[2 * i + 1 + 2 * j * sourceSize]; 33 | const h11 = mipSource[2 * i + 1 + 2 * j * sourceSize + sourceSize]; 34 | 35 | let [height, weight] = [0, 0]; 36 | 37 | if (isFinite(h00)) { 38 | height += h00; 39 | weight += 1; 40 | } 41 | if (isFinite(h01)) { 42 | height += h01; 43 | weight += 1; 44 | } 45 | if (isFinite(h10)) { 46 | height += h10; 47 | weight += 1; 48 | } 49 | if (isFinite(h11)) { 50 | height += h11; 51 | weight += 1; 52 | } 53 | 54 | height = height / weight; 55 | 56 | // let hs = [h00, h01, h10, h11].filter(h => isFinite(h)); 57 | // let height = hs.reduce( (a, v, i) => a + v, 0) / hs.length; 58 | 59 | mipData[i + j * mipSize] = height; 60 | } 61 | } 62 | 63 | this.mipMap.push(mipData); 64 | 65 | mipSource = mipData; 66 | sourceSize = mipSize; 67 | mipSize = Math.floor(mipSize / 2); 68 | } 69 | 70 | this.mipMapNeedsUpdate = false; 71 | } 72 | 73 | uv(position: Vector2): [number, number] { 74 | const boxSize = new Vector3(); 75 | this.box.getSize(boxSize); 76 | 77 | const u = (position.x - this.box.min.x) / boxSize.x; 78 | const v = (position.y - this.box.min.y) / boxSize.y; 79 | 80 | return [u, v]; 81 | } 82 | 83 | heightAtMipMapLevel(position: Vector2, mipMapLevel: number) { 84 | const uv = this.uv(position); 85 | 86 | const tileSize = Math.floor(this.tileSize / Math.floor(2 ** mipMapLevel)); 87 | const data = this.mipMap[mipMapLevel]; 88 | 89 | const i = Math.min(uv[0] * tileSize, tileSize - 1); 90 | const j = Math.min(uv[1] * tileSize, tileSize - 1); 91 | 92 | const a = i % 1; 93 | const b = j % 1; 94 | 95 | const [i0, i1] = [Math.floor(i), Math.ceil(i)]; 96 | const [j0, j1] = [Math.floor(j), Math.ceil(j)]; 97 | 98 | const h00 = data[i0 + tileSize * j0]; 99 | const h01 = data[i0 + tileSize * j1]; 100 | const h10 = data[i1 + tileSize * j0]; 101 | const h11 = data[i1 + tileSize * j1]; 102 | 103 | let wh00 = isFinite(h00) ? (1 - a) * (1 - b) : 0; 104 | let wh01 = isFinite(h01) ? (1 - a) * b : 0; 105 | let wh10 = isFinite(h10) ? a * (1 - b) : 0; 106 | let wh11 = isFinite(h11) ? a * b : 0; 107 | 108 | const wsum = wh00 + wh01 + wh10 + wh11; 109 | wh00 = wh00 / wsum; 110 | wh01 = wh01 / wsum; 111 | wh10 = wh10 / wsum; 112 | wh11 = wh11 / wsum; 113 | 114 | if (wsum === 0) { 115 | return null; 116 | } 117 | 118 | let h = 0; 119 | 120 | if (isFinite(h00)) { 121 | h += h00 * wh00; 122 | } 123 | if (isFinite(h01)) { 124 | h += h01 * wh01; 125 | } 126 | if (isFinite(h10)) { 127 | h += h10 * wh10; 128 | } 129 | if (isFinite(h11)) { 130 | h += h11 * wh11; 131 | } 132 | 133 | return h; 134 | } 135 | 136 | height(position: Vector2) { 137 | let h = null; 138 | 139 | for (let i = 0; i < this.mipMap.length; i++) { 140 | h = this.heightAtMipMapLevel(position, i); 141 | 142 | if (h !== null) { 143 | return h; 144 | } 145 | } 146 | 147 | return h; 148 | } 149 | 150 | traverse(handler: (node: DEMNode, level: number) => void, level = 0) { 151 | handler(this, level); 152 | 153 | this.children.filter(c => c !== undefined).forEach(child => child.traverse(handler, level + 1)); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/features.ts: -------------------------------------------------------------------------------- 1 | const canvas = document.createElement('canvas'); 2 | const gl: WebGLRenderingContext | null = canvas.getContext('webgl'); 3 | 4 | export const FEATURES = { 5 | SHADER_INTERPOLATION: hasExtension('EXT_frag_depth') && hasMinVaryingVectors(8), 6 | SHADER_SPLATS: 7 | hasExtension('EXT_frag_depth') && hasExtension('OES_texture_float') && hasMinVaryingVectors(8), 8 | SHADER_EDL: hasExtension('OES_texture_float') && hasMinVaryingVectors(8), 9 | precision: getPrecision(), 10 | }; 11 | 12 | function hasExtension(ext: string) { 13 | return gl !== null && Boolean(gl.getExtension(ext)); 14 | } 15 | 16 | function hasMinVaryingVectors(value: number) { 17 | return gl !== null && gl.getParameter(gl.MAX_VARYING_VECTORS) >= value; 18 | } 19 | 20 | function getPrecision() { 21 | if (gl === null) { 22 | return ''; 23 | } 24 | 25 | const vsHighpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT); 26 | const vsMediumpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT); 27 | 28 | const fsHighpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); 29 | const fsMediumpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT); 30 | 31 | const highpAvailable = 32 | vsHighpFloat && fsHighpFloat && vsHighpFloat.precision > 0 && fsHighpFloat.precision > 0; 33 | 34 | const mediumpAvailable = 35 | vsMediumpFloat && 36 | fsMediumpFloat && 37 | vsMediumpFloat.precision > 0 && 38 | fsMediumpFloat.precision > 0; 39 | 40 | return highpAvailable ? 'highp' : mediumpAvailable ? 'mediump' : 'lowp'; 41 | } 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './materials'; 2 | // export * from './point-attributes'; 3 | // export * from './point-cloud-octree-geometry-node'; 4 | // export * from './point-cloud-octree-geometry'; 5 | // export * from './point-cloud-octree-node'; 6 | // export * from './point-cloud-octree-picker'; 7 | export * from './point-cloud-octree'; 8 | // export * from './point-cloud-tree'; // Attempted hotfix for duplicate export error. 9 | export * from './potree'; 10 | export * from './types'; 11 | export * from './version'; 12 | -------------------------------------------------------------------------------- /src/loading/binary-loader.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------------------------- 2 | // Converted to Typescript and adapted from https://github.com/potree/potree 3 | // ------------------------------------------------------------------------------------------------- 4 | 5 | import { Box3, BufferAttribute, BufferGeometry, Uint8BufferAttribute, Vector3 } from 'three'; 6 | import { PointAttributeName, PointAttributeType } from '../point-attributes'; 7 | import { PointCloudOctreeGeometryNode } from '../point-cloud-octree-geometry-node'; 8 | import { Version } from '../version'; 9 | import { GetUrlFn, XhrRequest } from './types'; 10 | 11 | import ClassicWorker from "../workers/binary-decoder.worker.js?worker&inline"; 12 | 13 | interface AttributeData { 14 | attribute: { 15 | name: PointAttributeName; 16 | type: PointAttributeType; 17 | byteSize: number; 18 | numElements: number; 19 | }; 20 | buffer: ArrayBuffer; 21 | } 22 | 23 | interface WorkerResponse { 24 | data: { 25 | attributeBuffers: { [name: string]: AttributeData }; 26 | indices: ArrayBuffer; 27 | tightBoundingBox: { min: number[]; max: number[] }; 28 | mean: number[]; 29 | }; 30 | } 31 | 32 | interface BinaryLoaderOptions { 33 | getUrl?: GetUrlFn; 34 | version: string; 35 | boundingBox: Box3; 36 | scale: number; 37 | xhrRequest: XhrRequest; 38 | } 39 | 40 | type Callback = (node: PointCloudOctreeGeometryNode) => void; 41 | 42 | export class BinaryLoader { 43 | version: Version; 44 | boundingBox: Box3; 45 | scale: number; 46 | getUrl: GetUrlFn; 47 | disposed: boolean = false; 48 | xhrRequest: XhrRequest; 49 | callbacks: Callback[]; 50 | 51 | private workers: Worker[] = []; 52 | 53 | constructor({ 54 | getUrl = s => Promise.resolve(s), 55 | version, 56 | boundingBox, 57 | scale, 58 | xhrRequest, 59 | }: BinaryLoaderOptions) { 60 | console.log([getUrl, version, boundingBox, scale, xhrRequest]) 61 | if (typeof version === 'string') { 62 | this.version = new Version(version); 63 | } else { 64 | this.version = version; 65 | } 66 | 67 | this.xhrRequest = xhrRequest; 68 | this.getUrl = getUrl; 69 | this.boundingBox = boundingBox; 70 | this.scale = scale; 71 | this.callbacks = []; 72 | } 73 | 74 | dispose(): void { 75 | this.workers.forEach(worker => worker.terminate()); 76 | this.workers = []; 77 | 78 | this.disposed = true; 79 | } 80 | 81 | load(node: PointCloudOctreeGeometryNode): Promise { 82 | if (node.loaded || this.disposed) { 83 | return Promise.resolve(); 84 | } 85 | 86 | return Promise.resolve(this.getUrl(this.getNodeUrl(node))) 87 | .then(url => this.xhrRequest(url, { mode: 'cors' })) 88 | .then(res => res.arrayBuffer()) 89 | .then(buffer => { 90 | return new Promise(resolve => this.parse(node, buffer, resolve)); 91 | }); 92 | } 93 | 94 | private getNodeUrl(node: PointCloudOctreeGeometryNode): string { 95 | let url = node.getUrl(); 96 | if (this.version.equalOrHigher('1.4')) { 97 | url += '.bin'; 98 | } 99 | 100 | return url; 101 | } 102 | 103 | private parse( 104 | node: PointCloudOctreeGeometryNode, 105 | buffer: ArrayBuffer, 106 | resolve: () => void, 107 | ): void { 108 | if (this.disposed) { 109 | resolve(); 110 | return; 111 | } 112 | 113 | const worker = this.getWorker(); 114 | 115 | const pointAttributes = node.pcoGeometry.pointAttributes; 116 | const numPoints = buffer.byteLength / pointAttributes.byteSize; 117 | 118 | if (this.version.upTo('1.5')) { 119 | node.numPoints = numPoints; 120 | } 121 | 122 | worker.onmessage = (e: WorkerResponse) => { 123 | if (this.disposed) { 124 | resolve(); 125 | return; 126 | } 127 | 128 | const data = e.data; 129 | 130 | const geometry = (node.geometry = node.geometry || new BufferGeometry()); 131 | geometry.boundingBox = node.boundingBox; 132 | 133 | this.addBufferAttributes(geometry, data.attributeBuffers); 134 | this.addIndices(geometry, data.indices); 135 | this.addNormalAttribute(geometry, numPoints); 136 | 137 | node.mean = new Vector3().fromArray(data.mean); 138 | node.tightBoundingBox = this.getTightBoundingBox(data.tightBoundingBox); 139 | node.loaded = true; 140 | node.loading = false; 141 | node.failed = false; 142 | node.pcoGeometry.numNodesLoading--; 143 | node.pcoGeometry.needsUpdate = true; 144 | 145 | this.releaseWorker(worker); 146 | 147 | this.callbacks.forEach(callback => callback(node)); 148 | resolve(); 149 | }; 150 | 151 | const message = { 152 | buffer, 153 | pointAttributes, 154 | version: this.version.version, 155 | min: node.boundingBox.min.toArray(), 156 | offset: node.pcoGeometry.offset.toArray(), 157 | scale: this.scale, 158 | spacing: node.spacing, 159 | hasChildren: node.hasChildren, 160 | }; 161 | 162 | worker.postMessage(message, [message.buffer]); 163 | } 164 | 165 | private getWorker(): Worker { 166 | const worker = this.workers.pop(); 167 | if (worker) { 168 | return worker; 169 | } 170 | 171 | // return new Worker( 172 | // new URL('../workers/binary-decoder.worker.js', import.meta.url), 173 | // { type: 'module' }, 174 | // ) 175 | return new ClassicWorker(); 176 | } 177 | 178 | private releaseWorker(worker: Worker): void { 179 | this.workers.push(worker); 180 | } 181 | 182 | private getTightBoundingBox({ min, max }: { min: number[]; max: number[] }): Box3 { 183 | const box = new Box3(new Vector3().fromArray(min), new Vector3().fromArray(max)); 184 | box.max.sub(box.min); 185 | box.min.set(0, 0, 0); 186 | 187 | return box; 188 | } 189 | 190 | private addBufferAttributes( 191 | geometry: BufferGeometry, 192 | buffers: { [name: string]: { buffer: ArrayBuffer } }, 193 | ): void { 194 | Object.keys(buffers).forEach(property => { 195 | const buffer = buffers[property].buffer; 196 | 197 | if (this.isAttribute(property, PointAttributeName.POSITION_CARTESIAN)) { 198 | geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); 199 | } else if (this.isAttribute(property, PointAttributeName.COLOR_PACKED)) { 200 | geometry.setAttribute('color', new BufferAttribute(new Uint8Array(buffer), 3, true)); 201 | } else if (this.isAttribute(property, PointAttributeName.INTENSITY)) { 202 | geometry.setAttribute('intensity', new BufferAttribute(new Float32Array(buffer), 1)); 203 | } else if (this.isAttribute(property, PointAttributeName.CLASSIFICATION)) { 204 | geometry.setAttribute('classification', new BufferAttribute(new Uint8Array(buffer), 1)); 205 | } else if (this.isAttribute(property, PointAttributeName.NORMAL_SPHEREMAPPED)) { 206 | geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); 207 | } else if (this.isAttribute(property, PointAttributeName.NORMAL_OCT16)) { 208 | geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); 209 | } else if (this.isAttribute(property, PointAttributeName.NORMAL)) { 210 | geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); 211 | } 212 | }); 213 | } 214 | 215 | private addIndices(geometry: BufferGeometry, indices: ArrayBuffer): void { 216 | const indicesAttribute = new Uint8BufferAttribute(indices, 4); 217 | indicesAttribute.normalized = true; 218 | geometry.setAttribute('indices', indicesAttribute); 219 | } 220 | 221 | private addNormalAttribute(geometry: BufferGeometry, numPoints: number): void { 222 | if (!geometry.getAttribute('normal')) { 223 | const buffer = new Float32Array(numPoints * 3); 224 | geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); 225 | } 226 | } 227 | 228 | private isAttribute(property: string, name: PointAttributeName): boolean { 229 | return parseInt(property, 10) === name; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/loading/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binary-loader'; 2 | export * from './load-poc'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/loading/load-poc.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------------------------- 2 | // Converted to Typescript and adapted from https://github.com/potree/potree 3 | // ------------------------------------------------------------------------------------------------- 4 | 5 | import { Box3, Vector3 } from 'three'; 6 | import { PointAttributes, PointAttributeStringName } from '../point-attributes'; 7 | import { PointCloudOctreeGeometry } from '../point-cloud-octree-geometry'; 8 | import { PointCloudOctreeGeometryNode } from '../point-cloud-octree-geometry-node'; 9 | import { createChildAABB } from '../utils/bounds'; 10 | import { getIndexFromName } from '../utils/utils'; 11 | import { Version } from '../version'; 12 | import { BinaryLoader } from './binary-loader'; 13 | import { GetUrlFn, XhrRequest } from './types'; 14 | 15 | interface BoundingBoxData { 16 | lx: number; 17 | ly: number; 18 | lz: number; 19 | ux: number; 20 | uy: number; 21 | uz: number; 22 | } 23 | 24 | interface POCJson { 25 | version: string; 26 | octreeDir: string; 27 | projection: string; 28 | points: number; 29 | boundingBox: BoundingBoxData; 30 | tightBoundingBox?: BoundingBoxData; 31 | pointAttributes: PointAttributeStringName[]; 32 | spacing: number; 33 | scale: number; 34 | hierarchyStepSize: number; 35 | hierarchy: [string, number][]; // [name, numPoints][] 36 | } 37 | 38 | /** 39 | * 40 | * @param url 41 | * The url of the point cloud file (usually cloud.js). 42 | * @param getUrl 43 | * Function which receives the relative URL of a point cloud chunk file which is to be loaded 44 | * and shoud return a new url (e.g. signed) in the form of a string or a promise. 45 | * @param xhrRequest An arrow function for a fetch request 46 | * @returns 47 | * An observable which emits once when the first LOD of the point cloud is loaded. 48 | */ 49 | export function loadPOC( 50 | url: string, 51 | getUrl: GetUrlFn, 52 | xhrRequest: XhrRequest, 53 | ): Promise { 54 | return Promise.resolve(getUrl(url)).then(transformedUrl => { // 1. Make a request to the URL 55 | return xhrRequest(transformedUrl, { mode: 'cors' }) 56 | .then(res => res.json()) 57 | .then(parse(transformedUrl, getUrl, xhrRequest)); // 2. Parse the response 58 | }); 59 | } 60 | 61 | function parse(url: string, getUrl: GetUrlFn, xhrRequest: XhrRequest) { 62 | return (data: POCJson): Promise => { // Note: The response gets passed from loadPOC() 63 | const { offset, boundingBox, tightBoundingBox } = getBoundingBoxes(data); 64 | const loader = new BinaryLoader({ 65 | getUrl, 66 | version: data.version, 67 | boundingBox, 68 | scale: data.scale, 69 | xhrRequest, 70 | }); // 3. Create a BinaryLoader with the bounding box and scale 71 | 72 | const pco = new PointCloudOctreeGeometry( 73 | loader, 74 | boundingBox, 75 | tightBoundingBox, 76 | offset, 77 | xhrRequest, 78 | ); // 4. Create a PointCloudOctreeGeometry 79 | 80 | // 5. Fill in Geometry with the data from the POCJson 81 | pco.url = url; 82 | pco.octreeDir = data.octreeDir; 83 | pco.needsUpdate = true; 84 | pco.spacing = data.spacing; 85 | pco.hierarchyStepSize = data.hierarchyStepSize; 86 | pco.projection = data.projection; 87 | pco.offset = offset; 88 | pco.pointAttributes = new PointAttributes(data.pointAttributes); 89 | console.log(pco.pointAttributes) 90 | 91 | 92 | 93 | const nodes: Record = {}; // HMM! Juicy! 6. Create a map of nodes 94 | 95 | const version = new Version(data.version); 96 | 97 | return loadRoot(pco, data, nodes, version).then(() => { // 7. Load the root node 98 | if (version.upTo('1.4')) { 99 | loadRemainingHierarchy(pco, data, nodes); 100 | } 101 | 102 | pco.nodes = nodes; 103 | return pco; 104 | }); 105 | }; 106 | } 107 | 108 | function getBoundingBoxes( 109 | data: POCJson, 110 | ): { offset: Vector3; boundingBox: Box3; tightBoundingBox: Box3 } { 111 | const min = new Vector3(data.boundingBox.lx, data.boundingBox.ly, data.boundingBox.lz); 112 | const max = new Vector3(data.boundingBox.ux, data.boundingBox.uy, data.boundingBox.uz); 113 | const boundingBox = new Box3(min, max); 114 | const tightBoundingBox = boundingBox.clone(); 115 | 116 | const offset = min.clone(); 117 | 118 | if (data.tightBoundingBox) { 119 | const { lx, ly, lz, ux, uy, uz } = data.tightBoundingBox; 120 | tightBoundingBox.min.set(lx, ly, lz); 121 | tightBoundingBox.max.set(ux, uy, uz); 122 | } 123 | 124 | boundingBox.min.sub(offset); 125 | boundingBox.max.sub(offset); 126 | tightBoundingBox.min.sub(offset); 127 | tightBoundingBox.max.sub(offset); 128 | 129 | return { offset, boundingBox, tightBoundingBox }; 130 | } 131 | 132 | function loadRoot( 133 | pco: PointCloudOctreeGeometry, 134 | data: POCJson, 135 | nodes: Record, 136 | version: Version, 137 | ): Promise { 138 | const name = 'r'; 139 | 140 | const root = new PointCloudOctreeGeometryNode(name, pco, pco.boundingBox); 141 | root.hasChildren = true; 142 | root.spacing = pco.spacing; // Fill in root info from the POCJson 143 | 144 | if (version.upTo('1.5')) { 145 | root.numPoints = data.hierarchy[0][1]; 146 | } else { 147 | root.numPoints = 0; 148 | } 149 | 150 | pco.root = root; 151 | nodes[name] = root; 152 | return pco.root.load(); 153 | } 154 | 155 | function loadRemainingHierarchy( 156 | pco: PointCloudOctreeGeometry, 157 | data: POCJson, 158 | nodes: Record, 159 | ): void { 160 | for (let i = 1; i < data.hierarchy.length; i++) { 161 | const [name, numPoints] = data.hierarchy[i]; 162 | const { index, parentName, level } = parseName(name); 163 | const parentNode = nodes[parentName]; 164 | 165 | const boundingBox = createChildAABB(parentNode.boundingBox, index); 166 | const node = new PointCloudOctreeGeometryNode(name, pco, boundingBox); 167 | node.level = level; 168 | node.numPoints = numPoints; 169 | node.spacing = pco.spacing / Math.pow(2, node.level); 170 | 171 | nodes[name] = node; 172 | parentNode.addChild(node); 173 | } 174 | } 175 | 176 | function parseName(name: string): { index: number; parentName: string; level: number } { 177 | return { 178 | index: getIndexFromName(name), 179 | parentName: name.substring(0, name.length - 1), 180 | level: name.length - 1, 181 | }; 182 | } 183 | -------------------------------------------------------------------------------- /src/loading/types.ts: -------------------------------------------------------------------------------- 1 | export type GetUrlFn = (url: string) => string | Promise; 2 | export type XhrRequest = (input: RequestInfo, init?: RequestInit) => Promise; 3 | -------------------------------------------------------------------------------- /src/loading2/OctreeGeometry.ts: -------------------------------------------------------------------------------- 1 | import { NodeLoader, Metadata } from './OctreeLoader'; 2 | 3 | // import * as THREE from "../../../../libs/three.js/build/three.module.js"; 4 | import { Box3,Sphere, Vector3 } from "three"; 5 | import { PointAttributes } from "./PointAttributes"; 6 | import { OctreeGeometryNode } from './OctreeGeometryNode'; 7 | 8 | export class OctreeGeometry{ 9 | root!: OctreeGeometryNode; 10 | url: string | null = null; 11 | pointAttributes: PointAttributes | null = null; 12 | spacing: number = 0; 13 | tightBoundingBox: Box3; 14 | numNodesLoading: number = 0; 15 | maxNumNodesLoading: number = 3; // I don't understand why this is also a property of IPotree then. Duplicate functionality? 16 | boundingSphere: Sphere; 17 | tightBoundingSphere: Sphere; 18 | offset!: Vector3; 19 | scale!: [number, number, number]; 20 | disposed: boolean = false; 21 | 22 | projection?: Metadata["projection"]; 23 | constructor( 24 | public loader: NodeLoader, 25 | public boundingBox: Box3, // Need to be get from metadata.json 26 | ){ 27 | this.tightBoundingBox = this.boundingBox.clone(); 28 | this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere()); 29 | this.tightBoundingSphere = this.boundingBox.getBoundingSphere(new Sphere()); 30 | } 31 | 32 | dispose(): void { 33 | // this.loader.dispose(); 34 | this.root.traverse(node => node.dispose()); 35 | this.disposed = true; 36 | } 37 | 38 | }; -------------------------------------------------------------------------------- /src/loading2/OctreeGeometryNode.ts: -------------------------------------------------------------------------------- 1 | import { IPointCloudTreeNode } from './../types'; 2 | 3 | // import * as THREE from "../../../../libs/three.js/build/three.module.js"; 4 | import { Box3,Sphere } from "three"; 5 | import { OctreeGeometry } from './OctreeGeometry'; 6 | 7 | export class OctreeGeometryNode implements IPointCloudTreeNode{ 8 | 9 | constructor(public name:string, public octreeGeometry:OctreeGeometry, public boundingBox:Box3){ 10 | this.id = OctreeGeometryNode.IDCount++; 11 | this.index = parseInt(name.charAt(name.length - 1)); 12 | this.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); 13 | this.numPoints = 0; 14 | this.oneTimeDisposeHandlers = []; 15 | } 16 | 17 | loaded: boolean = false; 18 | loading: boolean = false; 19 | parent: OctreeGeometryNode | null = null; 20 | geometry: THREE.BufferGeometry | null = null; 21 | nodeType?: number; 22 | byteOffset?: bigint ; 23 | byteSize?: bigint; 24 | hierarchyByteOffset?: bigint; 25 | hierarchyByteSize?: bigint; 26 | hasChildren: boolean = false; 27 | spacing!: number; 28 | density?: number; 29 | isLeafNode: boolean = true; 30 | readonly isTreeNode: boolean = false; 31 | readonly isGeometryNode: boolean = true; 32 | readonly children: ReadonlyArray = [ 33 | null, 34 | null, 35 | null, 36 | null, 37 | null, 38 | null, 39 | null, 40 | null, 41 | ]; 42 | 43 | // create static IDCount variable 44 | static IDCount = 0; 45 | id: number; 46 | index: number; 47 | boundingSphere: Sphere; 48 | numPoints: number; 49 | level!: number; 50 | oneTimeDisposeHandlers: Function[]; 51 | 52 | // isGeometryNode(){ 53 | // return true; 54 | // } 55 | 56 | getLevel(){ 57 | return this.level; 58 | } 59 | 60 | // isTreeNode(){ 61 | // return false; 62 | // } // Converted to property 63 | 64 | isLoaded(){ 65 | return this.loaded; 66 | } 67 | 68 | getBoundingSphere(){ 69 | return this.boundingSphere; 70 | } 71 | 72 | // getChildren(){ 73 | // let children = []; 74 | 75 | // for (let i = 0; i < 8; i++) { 76 | // if (this.children[i]) { 77 | // children.push(this.children[i]); 78 | // } 79 | // } 80 | 81 | // return children; 82 | // } 83 | 84 | getBoundingBox(){ 85 | return this.boundingBox; 86 | } 87 | 88 | load(){ 89 | 90 | if (this.octreeGeometry.numNodesLoading >= this.octreeGeometry.maxNumNodesLoading) { 91 | return; 92 | } 93 | 94 | if (this.octreeGeometry.loader) { 95 | this.octreeGeometry.loader.load(this); 96 | } 97 | } 98 | 99 | getNumPoints(){ 100 | return this.numPoints; 101 | } 102 | 103 | dispose(): void{ 104 | if (this.geometry && this.parent != null) { 105 | this.geometry.dispose(); 106 | this.geometry = null; 107 | this.loaded = false; 108 | 109 | // this.dispatchEvent( { type: 'dispose' } ); 110 | for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) { 111 | let handler = this.oneTimeDisposeHandlers[i]; 112 | handler(); 113 | } 114 | this.oneTimeDisposeHandlers = []; 115 | } 116 | } 117 | 118 | traverse(cb: (node: OctreeGeometryNode) => void, includeSelf = true): void { 119 | const stack: OctreeGeometryNode[] = includeSelf ? [this] : []; 120 | 121 | let current: OctreeGeometryNode | undefined; 122 | 123 | while ((current = stack.pop()) !== undefined) { 124 | cb(current); 125 | 126 | for (const child of current.children) { 127 | if (child !== null) { 128 | stack.push(child); 129 | } 130 | } 131 | } 132 | } 133 | 134 | 135 | 136 | }; 137 | 138 | OctreeGeometryNode.IDCount = 0; -------------------------------------------------------------------------------- /src/loading2/OctreeLoader.ts: -------------------------------------------------------------------------------- 1 | import { XhrRequest } from './../loading/types'; 2 | import { BufferAttribute, BufferGeometry, Vector3 } from "three"; 3 | import {PointAttribute, PointAttributes, PointAttributeTypes} from "./PointAttributes"; 4 | import { Box3, Sphere } from "three"; 5 | import { WorkerPool, WorkerType } from "./WorkerPool"; 6 | import { OctreeGeometryNode } from './OctreeGeometryNode'; 7 | import { OctreeGeometry } from './OctreeGeometry'; 8 | 9 | export class NodeLoader{ 10 | 11 | attributes?: PointAttributes; 12 | scale?: [number, number, number]; 13 | offset?: [number, number, number]; 14 | 15 | 16 | constructor(public url:string, public workerPool:WorkerPool, public metadata: Metadata){ 17 | } 18 | 19 | async load(node: OctreeGeometryNode){ 20 | 21 | if(node.loaded || node.loading){ 22 | return; 23 | } 24 | 25 | node.loading = true; 26 | // TODO: Need to put the numNodesLoading to the pco 27 | node.octreeGeometry.numNodesLoading++; 28 | 29 | try{ 30 | if(node.nodeType === 2){ // TODO: Investigate 31 | await this.loadHierarchy(node); 32 | } 33 | 34 | let {byteOffset, byteSize} = node; 35 | 36 | if (byteOffset === undefined || byteSize === undefined) { 37 | throw new Error("byteOffset and byteSize are required"); 38 | } 39 | 40 | let urlOctree = `${this.url}/../octree.bin`; 41 | 42 | let first = byteOffset; 43 | let last = byteOffset + byteSize - BigInt(1); 44 | 45 | let buffer; 46 | 47 | if(byteSize === BigInt(0)){ 48 | buffer = new ArrayBuffer(0); 49 | console.warn(`loaded node with 0 bytes: ${node.name}`); 50 | }else{ 51 | let response = await fetch(urlOctree, { 52 | headers: { 53 | 'content-type': 'multipart/byteranges', 54 | 'Range': `bytes=${first}-${last}`, 55 | }, 56 | }); 57 | 58 | buffer = await response.arrayBuffer(); 59 | } 60 | 61 | const workerType = (this.metadata.encoding === "BROTLI") ? WorkerType.DECODER_WORKER_BROTLI : WorkerType.DECODER_WORKER; 62 | const worker = this.workerPool.getWorker(workerType) 63 | 64 | worker.onmessage = (e) => { 65 | 66 | let data = e.data; 67 | let buffers = data.attributeBuffers; 68 | 69 | this.workerPool.returnWorker(workerType, worker); 70 | 71 | let geometry = new BufferGeometry(); 72 | 73 | for(let property in buffers){ 74 | 75 | let buffer = buffers[property].buffer; 76 | 77 | if(property === "position"){ 78 | geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); 79 | }else if(property === "rgba"){ 80 | geometry.setAttribute('rgba', new BufferAttribute(new Uint8Array(buffer), 4, true)); 81 | }else if(property === "NORMAL"){ 82 | //geometry.setAttribute('rgba', new BufferAttribute(new Uint8Array(buffer), 4, true)); 83 | geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); 84 | }else if (property === "INDICES") { 85 | let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4); 86 | bufferAttribute.normalized = true; 87 | geometry.setAttribute('indices', bufferAttribute); 88 | }else{ 89 | const bufferAttribute: BufferAttribute & { 90 | potree?: object 91 | } = new BufferAttribute(new Float32Array(buffer), 1); 92 | 93 | let batchAttribute = buffers[property].attribute; 94 | bufferAttribute.potree = { 95 | offset: buffers[property].offset, 96 | scale: buffers[property].scale, 97 | preciseBuffer: buffers[property].preciseBuffer, 98 | range: batchAttribute.range, 99 | }; 100 | 101 | geometry.setAttribute(property, bufferAttribute); 102 | } 103 | 104 | } 105 | // indices ?? 106 | 107 | node.density = data.density; 108 | node.geometry = geometry; 109 | node.loaded = true; 110 | node.loading = false; 111 | // Potree.numNodesLoading--; 112 | node.octreeGeometry.numNodesLoading--; 113 | }; 114 | 115 | let pointAttributes = node.octreeGeometry.pointAttributes; 116 | let scale = node.octreeGeometry.scale; 117 | 118 | let box = node.boundingBox; 119 | let min = node.octreeGeometry.offset.clone().add(box.min); 120 | let size = box.max.clone().sub(box.min); 121 | let max = min.clone().add(size); 122 | let numPoints = node.numPoints; 123 | 124 | let offset = node.octreeGeometry.loader.offset; 125 | 126 | let message = { 127 | name: node.name, 128 | buffer: buffer, 129 | pointAttributes: pointAttributes, 130 | scale: scale, 131 | min: min, 132 | max: max, 133 | size: size, 134 | offset: offset, 135 | numPoints: numPoints 136 | }; 137 | 138 | worker.postMessage(message, [message.buffer]); 139 | }catch(e){ 140 | node.loaded = false; 141 | node.loading = false; 142 | node.octreeGeometry.numNodesLoading--; 143 | 144 | // console.log(`failed to load ${node.name}`); 145 | // console.log(e); 146 | // console.log(`trying again!`); 147 | } 148 | } 149 | 150 | parseHierarchy(node:OctreeGeometryNode, buffer:ArrayBuffer){ 151 | 152 | let view = new DataView(buffer); 153 | 154 | let bytesPerNode = 22; 155 | let numNodes = buffer.byteLength / bytesPerNode; 156 | 157 | let octree = node.octreeGeometry; 158 | // let nodes = [node]; 159 | let nodes: OctreeGeometryNode[] = new Array(numNodes); 160 | nodes[0] = node; 161 | let nodePos = 1; 162 | 163 | for(let i = 0; i < numNodes; i++){ 164 | let current = nodes[i]; 165 | 166 | let type = view.getUint8(i * bytesPerNode + 0); 167 | let childMask = view.getUint8(i * bytesPerNode + 1); 168 | let numPoints = view.getUint32(i * bytesPerNode + 2, true); 169 | let byteOffset = view.getBigInt64(i * bytesPerNode + 6, true); 170 | let byteSize = view.getBigInt64(i * bytesPerNode + 14, true); 171 | 172 | // if(byteSize === 0n){ 173 | // // debugger; 174 | // } 175 | 176 | 177 | if(current.nodeType === 2){ 178 | // replace proxy with real node 179 | current.byteOffset = byteOffset; 180 | current.byteSize = byteSize; 181 | current.numPoints = numPoints; 182 | }else if(type === 2){ 183 | // load proxy 184 | current.hierarchyByteOffset = byteOffset; 185 | current.hierarchyByteSize = byteSize; 186 | current.numPoints = numPoints; 187 | }else{ 188 | // load real node 189 | current.byteOffset = byteOffset; 190 | current.byteSize = byteSize; 191 | current.numPoints = numPoints; 192 | } 193 | 194 | current.nodeType = type; 195 | 196 | if(current.nodeType === 2){ 197 | continue; 198 | } 199 | 200 | for(let childIndex = 0; childIndex < 8; childIndex++){ 201 | let childExists = ((1 << childIndex) & childMask) !== 0; 202 | 203 | if(!childExists){ 204 | continue; 205 | } 206 | 207 | let childName = current.name + childIndex; 208 | 209 | let childAABB = createChildAABB(current.boundingBox, childIndex); 210 | let child = new OctreeGeometryNode(childName, octree, childAABB); 211 | child.name = childName; 212 | child.spacing = current.spacing / 2; 213 | child.level = current.level + 1; 214 | 215 | (current.children as any)[childIndex] = child; 216 | child.parent = current; 217 | 218 | // nodes.push(child); 219 | nodes[nodePos] = child; 220 | nodePos++; 221 | } 222 | 223 | // if((i % 500) === 0){ 224 | // yield; 225 | // } 226 | } 227 | 228 | // if(duration > 20){ 229 | // let msg = `duration: ${duration}ms, numNodes: ${numNodes}`; 230 | // console.log(msg); 231 | // } 232 | } 233 | 234 | async loadHierarchy(node: OctreeGeometryNode){ 235 | 236 | let {hierarchyByteOffset, hierarchyByteSize} = node; 237 | 238 | if (hierarchyByteOffset === undefined || hierarchyByteSize === undefined) { 239 | throw new Error(`hierarchyByteOffset and hierarchyByteSize are undefined for node ${node.name}`); 240 | } 241 | 242 | let hierarchyPath = `${this.url}/../hierarchy.bin`; 243 | 244 | let first = hierarchyByteOffset; 245 | let last = first + hierarchyByteSize - BigInt(1); 246 | 247 | let response = await fetch(hierarchyPath, { 248 | headers: { 249 | 'content-type': 'multipart/byteranges', 250 | 'Range': `bytes=${first}-${last}`, 251 | }, 252 | }); 253 | 254 | let buffer = await response.arrayBuffer(); 255 | 256 | this.parseHierarchy(node, buffer); 257 | } 258 | 259 | } 260 | 261 | let tmpVec3 = new Vector3(); 262 | function createChildAABB(aabb:Box3, index:number){ 263 | let min = aabb.min.clone(); 264 | let max = aabb.max.clone(); 265 | let size = tmpVec3.subVectors(max, min); 266 | 267 | if ((index & 0b0001) > 0) { 268 | min.z += size.z / 2; 269 | } else { 270 | max.z -= size.z / 2; 271 | } 272 | 273 | if ((index & 0b0010) > 0) { 274 | min.y += size.y / 2; 275 | } else { 276 | max.y -= size.y / 2; 277 | } 278 | 279 | if ((index & 0b0100) > 0) { 280 | min.x += size.x / 2; 281 | } else { 282 | max.x -= size.x / 2; 283 | } 284 | 285 | return new Box3(min, max); 286 | } 287 | 288 | let typenameTypeattributeMap = { 289 | "double": PointAttributeTypes.DATA_TYPE_DOUBLE, 290 | "float": PointAttributeTypes.DATA_TYPE_FLOAT, 291 | "int8": PointAttributeTypes.DATA_TYPE_INT8, 292 | "uint8": PointAttributeTypes.DATA_TYPE_UINT8, 293 | "int16": PointAttributeTypes.DATA_TYPE_INT16, 294 | "uint16": PointAttributeTypes.DATA_TYPE_UINT16, 295 | "int32": PointAttributeTypes.DATA_TYPE_INT32, 296 | "uint32": PointAttributeTypes.DATA_TYPE_UINT32, 297 | "int64": PointAttributeTypes.DATA_TYPE_INT64, 298 | "uint64": PointAttributeTypes.DATA_TYPE_UINT64, 299 | } 300 | 301 | type AttributeType = keyof typeof typenameTypeattributeMap; 302 | 303 | export interface Attribute { 304 | name: string; 305 | description: string; 306 | size: number; 307 | numElements: number; 308 | type: AttributeType; 309 | min: number[]; 310 | max: number[]; 311 | } 312 | 313 | export interface Metadata { 314 | version: string; 315 | name: string; 316 | description: string; 317 | points: number; 318 | projection: string; 319 | hierarchy: { 320 | firstChunkSize: number; 321 | stepSize: number; 322 | depth: number; 323 | }, 324 | offset: [number, number, number], 325 | scale: [number, number, number], 326 | spacing: number, 327 | boundingBox: { 328 | min: [number, number, number], 329 | max: [number, number, number], 330 | }, 331 | encoding: string; 332 | attributes: Attribute[]; 333 | } 334 | 335 | export class OctreeLoader{ 336 | 337 | workerPool: WorkerPool = new WorkerPool(); 338 | 339 | constructor() { 340 | } 341 | 342 | static parseAttributes(jsonAttributes:Attribute[]){ 343 | 344 | let attributes = new PointAttributes(); 345 | 346 | // Replacements object for string to string 347 | let replacements: {[key: string]: string} = { 348 | "rgb": "rgba", 349 | }; 350 | 351 | for (const jsonAttribute of jsonAttributes) { 352 | let {name, numElements, min, max} = jsonAttribute; 353 | 354 | let type = typenameTypeattributeMap[jsonAttribute.type]; // Fix the typing, currently jsonAttribute has type "never" 355 | 356 | let potreeAttributeName = replacements[name] ? replacements[name] : name; 357 | 358 | let attribute = new PointAttribute(potreeAttributeName, type, numElements); 359 | 360 | if(numElements === 1){ 361 | attribute.range = [min[0], max[0]]; 362 | }else{ 363 | attribute.range = [min, max]; 364 | } 365 | 366 | if (name === "gps-time") { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909 367 | if (typeof attribute.range[0] === "number" && attribute.range[0] === attribute.range[1]) { 368 | attribute.range[1] += 1; 369 | } 370 | } 371 | 372 | attribute.initialRange = attribute.range; 373 | 374 | attributes.add(attribute); 375 | } 376 | 377 | { 378 | // check if it has normals 379 | let hasNormals = 380 | attributes.attributes.find(a => a.name === "NormalX") !== undefined && 381 | attributes.attributes.find(a => a.name === "NormalY") !== undefined && 382 | attributes.attributes.find(a => a.name === "NormalZ") !== undefined; 383 | 384 | if(hasNormals){ 385 | let vector = { 386 | name: "NORMAL", 387 | attributes: ["NormalX", "NormalY", "NormalZ"], 388 | }; 389 | attributes.addVector(vector); 390 | } 391 | } 392 | 393 | return attributes; 394 | } 395 | 396 | async load(url:string, xhrRequest: XhrRequest){ // Previously a static method 397 | 398 | let response = await xhrRequest(url); 399 | let metadata: Metadata = await response.json(); 400 | 401 | let attributes = OctreeLoader.parseAttributes(metadata.attributes); 402 | // console.log(attributes) 403 | 404 | let loader = new NodeLoader(url, this.workerPool, metadata); 405 | loader.attributes = attributes; 406 | loader.scale = metadata.scale; 407 | loader.offset = metadata.offset; 408 | 409 | let octree = new OctreeGeometry(loader, new Box3(new Vector3(...metadata.boundingBox.min), new Vector3(...metadata.boundingBox.max))); 410 | octree.url = url; 411 | octree.spacing = metadata.spacing; 412 | octree.scale = metadata.scale; 413 | 414 | let min = new Vector3(...metadata.boundingBox.min); 415 | let max = new Vector3(...metadata.boundingBox.max); 416 | let boundingBox = new Box3(min, max); 417 | 418 | let offset = min.clone(); 419 | boundingBox.min.sub(offset); 420 | boundingBox.max.sub(offset); 421 | 422 | octree.projection = metadata.projection; 423 | octree.boundingBox = boundingBox; 424 | octree.tightBoundingBox = boundingBox.clone(); 425 | octree.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); 426 | octree.tightBoundingSphere = boundingBox.getBoundingSphere(new Sphere()); 427 | octree.offset = offset; 428 | octree.pointAttributes = OctreeLoader.parseAttributes(metadata.attributes); 429 | 430 | let root = new OctreeGeometryNode("r", octree, boundingBox); 431 | root.level = 0; 432 | root.nodeType = 2; 433 | root.hierarchyByteOffset = BigInt(0); 434 | root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize); 435 | root.spacing = octree.spacing; 436 | root.byteOffset = BigInt(0); // Originally 0 437 | 438 | octree.root = root; 439 | 440 | loader.load(root); 441 | 442 | let result = { 443 | geometry: octree, 444 | }; 445 | 446 | return result; 447 | 448 | } 449 | 450 | }; -------------------------------------------------------------------------------- /src/loading2/PointAttributes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Some types of possible point attribute data formats 3 | * 4 | * @class 5 | */ 6 | const PointAttributeTypes:PointAttributeTypesType = { 7 | DATA_TYPE_DOUBLE: {ordinal: 0, name: "double", size: 8}, 8 | DATA_TYPE_FLOAT: {ordinal: 1, name: "float", size: 4}, 9 | DATA_TYPE_INT8: {ordinal: 2, name: "int8", size: 1}, 10 | DATA_TYPE_UINT8: {ordinal: 3, name: "uint8", size: 1}, 11 | DATA_TYPE_INT16: {ordinal: 4, name: "int16", size: 2}, 12 | DATA_TYPE_UINT16: {ordinal: 5, name: "uint16", size: 2}, 13 | DATA_TYPE_INT32: {ordinal: 6, name: "int32", size: 4}, 14 | DATA_TYPE_UINT32: {ordinal: 7, name: "uint32", size: 4}, 15 | DATA_TYPE_INT64: {ordinal: 8, name: "int64", size: 8}, 16 | DATA_TYPE_UINT64: {ordinal: 9, name: "uint64", size: 8} 17 | }; 18 | 19 | type PointAttributeTypesType = { 20 | [key: string]: PointAttributeTypeType; 21 | }; 22 | 23 | type PointAttributeTypeType = { 24 | ordinal: number; 25 | name: string; 26 | size: number; 27 | }; 28 | 29 | let i = 0; 30 | for (let obj in PointAttributeTypes) { 31 | PointAttributeTypes[i] = PointAttributeTypes[obj]; 32 | i++; 33 | } 34 | 35 | export {PointAttributeTypes}; 36 | 37 | type RangeType = number[] | [number[], number[]] 38 | 39 | // Class that represents a certain point attribute 40 | class PointAttribute{ 41 | byteSize: number; 42 | description: string; 43 | public initialRange?: RangeType 44 | 45 | constructor( 46 | public name:string, 47 | public type:PointAttributeTypeType, 48 | public numElements:number, 49 | public range: RangeType = [Infinity, -Infinity] 50 | ){ 51 | this.byteSize = this.numElements * this.type.size; 52 | this.description = ""; 53 | } 54 | }; 55 | 56 | export {PointAttribute}; 57 | 58 | // Map that represents all point attributes, these were previoiusly properties of the PointAttribute class 59 | export const POINT_ATTRIBUTES:{[key:string]:PointAttribute} = { 60 | POSITION_CARTESIAN: new PointAttribute("POSITION_CARTESIAN", PointAttributeTypes.DATA_TYPE_FLOAT, 3), 61 | RGBA_PACKED: new PointAttribute("COLOR_PACKED", PointAttributeTypes.DATA_TYPE_INT8, 4), 62 | COLOR_PACKED: new PointAttribute("COLOR_PACKED", PointAttributeTypes.DATA_TYPE_INT8, 4), 63 | RGB_PACKED: new PointAttribute("COLOR_PACKED", PointAttributeTypes.DATA_TYPE_INT8, 3), 64 | NORMAL_FLOATS: new PointAttribute("NORMAL_FLOATS", PointAttributeTypes.DATA_TYPE_FLOAT, 3), 65 | INTENSITY: new PointAttribute("INTENSITY", PointAttributeTypes.DATA_TYPE_UINT16, 1), 66 | CLASSIFICATION: new PointAttribute("CLASSIFICATION", PointAttributeTypes.DATA_TYPE_UINT8, 1), 67 | NORMAL_SPHEREMAPPED: new PointAttribute("NORMAL_SPHEREMAPPED", PointAttributeTypes.DATA_TYPE_UINT8, 2), 68 | NORMAL_OCT16: new PointAttribute("NORMAL_OCT16", PointAttributeTypes.DATA_TYPE_UINT8, 2), 69 | NORMAL: new PointAttribute("NORMAL", PointAttributeTypes.DATA_TYPE_FLOAT, 3), 70 | RETURN_NUMBER: new PointAttribute("RETURN_NUMBER", PointAttributeTypes.DATA_TYPE_UINT8, 1), 71 | NUMBER_OF_RETURNS: new PointAttribute("NUMBER_OF_RETURNS", PointAttributeTypes.DATA_TYPE_UINT8, 1), 72 | SOURCE_ID: new PointAttribute("SOURCE_ID", PointAttributeTypes.DATA_TYPE_UINT16, 1), 73 | INDICES: new PointAttribute("INDICES", PointAttributeTypes.DATA_TYPE_UINT32, 1), 74 | SPACING: new PointAttribute("SPACING", PointAttributeTypes.DATA_TYPE_FLOAT, 1), 75 | GPS_TIME: new PointAttribute("GPS_TIME", PointAttributeTypes.DATA_TYPE_DOUBLE, 1) 76 | } 77 | 78 | type PAVectorType = { 79 | name: string; 80 | attributes: string[]; 81 | } 82 | 83 | // Instantiated during loading 84 | export class PointAttributes{ 85 | 86 | 87 | 88 | // pointAttributes will be a list of strings 89 | constructor(pointAttributes?:string[], 90 | public attributes:PointAttribute[] = [], 91 | public byteSize:number = 0, 92 | public size:number = 0, 93 | public vectors:PAVectorType[]=[] 94 | ){ 95 | 96 | if (pointAttributes != null) { 97 | for (let i = 0; i < pointAttributes.length; i++) { 98 | let pointAttributeName = pointAttributes[i]; 99 | let pointAttribute = POINT_ATTRIBUTES[pointAttributeName]; 100 | this.attributes.push(pointAttribute); 101 | this.byteSize += pointAttribute.byteSize; 102 | this.size++; 103 | } 104 | } 105 | } 106 | 107 | // I hate these argument names that are so similar to each other but have completely different types 108 | add(pointAttribute:PointAttribute){ 109 | this.attributes.push(pointAttribute); 110 | this.byteSize += pointAttribute.byteSize; 111 | this.size++; 112 | }; 113 | 114 | addVector(vector:PAVectorType){ 115 | this.vectors.push(vector); 116 | } 117 | 118 | hasNormals(){ 119 | for (let name in this.attributes) { 120 | let pointAttribute = this.attributes[name]; 121 | if ( 122 | pointAttribute === POINT_ATTRIBUTES.NORMAL_SPHEREMAPPED || 123 | pointAttribute === POINT_ATTRIBUTES.NORMAL_FLOATS || 124 | pointAttribute === POINT_ATTRIBUTES.NORMAL || 125 | pointAttribute === POINT_ATTRIBUTES.NORMAL_OCT16) { 126 | return true; 127 | } 128 | } 129 | 130 | return false; 131 | }; 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/loading2/WorkerPool.ts: -------------------------------------------------------------------------------- 1 | import BrotliDecoderWorker from "./brotli-decoder.worker.js?worker&inline" 2 | import DecoderWorker from "./decoder.worker.js?worker&inline" 3 | 4 | // Create enums for different types of workers 5 | export enum WorkerType { 6 | DECODER_WORKER_BROTLI = "DECODER_WORKER_BROTLI", 7 | DECODER_WORKER = "DECODER_WORKER", 8 | } 9 | 10 | // Worker JS names: "BinaryDecoderWorker.js", "DEMWorker.js", "EptBinaryDecoderWorker.js", "EptLaszipDecoderWorker.js", 11 | // EptZstandardDecoder_preamble.js", "EptZstandardDecoderWorker.js", "LASDecoderWorker.js", "LASLAZWorker.js", "LazLoaderWorker.js" 12 | 13 | function createWorker(type: WorkerType): Worker { 14 | // console.log(type) 15 | switch (type) { 16 | case WorkerType.DECODER_WORKER_BROTLI: { 17 | // const worker = require("./brotli-decoder.worker.js"); 18 | // return new worker(); 19 | // return new Worker( 20 | // new URL('./brotli-decoder.worker.js', import.meta.url), 21 | // { type: 'module' }, 22 | // ) 23 | return new BrotliDecoderWorker() 24 | } 25 | case WorkerType.DECODER_WORKER: { 26 | // let ctor = require("./decoder.worker.js"); 27 | // return new ctor(); 28 | // return new Worker( 29 | // new URL('./decoder.worker.js', import.meta.url), 30 | // { type: 'module' }, 31 | // ) 32 | return new DecoderWorker() 33 | } 34 | default: 35 | throw new Error("Unknown worker type"); 36 | } 37 | } 38 | 39 | 40 | export class WorkerPool{ 41 | // Workers will be an object that has a key for each worker type and the value is an array of Workers that can be empty 42 | private workers: { [key in WorkerType]: Worker[] } = {DECODER_WORKER: [], DECODER_WORKER_BROTLI: []}; 43 | 44 | getWorker(workerType: WorkerType): Worker{ 45 | // Throw error if workerType is not recognized 46 | if (this.workers[workerType] === undefined) { 47 | throw new Error("Unknown worker type"); 48 | } 49 | // Given a worker URL, if URL does not exist in the worker object, create a new array with the URL as a key 50 | if (this.workers[workerType].length === 0){ 51 | let worker = createWorker(workerType); 52 | this.workers[workerType].push(worker); 53 | } 54 | let worker = this.workers[workerType].pop(); 55 | if (worker === undefined) { // Typescript needs this 56 | throw new Error("No workers available"); 57 | } 58 | // Return the last worker in the array and remove it from the array 59 | return worker; 60 | } 61 | 62 | returnWorker(workerType: WorkerType, worker:Worker){ 63 | this.workers[workerType].push(worker); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/loading2/decoder.worker.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {PointAttribute, PointAttributeTypes} from "./PointAttributes"; 4 | 5 | const typedArrayMapping = { 6 | "int8": Int8Array, 7 | "int16": Int16Array, 8 | "int32": Int32Array, 9 | "int64": Float64Array, 10 | "uint8": Uint8Array, 11 | "uint16": Uint16Array, 12 | "uint32": Uint32Array, 13 | "uint64": Float64Array, 14 | "float": Float32Array, 15 | "double": Float64Array, 16 | }; 17 | 18 | // Potree = {}; 19 | 20 | onmessage = function (event) { 21 | 22 | let {buffer, pointAttributes, scale, name, min, max, size, offset, numPoints} = event.data; 23 | 24 | let tStart = performance.now(); 25 | 26 | let view = new DataView(buffer); 27 | 28 | let attributeBuffers = {}; 29 | let attributeOffset = 0; 30 | 31 | let bytesPerPoint = 0; 32 | for (let pointAttribute of pointAttributes.attributes) { 33 | bytesPerPoint += pointAttribute.byteSize; 34 | } 35 | 36 | let gridSize = 32; 37 | let grid = new Uint32Array(gridSize ** 3); 38 | let toIndex = (x, y, z) => { 39 | // let dx = gridSize * (x - min.x) / size.x; 40 | // let dy = gridSize * (y - min.y) / size.y; 41 | // let dz = gridSize * (z - min.z) / size.z; 42 | 43 | // min is already subtracted 44 | let dx = gridSize * x / size.x; 45 | let dy = gridSize * y / size.y; 46 | let dz = gridSize * z / size.z; 47 | 48 | let ix = Math.min(parseInt(dx), gridSize - 1); 49 | let iy = Math.min(parseInt(dy), gridSize - 1); 50 | let iz = Math.min(parseInt(dz), gridSize - 1); 51 | 52 | let index = ix + iy * gridSize + iz * gridSize * gridSize; 53 | 54 | return index; 55 | }; 56 | 57 | let numOccupiedCells = 0; 58 | for (let pointAttribute of pointAttributes.attributes) { 59 | 60 | if(["POSITION_CARTESIAN", "position"].includes(pointAttribute.name)){ 61 | let buff = new ArrayBuffer(numPoints * 4 * 3); 62 | let positions = new Float32Array(buff); 63 | 64 | for (let j = 0; j < numPoints; j++) { 65 | 66 | let pointOffset = j * bytesPerPoint; 67 | 68 | let x = (view.getInt32(pointOffset + attributeOffset + 0, true) * scale[0]) + offset[0] - min.x; 69 | let y = (view.getInt32(pointOffset + attributeOffset + 4, true) * scale[1]) + offset[1] - min.y; 70 | let z = (view.getInt32(pointOffset + attributeOffset + 8, true) * scale[2]) + offset[2] - min.z; 71 | 72 | let index = toIndex(x, y, z); 73 | let count = grid[index]++; 74 | if(count === 0){ 75 | numOccupiedCells++; 76 | } 77 | 78 | positions[3 * j + 0] = x; 79 | positions[3 * j + 1] = y; 80 | positions[3 * j + 2] = z; 81 | } 82 | 83 | attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; 84 | }else if(["RGBA", "rgba"].includes(pointAttribute.name)){ 85 | let buff = new ArrayBuffer(numPoints * 4); 86 | let colors = new Uint8Array(buff); 87 | 88 | for (let j = 0; j < numPoints; j++) { 89 | let pointOffset = j * bytesPerPoint; 90 | 91 | let r = view.getUint16(pointOffset + attributeOffset + 0, true); 92 | let g = view.getUint16(pointOffset + attributeOffset + 2, true); 93 | let b = view.getUint16(pointOffset + attributeOffset + 4, true); 94 | 95 | colors[4 * j + 0] = r > 255 ? r / 256 : r; 96 | colors[4 * j + 1] = g > 255 ? g / 256 : g; 97 | colors[4 * j + 2] = b > 255 ? b / 256 : b; 98 | } 99 | 100 | attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; 101 | }else{ 102 | let buff = new ArrayBuffer(numPoints * 4); 103 | let f32 = new Float32Array(buff); 104 | 105 | let TypedArray = typedArrayMapping[pointAttribute.type.name]; 106 | let preciseBuffer = new TypedArray(numPoints); 107 | 108 | let [offset, scale] = [0, 1]; 109 | 110 | const getterMap = { 111 | "int8": view.getInt8, 112 | "int16": view.getInt16, 113 | "int32": view.getInt32, 114 | // "int64": view.getInt64, 115 | "uint8": view.getUint8, 116 | "uint16": view.getUint16, 117 | "uint32": view.getUint32, 118 | // "uint64": view.getUint64, 119 | "float": view.getFloat32, 120 | "double": view.getFloat64, 121 | }; 122 | const getter = getterMap[pointAttribute.type.name].bind(view); 123 | 124 | // compute offset and scale to pack larger types into 32 bit floats 125 | if(pointAttribute.type.size > 4){ 126 | let [amin, amax] = pointAttribute.range; 127 | offset = amin; 128 | scale = 1 / (amax - amin); 129 | } 130 | 131 | for(let j = 0; j < numPoints; j++){ 132 | let pointOffset = j * bytesPerPoint; 133 | let value = getter(pointOffset + attributeOffset, true); 134 | 135 | f32[j] = (value - offset) * scale; 136 | preciseBuffer[j] = value; 137 | } 138 | 139 | attributeBuffers[pointAttribute.name] = { 140 | buffer: buff, 141 | preciseBuffer: preciseBuffer, 142 | attribute: pointAttribute, 143 | offset: offset, 144 | scale: scale, 145 | }; 146 | } 147 | 148 | attributeOffset += pointAttribute.byteSize; 149 | 150 | 151 | } 152 | 153 | let occupancy = parseInt(numPoints / numOccupiedCells); 154 | // console.log(`${name}: #points: ${numPoints}: #occupiedCells: ${numOccupiedCells}, occupancy: ${occupancy} points/cell`); 155 | 156 | { // add indices 157 | let buff = new ArrayBuffer(numPoints * 4); 158 | let indices = new Uint32Array(buff); 159 | 160 | for (let i = 0; i < numPoints; i++) { 161 | indices[i] = i; 162 | } 163 | 164 | attributeBuffers["INDICES"] = { buffer: buff, attribute: PointAttribute.INDICES }; 165 | } 166 | 167 | 168 | { // handle attribute vectors 169 | let vectors = pointAttributes.vectors; 170 | 171 | for(let vector of vectors){ 172 | 173 | let {name, attributes} = vector; 174 | let numVectorElements = attributes.length; 175 | let buffer = new ArrayBuffer(numVectorElements * numPoints * 4); 176 | let f32 = new Float32Array(buffer); 177 | 178 | let iElement = 0; 179 | for(let sourceName of attributes){ 180 | let sourceBuffer = attributeBuffers[sourceName]; 181 | let {offset, scale} = sourceBuffer; 182 | let view = new DataView(sourceBuffer.buffer); 183 | 184 | const getter = view.getFloat32.bind(view); 185 | 186 | for(let j = 0; j < numPoints; j++){ 187 | let value = getter(j * 4, true); 188 | 189 | f32[j * numVectorElements + iElement] = (value / scale) + offset; 190 | } 191 | 192 | iElement++; 193 | } 194 | 195 | let vecAttribute = new PointAttribute(name, PointAttributeTypes.DATA_TYPE_FLOAT, 3); 196 | 197 | attributeBuffers[name] = { 198 | buffer: buffer, 199 | attribute: vecAttribute, 200 | }; 201 | 202 | } 203 | 204 | } 205 | 206 | // let duration = performance.now() - tStart; 207 | // let pointsPerMs = numPoints / duration; 208 | // console.log(`duration: ${duration.toFixed(1)}ms, #points: ${numPoints}, points/ms: ${pointsPerMs.toFixed(1)}`); 209 | 210 | let message = { 211 | buffer: buffer, 212 | attributeBuffers: attributeBuffers, 213 | density: occupancy, 214 | }; 215 | 216 | let transferables = []; 217 | for (let property in message.attributeBuffers) { 218 | transferables.push(message.attributeBuffers[property].buffer); 219 | } 220 | transferables.push(buffer); 221 | // console.log("new", message) 222 | 223 | postMessage(message, transferables); 224 | }; 225 | -------------------------------------------------------------------------------- /src/loading2/libs/brotli/BUILD: -------------------------------------------------------------------------------- 1 | package( 2 | default_visibility = ["//visibility:public"], 3 | ) 4 | 5 | licenses(["notice"]) # MIT 6 | 7 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library") 8 | 9 | # Not a real polyfill. Do NOT use for anything, but tests. 10 | closure_js_library( 11 | name = "polyfill", 12 | srcs = ["polyfill.js"], 13 | suppress = [ 14 | "JSC_INVALID_OPERAND_TYPE", 15 | "JSC_MISSING_JSDOC", 16 | "JSC_STRICT_INEXISTENT_PROPERTY", 17 | "JSC_TYPE_MISMATCH", 18 | "JSC_UNKNOWN_EXPR_TYPE", 19 | ], 20 | ) 21 | 22 | # Do NOT use this artifact; it is for test purposes only. 23 | closure_js_library( 24 | name = "decode", 25 | srcs = ["decode.js"], 26 | suppress = [ 27 | "JSC_DUP_VAR_DECLARATION", 28 | "JSC_USELESS_BLOCK", 29 | ], 30 | deps = [":polyfill"], 31 | ) 32 | 33 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_test") 34 | 35 | closure_js_test( 36 | name = "all_tests", 37 | srcs = ["decode_test.js"], 38 | deps = [ 39 | ":decode", 40 | ":polyfill", 41 | "@io_bazel_rules_closure//closure/library:testing", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /src/loading2/libs/brotli/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/loading2/libs/brotli/WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "org_brotli_js") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") 4 | 5 | git_repository( 6 | name = "io_bazel_rules_closure", 7 | commit = "29ec97e7c85d607ba9e41cab3993fbb13f812c4b", 8 | remote = "https://github.com/bazelbuild/rules_closure.git", 9 | ) 10 | 11 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories") 12 | closure_repositories() 13 | -------------------------------------------------------------------------------- /src/loading2/libs/brotli/decode_test.js: -------------------------------------------------------------------------------- 1 | goog.require('goog.testing.asserts'); 2 | goog.require('goog.testing.jsunit'); 3 | 4 | /** 5 | * @param {!Int8Array} bytes 6 | * @return {string} 7 | */ 8 | function bytesToString(bytes) { 9 | return String.fromCharCode.apply(null, new Uint16Array(bytes)); 10 | } 11 | 12 | function testMetadata() { 13 | assertEquals("", bytesToString(BrotliDecode(Int8Array.from([1, 11, 0, 42, 3])))); 14 | } 15 | 16 | function testEmpty() { 17 | assertEquals("", bytesToString(BrotliDecode(Int8Array.from([6])))); 18 | assertEquals("", bytesToString(BrotliDecode(Int8Array.from([0x81, 1])))); 19 | } 20 | 21 | function testBaseDictWord() { 22 | var input = Int8Array.from([ 23 | 0x1b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe3, 0xb4, 0x0d, 0x00, 0x00, 24 | 0x07, 0x5b, 0x26, 0x31, 0x40, 0x02, 0x00, 0xe0, 0x4e, 0x1b, 0x41, 0x02 25 | ]); 26 | /** @type {!Int8Array} */ 27 | var output = BrotliDecode(input); 28 | assertEquals("time", bytesToString(output)); 29 | } 30 | 31 | function testBlockCountMessage() { 32 | var input = Int8Array.from([ 33 | 0x1b, 0x0b, 0x00, 0x11, 0x01, 0x8c, 0xc1, 0xc5, 0x0d, 0x08, 0x00, 0x22, 34 | 0x65, 0xe1, 0xfc, 0xfd, 0x22, 0x2c, 0xc4, 0x00, 0x00, 0x38, 0xd8, 0x32, 35 | 0x89, 0x01, 0x12, 0x00, 0x00, 0x77, 0xda, 0x04, 0x10, 0x42, 0x00, 0x00, 0x00 36 | ]); 37 | /** @type {!Int8Array} */ 38 | var output = BrotliDecode(input); 39 | assertEquals("aabbaaaaabab", bytesToString(output)); 40 | } 41 | 42 | function testCompressedUncompressedShortCompressedSmallWindow() { 43 | var input = Int8Array.from([ 44 | 0x21, 0xf4, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xa7, 0x6d, 0x00, 0x00, 45 | 0x38, 0xd8, 0x32, 0x89, 0x01, 0x12, 0x00, 0x00, 0x77, 0xda, 0x34, 0x7b, 46 | 0xdb, 0x50, 0x80, 0x02, 0x80, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x31, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x4e, 0xdb, 0x00, 0x00, 0x70, 0xb0, 48 | 0x65, 0x12, 0x03, 0x24, 0x00, 0x00, 0xee, 0xb4, 0x11, 0x24, 0x00 49 | ]); 50 | /** @type {!Int8Array} */ 51 | var output = BrotliDecode(input); 52 | assertEquals( 53 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 54 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 55 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 56 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 57 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 58 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 59 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 60 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 61 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 62 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 63 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 64 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 65 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 66 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 67 | "aaaaaaaaaaaaaabbbbbbbbbb", bytesToString(output)); 68 | } 69 | 70 | function testIntactDistanceRingBuffer0() { 71 | var input = Int8Array.from([ 72 | 0x1b, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe3, 0xb4, 0x0d, 0x00, 0x00, 73 | 0x07, 0x5b, 0x26, 0x31, 0x40, 0x02, 0x00, 0xe0, 0x4e, 0x1b, 0xa1, 0x80, 74 | 0x20, 0x00 75 | ]); 76 | /** @type {!Int8Array} */ 77 | var output = BrotliDecode(input); 78 | assertEquals("himselfself", bytesToString(output)); 79 | } 80 | -------------------------------------------------------------------------------- /src/loading2/libs/brotli/polyfill.js: -------------------------------------------------------------------------------- 1 | if (!Int32Array.__proto__.from) { 2 | Object.defineProperty(Int32Array.__proto__, 'from', { 3 | value: function(obj) { 4 | obj = Object(obj); 5 | if (!obj['length']) { 6 | return new this(0); 7 | } 8 | var typed_array = new this(obj.length); 9 | for(var i = 0; i < typed_array.length; i++) { 10 | typed_array[i] = obj[i]; 11 | } 12 | return typed_array; 13 | } 14 | }); 15 | } 16 | 17 | if (!Array.prototype.copyWithin) { 18 | Array.prototype.copyWithin = function(target, start, end) { 19 | var O = Object(this); 20 | var len = O.length >>> 0; 21 | var to = target | 0; 22 | var from = start | 0; 23 | var count = Math.min(Math.min(end | 0, len) - from, len - to); 24 | var direction = 1; 25 | if (from < to && to < (from + count)) { 26 | direction = -1; 27 | from += count - 1; 28 | to += count - 1; 29 | } 30 | while (count > 0) { 31 | O[to] = O[from]; 32 | from += direction; 33 | to += direction; 34 | count--; 35 | } 36 | return O; 37 | }; 38 | } 39 | 40 | if (!Array.prototype.fill) { 41 | Object.defineProperty(Array.prototype, 'fill', { 42 | value: function(value, start, end) { 43 | end = end | 0; 44 | var O = Object(this); 45 | var k = start | 0; 46 | while (k < end) { 47 | O[k] = value; 48 | k++; 49 | } 50 | return O; 51 | } 52 | }); 53 | } 54 | 55 | if (!Int8Array.prototype.copyWithin) { 56 | Int8Array.prototype.copyWithin = Array.prototype.copyWithin; 57 | } 58 | 59 | if (!Int8Array.prototype.fill) { 60 | Int8Array.prototype.fill = Array.prototype.fill; 61 | } 62 | 63 | if (!Int32Array.prototype.fill) { 64 | Int32Array.prototype.fill = Array.prototype.fill; 65 | } 66 | -------------------------------------------------------------------------------- /src/loading2/load-octree.ts: -------------------------------------------------------------------------------- 1 | import { OctreeLoader } from './OctreeLoader'; 2 | import { GetUrlFn, XhrRequest } from '../loading/types'; 3 | 4 | export async function loadOctree( 5 | url: string, 6 | getUrl: GetUrlFn, 7 | xhrRequest: XhrRequest, 8 | ) { 9 | const trueUrl = await getUrl(url); 10 | const loader = new OctreeLoader() 11 | const {geometry} = await loader.load(trueUrl, xhrRequest) 12 | return geometry 13 | } -------------------------------------------------------------------------------- /src/materials/blur-material.ts: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial, Texture } from 'three'; 2 | import { IUniform } from './types'; 3 | 4 | // see http://john-chapman-graphics.blogspot.co.at/2013/01/ssao-tutorial.html 5 | 6 | export interface IBlurMaterialUniforms { 7 | [name: string]: IUniform; 8 | screenWidth: IUniform; 9 | screenHeight: IUniform; 10 | map: IUniform; 11 | } 12 | 13 | export class BlurMaterial extends ShaderMaterial { 14 | // vertexShader = require('./shaders/blur.vert'); 15 | // fragmentShader = require('./shaders/blur.frag'); 16 | uniforms: IBlurMaterialUniforms = { 17 | screenWidth: { type: 'f', value: 0 }, 18 | screenHeight: { type: 'f', value: 0 }, 19 | map: { type: 't', value: null }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/materials/classification.ts: -------------------------------------------------------------------------------- 1 | import { Vector4 } from 'three'; 2 | import { IClassification } from './types'; 3 | 4 | export const DEFAULT_CLASSIFICATION: IClassification = { 5 | 0: new Vector4(0.5, 0.5, 0.5, 1.0), 6 | 1: new Vector4(0.5, 0.5, 0.5, 1.0), 7 | 2: new Vector4(0.63, 0.32, 0.18, 1.0), 8 | 3: new Vector4(0.0, 1.0, 0.0, 1.0), 9 | 4: new Vector4(0.0, 0.8, 0.0, 1.0), 10 | 5: new Vector4(0.0, 0.6, 0.0, 1.0), 11 | 6: new Vector4(1.0, 0.66, 0.0, 1.0), 12 | 7: new Vector4(1.0, 0, 1.0, 1.0), 13 | 8: new Vector4(1.0, 0, 0.0, 1.0), 14 | 9: new Vector4(0.0, 0.0, 1.0, 1.0), 15 | 12: new Vector4(1.0, 1.0, 0.0, 1.0), 16 | DEFAULT: new Vector4(0.3, 0.6, 0.6, 0.5), 17 | }; 18 | -------------------------------------------------------------------------------- /src/materials/clipping.ts: -------------------------------------------------------------------------------- 1 | import { Box3, Matrix4, Vector3 } from 'three'; 2 | 3 | export enum ClipMode { 4 | DISABLED = 0, 5 | CLIP_OUTSIDE = 1, 6 | HIGHLIGHT_INSIDE = 2, 7 | } 8 | 9 | export interface IClipBox { 10 | box: Box3; 11 | inverse: Matrix4; 12 | matrix: Matrix4; 13 | position: Vector3; 14 | } 15 | -------------------------------------------------------------------------------- /src/materials/color-encoding.ts: -------------------------------------------------------------------------------- 1 | export enum ColorEncoding { 2 | LINEAR = 0, 3 | SRGB = 1, 4 | } -------------------------------------------------------------------------------- /src/materials/enums.ts: -------------------------------------------------------------------------------- 1 | export enum PointSizeType { 2 | FIXED = 0, 3 | ATTENUATED = 1, 4 | ADAPTIVE = 2, 5 | } 6 | 7 | export enum PointShape { 8 | SQUARE = 0, 9 | CIRCLE = 1, 10 | PARABOLOID = 2, 11 | } 12 | 13 | export enum TreeType { 14 | OCTREE = 0, 15 | KDTREE = 1, 16 | } 17 | 18 | export enum PointOpacityType { 19 | FIXED = 0, 20 | ATTENUATED = 1, 21 | } 22 | 23 | export enum PointColorType { 24 | RGB = 0, 25 | COLOR = 1, 26 | DEPTH = 2, 27 | HEIGHT = 3, 28 | ELEVATION = 3, 29 | INTENSITY = 4, 30 | INTENSITY_GRADIENT = 5, 31 | LOD = 6, 32 | LEVEL_OF_DETAIL = 6, 33 | POINT_INDEX = 7, 34 | CLASSIFICATION = 8, 35 | RETURN_NUMBER = 9, 36 | SOURCE = 10, 37 | NORMAL = 11, 38 | PHONG = 12, 39 | RGB_HEIGHT = 13, 40 | COMPOSITE = 50, 41 | } 42 | -------------------------------------------------------------------------------- /src/materials/gradients/grayscale.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | export const GRAYSCALE: IGradient = [ 5 | [0, new Color(0, 0, 0)], 6 | [1, new Color(1, 1, 1)], 7 | ]; 8 | -------------------------------------------------------------------------------- /src/materials/gradients/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grayscale'; 2 | export * from './inferno'; 3 | export * from './plasma'; 4 | export * from './rainbow'; 5 | export * from './spectral'; 6 | export * from './vidris'; 7 | export * from './yellow-green'; 8 | -------------------------------------------------------------------------------- /src/materials/gradients/inferno.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | export const INFERNO: IGradient = [ 5 | [0.0, new Color(0.077, 0.042, 0.206)], 6 | [0.1, new Color(0.225, 0.036, 0.388)], 7 | [0.2, new Color(0.373, 0.074, 0.432)], 8 | [0.3, new Color(0.522, 0.128, 0.42)], 9 | [0.4, new Color(0.665, 0.182, 0.37)], 10 | [0.5, new Color(0.797, 0.255, 0.287)], 11 | [0.6, new Color(0.902, 0.364, 0.184)], 12 | [0.7, new Color(0.969, 0.516, 0.063)], 13 | [0.8, new Color(0.988, 0.683, 0.072)], 14 | [0.9, new Color(0.961, 0.859, 0.298)], 15 | [1.0, new Color(0.988, 0.998, 0.645)], 16 | ]; 17 | -------------------------------------------------------------------------------- /src/materials/gradients/plasma.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | export const PLASMA: IGradient = [ 5 | [0.0, new Color(0.241, 0.015, 0.61)], 6 | [0.1, new Color(0.387, 0.001, 0.654)], 7 | [0.2, new Color(0.524, 0.025, 0.653)], 8 | [0.3, new Color(0.651, 0.125, 0.596)], 9 | [0.4, new Color(0.752, 0.227, 0.513)], 10 | [0.5, new Color(0.837, 0.329, 0.431)], 11 | [0.6, new Color(0.907, 0.435, 0.353)], 12 | [0.7, new Color(0.963, 0.554, 0.272)], 13 | [0.8, new Color(0.992, 0.681, 0.195)], 14 | [0.9, new Color(0.987, 0.822, 0.144)], 15 | [1.0, new Color(0.94, 0.975, 0.131)], 16 | ]; 17 | -------------------------------------------------------------------------------- /src/materials/gradients/rainbow.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | export const RAINBOW: IGradient = [ 5 | [0, new Color(0.278, 0, 0.714)], 6 | [1 / 6, new Color(0, 0, 1)], 7 | [2 / 6, new Color(0, 1, 1)], 8 | [3 / 6, new Color(0, 1, 0)], 9 | [4 / 6, new Color(1, 1, 0)], 10 | [5 / 6, new Color(1, 0.64, 0)], 11 | [1, new Color(1, 0, 0)], 12 | ]; 13 | -------------------------------------------------------------------------------- /src/materials/gradients/spectral.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | // From chroma spectral http://gka.github.io/chroma.js/ 5 | export const SPECTRAL: IGradient = [ 6 | [0, new Color(0.3686, 0.3098, 0.6353)], 7 | [0.1, new Color(0.1961, 0.5333, 0.7412)], 8 | [0.2, new Color(0.4, 0.7608, 0.6471)], 9 | [0.3, new Color(0.6706, 0.8667, 0.6431)], 10 | [0.4, new Color(0.902, 0.9608, 0.5961)], 11 | [0.5, new Color(1.0, 1.0, 0.749)], 12 | [0.6, new Color(0.9961, 0.8784, 0.5451)], 13 | [0.7, new Color(0.9922, 0.6824, 0.3804)], 14 | [0.8, new Color(0.9569, 0.4275, 0.2627)], 15 | [0.9, new Color(0.8353, 0.2431, 0.3098)], 16 | [1, new Color(0.6196, 0.0039, 0.2588)], 17 | ]; 18 | -------------------------------------------------------------------------------- /src/materials/gradients/vidris.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | export const VIRIDIS: IGradient = [ 5 | [0.0, new Color(0.267, 0.005, 0.329)], 6 | [0.1, new Color(0.283, 0.141, 0.458)], 7 | [0.2, new Color(0.254, 0.265, 0.53)], 8 | [0.3, new Color(0.207, 0.372, 0.553)], 9 | [0.4, new Color(0.164, 0.471, 0.558)], 10 | [0.5, new Color(0.128, 0.567, 0.551)], 11 | [0.6, new Color(0.135, 0.659, 0.518)], 12 | [0.7, new Color(0.267, 0.749, 0.441)], 13 | [0.8, new Color(0.478, 0.821, 0.318)], 14 | [0.9, new Color(0.741, 0.873, 0.15)], 15 | [1.0, new Color(0.993, 0.906, 0.144)], 16 | ]; 17 | -------------------------------------------------------------------------------- /src/materials/gradients/yellow-green.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { IGradient } from '../types'; 3 | 4 | export const YELLOW_GREEN: IGradient = [ 5 | [0, new Color(0.1647, 0.2824, 0.3451)], 6 | [0.1, new Color(0.1338, 0.3555, 0.4227)], 7 | [0.2, new Color(0.061, 0.4319, 0.4864)], 8 | [0.3, new Color(0.0, 0.5099, 0.5319)], 9 | [0.4, new Color(0.0, 0.5881, 0.5569)], 10 | [0.5, new Color(0.137, 0.665, 0.5614)], 11 | [0.6, new Color(0.2906, 0.7395, 0.5477)], 12 | [0.7, new Color(0.4453, 0.8099, 0.5201)], 13 | [0.8, new Color(0.6102, 0.8748, 0.485)], 14 | [0.9, new Color(0.7883, 0.9323, 0.4514)], 15 | [1, new Color(0.9804, 0.9804, 0.4314)], 16 | ]; 17 | -------------------------------------------------------------------------------- /src/materials/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blur-material'; 2 | export * from './clipping'; 3 | export * from './enums'; 4 | export * from './point-cloud-material'; 5 | export * from './texture-generation'; 6 | export * from './types'; 7 | export * from './gradients'; 8 | -------------------------------------------------------------------------------- /src/materials/shaders/blur.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | 4 | uniform mat4 projectionMatrix; 5 | 6 | uniform float screenWidth; 7 | uniform float screenHeight; 8 | 9 | uniform sampler2D map; 10 | 11 | varying vec2 vUv; 12 | 13 | void main() { 14 | 15 | float dx = 1.0 / screenWidth; 16 | float dy = 1.0 / screenHeight; 17 | 18 | vec3 color = vec3(0.0, 0.0, 0.0); 19 | color += texture2D(map, vUv + vec2(-dx, -dy)).rgb; 20 | color += texture2D(map, vUv + vec2( 0, -dy)).rgb; 21 | color += texture2D(map, vUv + vec2(+dx, -dy)).rgb; 22 | color += texture2D(map, vUv + vec2(-dx, 0)).rgb; 23 | color += texture2D(map, vUv + vec2( 0, 0)).rgb; 24 | color += texture2D(map, vUv + vec2(+dx, 0)).rgb; 25 | color += texture2D(map, vUv + vec2(-dx, dy)).rgb; 26 | color += texture2D(map, vUv + vec2( 0, dy)).rgb; 27 | color += texture2D(map, vUv + vec2(+dx, dy)).rgb; 28 | 29 | color = color / 9.0; 30 | 31 | gl_FragColor = vec4(color, 1.0); 32 | 33 | 34 | } -------------------------------------------------------------------------------- /src/materials/shaders/blur.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | 4 | attribute vec3 position; 5 | attribute vec2 uv; 6 | 7 | uniform mat4 modelViewMatrix; 8 | uniform mat4 projectionMatrix; 9 | 10 | varying vec2 vUv; 11 | 12 | void main() { 13 | vUv = uv; 14 | 15 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 16 | } -------------------------------------------------------------------------------- /src/materials/shaders/edl.frag: -------------------------------------------------------------------------------- 1 | // 2 | // adapted from the EDL shader code from Christian Boucheny in cloud compare: 3 | // https://github.com/cloudcompare/trunk/tree/master/plugins/qEDL/shaders/EDL 4 | // 5 | 6 | uniform float screenWidth; 7 | uniform float screenHeight; 8 | uniform vec2 neighbours[NEIGHBOUR_COUNT]; 9 | uniform float edlStrength; 10 | uniform float radius; 11 | uniform float opacity; 12 | 13 | uniform sampler2D colorMap; 14 | 15 | varying vec2 vUv; 16 | 17 | float response(float depth){ 18 | vec2 uvRadius = radius / vec2(screenWidth, screenHeight); 19 | 20 | float sum = 0.0; 21 | 22 | for(int i = 0; i < NEIGHBOUR_COUNT; i++){ 23 | vec2 uvNeighbor = vUv + uvRadius * neighbours[i]; 24 | 25 | float neighbourDepth = texture2D(colorMap, uvNeighbor).a; 26 | 27 | if(neighbourDepth != 0.0){ 28 | if(depth == 0.0){ 29 | sum += 100.0; 30 | }else{ 31 | sum += max(0.0, depth - neighbourDepth); 32 | } 33 | } 34 | } 35 | 36 | return sum / float(NEIGHBOUR_COUNT); 37 | } 38 | 39 | void main(){ 40 | vec4 color = texture2D(colorMap, vUv); 41 | 42 | float depth = color.a; 43 | float res = response(depth); 44 | float shade = exp(-res * 300.0 * edlStrength); 45 | 46 | if(color.a == 0.0 && res == 0.0){ 47 | discard; 48 | }else{ 49 | gl_FragColor = vec4(color.rgb * shade, opacity); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/materials/shaders/edl.vert: -------------------------------------------------------------------------------- 1 | 2 | 3 | varying vec2 vUv; 4 | 5 | void main() { 6 | vUv = uv; 7 | 8 | vec4 mvPosition = modelViewMatrix * vec4(position,1.0); 9 | 10 | gl_Position = projectionMatrix * mvPosition; 11 | } -------------------------------------------------------------------------------- /src/materials/shaders/normalize.frag: -------------------------------------------------------------------------------- 1 | 2 | #extension GL_EXT_frag_depth : enable 3 | 4 | uniform sampler2D depthMap; 5 | uniform sampler2D texture; 6 | 7 | varying vec2 vUv; 8 | 9 | void main() { 10 | float depth = texture2D(depthMap, vUv).g; 11 | 12 | if(depth <= 0.0){ 13 | discard; 14 | } 15 | 16 | vec4 color = texture2D(texture, vUv); 17 | color = color / color.w; 18 | 19 | gl_FragColor = vec4(color.xyz, 1.0); 20 | 21 | gl_FragDepthEXT = depth; 22 | } -------------------------------------------------------------------------------- /src/materials/shaders/normalize.vert: -------------------------------------------------------------------------------- 1 | 2 | varying vec2 vUv; 3 | 4 | void main() { 5 | vUv = uv; 6 | 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); 8 | } -------------------------------------------------------------------------------- /src/materials/shaders/pointcloud.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | precision highp int; 5 | 6 | uniform mat4 viewMatrix; 7 | uniform vec3 cameraPosition; 8 | 9 | uniform mat4 projectionMatrix; 10 | uniform float opacity; 11 | 12 | uniform float blendHardness; 13 | uniform float blendDepthSupplement; 14 | uniform float fov; 15 | uniform float spacing; 16 | uniform float pcIndex; 17 | uniform float screenWidth; 18 | uniform float screenHeight; 19 | 20 | uniform sampler2D depthMap; 21 | 22 | out vec4 fragColor; 23 | 24 | #ifdef highlight_point 25 | uniform vec4 highlightedPointColor; 26 | #endif 27 | 28 | #ifdef new_format 29 | in vec4 vColor; 30 | #else 31 | in vec3 vColor; 32 | #endif 33 | 34 | #if !defined(color_type_point_index) 35 | in float vOpacity; 36 | #endif 37 | 38 | #if defined(weighted_splats) 39 | in float vLinearDepth; 40 | #endif 41 | 42 | #if !defined(paraboloid_point_shape) && defined(use_edl) 43 | in float vLogDepth; 44 | #endif 45 | 46 | #if defined(color_type_phong) && (MAX_POINT_LIGHTS > 0 || MAX_DIR_LIGHTS > 0) || defined(paraboloid_point_shape) 47 | in vec3 vViewPosition; 48 | #endif 49 | 50 | #if defined(weighted_splats) || defined(paraboloid_point_shape) 51 | in float vRadius; 52 | #endif 53 | 54 | #if defined(color_type_phong) && (MAX_POINT_LIGHTS > 0 || MAX_DIR_LIGHTS > 0) 55 | in vec3 vNormal; 56 | #endif 57 | 58 | #ifdef highlight_point 59 | in float vHighlight; 60 | #endif 61 | 62 | float specularStrength = 1.0; 63 | 64 | void main() { 65 | 66 | #ifdef new_format 67 | // set actualColor vec3 from vec4 vColor 68 | vec3 actualColor = vColor.xyz; 69 | #else 70 | // set actualColor RGB from the XYZ of vColor 71 | vec3 actualColor = vColor; 72 | #endif 73 | 74 | vec3 color = actualColor; 75 | float depth = gl_FragCoord.z; 76 | 77 | #if defined(circle_point_shape) || defined(paraboloid_point_shape) || defined (weighted_splats) 78 | float u = 2.0 * gl_PointCoord.x - 1.0; 79 | float v = 2.0 * gl_PointCoord.y - 1.0; 80 | #endif 81 | 82 | #if defined(circle_point_shape) || defined (weighted_splats) 83 | float cc = u*u + v*v; 84 | if(cc > 1.0){ 85 | discard; 86 | } 87 | #endif 88 | 89 | #if defined weighted_splats 90 | vec2 uv = gl_FragCoord.xy / vec2(screenWidth, screenHeight); 91 | float sDepth = texture2D(depthMap, uv).r; 92 | if(vLinearDepth > sDepth + vRadius + blendDepthSupplement){ 93 | discard; 94 | } 95 | #endif 96 | 97 | #if defined color_type_point_index 98 | fragColor = vec4(color, pcIndex / 255.0); 99 | #else 100 | fragColor = vec4(color, vOpacity); 101 | #endif 102 | 103 | #if defined(color_type_phong) 104 | #if MAX_POINT_LIGHTS > 0 || MAX_DIR_LIGHTS > 0 105 | vec3 normal = normalize( vNormal ); 106 | normal.z = abs(normal.z); 107 | 108 | vec3 viewPosition = normalize( vViewPosition ); 109 | #endif 110 | 111 | // code taken from three.js phong light fragment shader 112 | 113 | #if MAX_POINT_LIGHTS > 0 114 | 115 | vec3 pointDiffuse = vec3( 0.0 ); 116 | vec3 pointSpecular = vec3( 0.0 ); 117 | 118 | for ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) { 119 | 120 | vec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 ); 121 | vec3 lVector = lPosition.xyz + vViewPosition.xyz; 122 | 123 | float lDistance = 1.0; 124 | if ( pointLightDistance[ i ] > 0.0 ) 125 | lDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 ); 126 | 127 | lVector = normalize( lVector ); 128 | 129 | // diffuse 130 | 131 | float dotProduct = dot( normal, lVector ); 132 | 133 | #ifdef WRAP_AROUND 134 | 135 | float pointDiffuseWeightFull = max( dotProduct, 0.0 ); 136 | float pointDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 ); 137 | 138 | vec3 pointDiffuseWeight = mix( vec3( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), wrapRGB ); 139 | 140 | #else 141 | 142 | float pointDiffuseWeight = max( dotProduct, 0.0 ); 143 | 144 | #endif 145 | 146 | pointDiffuse += diffuse * pointLightColor[ i ] * pointDiffuseWeight * lDistance; 147 | 148 | // specular 149 | 150 | vec3 pointHalfVector = normalize( lVector + viewPosition ); 151 | float pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 ); 152 | float pointSpecularWeight = specularStrength * max( pow( pointDotNormalHalf, shininess ), 0.0 ); 153 | 154 | float specularNormalization = ( shininess + 2.0 ) / 8.0; 155 | 156 | vec3 schlick = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( lVector, pointHalfVector ), 0.0 ), 5.0 ); 157 | pointSpecular += schlick * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * lDistance * specularNormalization; 158 | pointSpecular = vec3(0.0, 0.0, 0.0); 159 | } 160 | 161 | #endif 162 | 163 | #if MAX_DIR_LIGHTS > 0 164 | 165 | vec3 dirDiffuse = vec3( 0.0 ); 166 | vec3 dirSpecular = vec3( 0.0 ); 167 | 168 | for( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) { 169 | 170 | vec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 ); 171 | vec3 dirVector = normalize( lDirection.xyz ); 172 | 173 | // diffuse 174 | 175 | float dotProduct = dot( normal, dirVector ); 176 | 177 | #ifdef WRAP_AROUND 178 | 179 | float dirDiffuseWeightFull = max( dotProduct, 0.0 ); 180 | float dirDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 ); 181 | 182 | vec3 dirDiffuseWeight = mix( vec3( dirDiffuseWeightFull ), vec3( dirDiffuseWeightHalf ), wrapRGB ); 183 | 184 | #else 185 | 186 | float dirDiffuseWeight = max( dotProduct, 0.0 ); 187 | 188 | #endif 189 | 190 | dirDiffuse += diffuse * directionalLightColor[ i ] * dirDiffuseWeight; 191 | 192 | // specular 193 | 194 | vec3 dirHalfVector = normalize( dirVector + viewPosition ); 195 | float dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 ); 196 | float dirSpecularWeight = specularStrength * max( pow( dirDotNormalHalf, shininess ), 0.0 ); 197 | 198 | float specularNormalization = ( shininess + 2.0 ) / 8.0; 199 | 200 | vec3 schlick = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( dirVector, dirHalfVector ), 0.0 ), 5.0 ); 201 | dirSpecular += schlick * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization; 202 | } 203 | 204 | #endif 205 | 206 | vec3 totalDiffuse = vec3( 0.0 ); 207 | vec3 totalSpecular = vec3( 0.0 ); 208 | 209 | #if MAX_POINT_LIGHTS > 0 210 | 211 | totalDiffuse += pointDiffuse; 212 | totalSpecular += pointSpecular; 213 | 214 | #endif 215 | 216 | #if MAX_DIR_LIGHTS > 0 217 | 218 | totalDiffuse += dirDiffuse; 219 | totalSpecular += dirSpecular; 220 | 221 | #endif 222 | 223 | gl_FragColor.xyz = gl_FragColor.xyz * ( emissive + totalDiffuse + ambientLightColor * ambient ) + totalSpecular; 224 | 225 | #endif 226 | 227 | #if defined weighted_splats 228 | //float w = pow(1.0 - (u*u + v*v), blendHardness); 229 | 230 | float wx = 2.0 * length(2.0 * gl_PointCoord - 1.0); 231 | float w = exp(-wx * wx * 0.5); 232 | 233 | //float distance = length(2.0 * gl_PointCoord - 1.0); 234 | //float w = exp( -(distance * distance) / blendHardness); 235 | 236 | gl_FragColor.rgb = gl_FragColor.rgb * w; 237 | gl_FragColor.a = w; 238 | #endif 239 | 240 | #if defined paraboloid_point_shape 241 | float wi = 0.0 - ( u*u + v*v); 242 | vec4 pos = vec4(vViewPosition, 1.0); 243 | pos.z += wi * vRadius; 244 | float linearDepth = -pos.z; 245 | pos = projectionMatrix * pos; 246 | pos = pos / pos.w; 247 | float expDepth = pos.z; 248 | depth = (pos.z + 1.0) / 2.0; 249 | gl_FragDepth = depth; 250 | 251 | #if defined(color_type_depth) 252 | gl_FragColor.r = linearDepth; 253 | gl_FragColor.g = expDepth; 254 | #endif 255 | 256 | #if defined(use_edl) 257 | gl_FragColor.a = log2(linearDepth); 258 | #endif 259 | 260 | #else 261 | #if defined(use_edl) 262 | gl_FragColor.a = vLogDepth; 263 | #endif 264 | #endif 265 | 266 | #ifdef highlight_point 267 | if (vHighlight > 0.0) { 268 | gl_FragColor = highlightedPointColor; 269 | } 270 | #endif 271 | } 272 | -------------------------------------------------------------------------------- /src/materials/texture-generation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanvasTexture, 3 | Color, 4 | DataTexture, 5 | LinearFilter, 6 | NearestFilter, 7 | RGBAFormat, 8 | Texture, 9 | } from 'three'; 10 | import { IClassification, IGradient } from '../materials/types'; 11 | 12 | export function generateDataTexture(width: number, height: number, color: Color): Texture { 13 | const size = width * height; 14 | const data = new Uint8Array(4 * size); 15 | 16 | const r = Math.floor(color.r * 255); 17 | const g = Math.floor(color.g * 255); 18 | const b = Math.floor(color.b * 255); 19 | 20 | for (let i = 0; i < size; i++) { 21 | data[i * 3] = r; 22 | data[i * 3 + 1] = g; 23 | data[i * 3 + 2] = b; 24 | } 25 | 26 | const texture = new DataTexture(data, width, height, RGBAFormat); 27 | texture.needsUpdate = true; 28 | texture.magFilter = NearestFilter; 29 | 30 | return texture; 31 | } 32 | 33 | export function generateGradientTexture(gradient: IGradient): Texture { 34 | const size = 64; 35 | 36 | const canvas = document.createElement('canvas'); 37 | canvas.width = size; 38 | canvas.height = size; 39 | 40 | const context = canvas.getContext('2d')!; 41 | 42 | context.rect(0, 0, size, size); 43 | const ctxGradient = context.createLinearGradient(0, 0, size, size); 44 | 45 | for (let i = 0; i < gradient.length; i++) { 46 | const step = gradient[i]; 47 | ctxGradient.addColorStop(step[0], `#${step[1].getHexString()}`); 48 | } 49 | 50 | context.fillStyle = ctxGradient; 51 | context.fill(); 52 | 53 | const texture = new CanvasTexture(canvas); 54 | texture.needsUpdate = true; 55 | 56 | texture.minFilter = LinearFilter; 57 | // textureImage = texture.image; 58 | 59 | return texture; 60 | } 61 | 62 | export function generateClassificationTexture(classification: IClassification): Texture { 63 | const width = 256; 64 | const height = 256; 65 | const size = width * height; 66 | 67 | const data = new Uint8Array(4 * size); 68 | 69 | for (let x = 0; x < width; x++) { 70 | for (let y = 0; y < height; y++) { 71 | const i = x + width * y; 72 | 73 | let color; 74 | if (classification[x]) { 75 | color = classification[x]; 76 | } else if (classification[x % 32]) { 77 | color = classification[x % 32]; 78 | } else { 79 | color = classification.DEFAULT; 80 | } 81 | 82 | data[4 * i + 0] = 255 * color.x; 83 | data[4 * i + 1] = 255 * color.y; 84 | data[4 * i + 2] = 255 * color.z; 85 | data[4 * i + 3] = 255 * color.w; 86 | } 87 | } 88 | 89 | const texture = new DataTexture(data, width, height, RGBAFormat); 90 | texture.magFilter = NearestFilter; 91 | texture.needsUpdate = true; 92 | 93 | return texture; 94 | } 95 | -------------------------------------------------------------------------------- /src/materials/types.ts: -------------------------------------------------------------------------------- 1 | import { Color, IUniform as IThreeUniform, Vector4 } from 'three'; 2 | 3 | export type IGradient = [number, Color][]; 4 | 5 | export interface IClassification { 6 | [value: string]: Vector4; 7 | DEFAULT: Vector4; 8 | } 9 | 10 | export interface IUniform extends IThreeUniform { 11 | type: string; 12 | value: T; 13 | } 14 | -------------------------------------------------------------------------------- /src/point-attributes.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------------------------- 2 | // Converted to Typescript and adapted from https://github.com/potree/potree 3 | // ------------------------------------------------------------------------------------------------- 4 | 5 | export enum PointAttributeName { 6 | POSITION_CARTESIAN = 0, // float x, y, z; 7 | COLOR_PACKED = 1, // byte r, g, b, a; I = [0,1] 8 | COLOR_FLOATS_1 = 2, // float r, g, b; I = [0,1] 9 | COLOR_FLOATS_255 = 3, // float r, g, b; I = [0,255] 10 | NORMAL_FLOATS = 4, // float x, y, z; 11 | FILLER = 5, 12 | INTENSITY = 6, 13 | CLASSIFICATION = 7, 14 | NORMAL_SPHEREMAPPED = 8, 15 | NORMAL_OCT16 = 9, 16 | NORMAL = 10, 17 | } 18 | 19 | export interface PointAttributeType { 20 | ordinal: number; 21 | size: number; 22 | } 23 | 24 | export const POINT_ATTRIBUTE_TYPES: Record = { 25 | DATA_TYPE_DOUBLE: { ordinal: 0, size: 8 }, 26 | DATA_TYPE_FLOAT: { ordinal: 1, size: 4 }, 27 | DATA_TYPE_INT8: { ordinal: 2, size: 1 }, 28 | DATA_TYPE_UINT8: { ordinal: 3, size: 1 }, 29 | DATA_TYPE_INT16: { ordinal: 4, size: 2 }, 30 | DATA_TYPE_UINT16: { ordinal: 5, size: 2 }, 31 | DATA_TYPE_INT32: { ordinal: 6, size: 4 }, 32 | DATA_TYPE_UINT32: { ordinal: 7, size: 4 }, 33 | DATA_TYPE_INT64: { ordinal: 8, size: 8 }, 34 | DATA_TYPE_UINT64: { ordinal: 9, size: 8 }, 35 | }; 36 | 37 | export interface IPointAttribute { 38 | name: PointAttributeName; 39 | type: PointAttributeType; 40 | numElements: number; 41 | byteSize: number; 42 | } 43 | 44 | export interface IPointAttributes { 45 | attributes: IPointAttribute[]; 46 | byteSize: number; 47 | size: number; 48 | } 49 | 50 | function makePointAttribute( 51 | name: PointAttributeName, 52 | type: PointAttributeType, 53 | numElements: number, 54 | ): IPointAttribute { 55 | return { 56 | name, 57 | type, 58 | numElements, 59 | byteSize: numElements * type.size, 60 | }; 61 | } 62 | 63 | const RGBA_PACKED = makePointAttribute( 64 | PointAttributeName.COLOR_PACKED, 65 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_INT8, 66 | 4, 67 | ); 68 | 69 | export const POINT_ATTRIBUTES = { 70 | POSITION_CARTESIAN: makePointAttribute( 71 | PointAttributeName.POSITION_CARTESIAN, 72 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_FLOAT, 73 | 3, 74 | ), 75 | RGBA_PACKED, 76 | COLOR_PACKED: RGBA_PACKED, 77 | RGB_PACKED: makePointAttribute( 78 | PointAttributeName.COLOR_PACKED, 79 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_INT8, 80 | 3, 81 | ), 82 | NORMAL_FLOATS: makePointAttribute( 83 | PointAttributeName.NORMAL_FLOATS, 84 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_FLOAT, 85 | 3, 86 | ), 87 | FILLER_1B: makePointAttribute( 88 | PointAttributeName.FILLER, 89 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_UINT8, 90 | 1, 91 | ), 92 | INTENSITY: makePointAttribute( 93 | PointAttributeName.INTENSITY, 94 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_UINT16, 95 | 1, 96 | ), 97 | CLASSIFICATION: makePointAttribute( 98 | PointAttributeName.CLASSIFICATION, 99 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_UINT8, 100 | 1, 101 | ), 102 | NORMAL_SPHEREMAPPED: makePointAttribute( 103 | PointAttributeName.NORMAL_SPHEREMAPPED, 104 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_UINT8, 105 | 2, 106 | ), 107 | NORMAL_OCT16: makePointAttribute( 108 | PointAttributeName.NORMAL_OCT16, 109 | POINT_ATTRIBUTE_TYPES.DATA_TYPE_UINT8, 110 | 2, 111 | ), 112 | NORMAL: makePointAttribute(PointAttributeName.NORMAL, POINT_ATTRIBUTE_TYPES.DATA_TYPE_FLOAT, 3), 113 | }; 114 | 115 | export type PointAttributeStringName = keyof typeof POINT_ATTRIBUTES; 116 | 117 | export class PointAttributes implements IPointAttributes { 118 | attributes: IPointAttribute[] = []; 119 | byteSize: number = 0; 120 | size: number = 0; 121 | 122 | constructor(pointAttributeNames: PointAttributeStringName[] = []) { 123 | for (let i = 0; i < pointAttributeNames.length; i++) { 124 | const pointAttributeName = pointAttributeNames[i]; 125 | const pointAttribute = POINT_ATTRIBUTES[pointAttributeName]; 126 | this.attributes.push(pointAttribute); 127 | this.byteSize += pointAttribute.byteSize; 128 | this.size++; 129 | } 130 | } 131 | 132 | add(pointAttribute: IPointAttribute): void { 133 | this.attributes.push(pointAttribute); 134 | this.byteSize += pointAttribute.byteSize; 135 | this.size++; 136 | } 137 | 138 | hasColors(): boolean { 139 | return this.attributes.find(isColorAttribute) !== undefined; 140 | } 141 | 142 | hasNormals(): boolean { 143 | return this.attributes.find(isNormalAttribute) !== undefined; 144 | } 145 | } 146 | 147 | function isColorAttribute({ name }: IPointAttribute): boolean { 148 | return name === PointAttributeName.COLOR_PACKED; 149 | } 150 | 151 | function isNormalAttribute({ name }: IPointAttribute): boolean { 152 | return ( 153 | name === PointAttributeName.NORMAL_SPHEREMAPPED || 154 | name === PointAttributeName.NORMAL_FLOATS || 155 | name === PointAttributeName.NORMAL || 156 | name === PointAttributeName.NORMAL_OCT16 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/point-cloud-octree-geometry-node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Potree.js http://potree.org 3 | * Potree License: https://github.com/potree/potree/blob/1.5/LICENSE 4 | */ 5 | 6 | import { Box3, BufferGeometry, EventDispatcher, Sphere, Vector3 } from 'three'; 7 | import { PointCloudOctreeGeometry } from './point-cloud-octree-geometry'; 8 | import { IPointCloudTreeNode } from './types'; 9 | import { createChildAABB } from './utils/bounds'; 10 | import { getIndexFromName } from './utils/utils'; 11 | 12 | export interface NodeData { 13 | children: number; 14 | numPoints: number; 15 | name: string; 16 | } 17 | 18 | const NODE_STRIDE = 5; 19 | 20 | export class PointCloudOctreeGeometryNode extends EventDispatcher implements IPointCloudTreeNode { 21 | id: number = PointCloudOctreeGeometryNode.idCount++; 22 | name: string; 23 | pcoGeometry: PointCloudOctreeGeometry; 24 | index: number; 25 | level: number = 0; 26 | spacing: number = 0; 27 | hasChildren: boolean = false; 28 | readonly children: ReadonlyArray = [ 29 | null, 30 | null, 31 | null, 32 | null, 33 | null, 34 | null, 35 | null, 36 | null, 37 | ]; 38 | boundingBox: Box3; 39 | tightBoundingBox: Box3; 40 | boundingSphere: Sphere; 41 | mean: Vector3 = new Vector3(); 42 | numPoints: number = 0; 43 | geometry: BufferGeometry | undefined; 44 | loaded: boolean = false; 45 | loading: boolean = false; 46 | failed: boolean = false; 47 | parent: PointCloudOctreeGeometryNode | null = null; 48 | oneTimeDisposeHandlers: (() => void)[] = []; 49 | isLeafNode: boolean = true; 50 | readonly isTreeNode: boolean = false; 51 | readonly isGeometryNode: boolean = true; 52 | 53 | private static idCount = 0; 54 | 55 | constructor(name: string, pcoGeometry: PointCloudOctreeGeometry, boundingBox: Box3) { 56 | super(); 57 | 58 | this.name = name; 59 | this.index = getIndexFromName(name); 60 | this.pcoGeometry = pcoGeometry; 61 | this.boundingBox = boundingBox; 62 | this.tightBoundingBox = boundingBox.clone(); 63 | this.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); 64 | } 65 | 66 | dispose(): void { 67 | if (!this.geometry || !this.parent) { 68 | return; 69 | } 70 | 71 | this.geometry.dispose(); 72 | this.geometry = undefined; 73 | this.loaded = false; 74 | 75 | this.oneTimeDisposeHandlers.forEach(handler => handler()); 76 | this.oneTimeDisposeHandlers = []; 77 | } 78 | 79 | /** 80 | * Gets the url of the binary file for this node. 81 | */ 82 | getUrl(): string { 83 | const geometry = this.pcoGeometry; 84 | const version = geometry.loader.version; 85 | const pathParts = [geometry.octreeDir]; 86 | 87 | if (geometry.loader && version.equalOrHigher('1.5')) { 88 | pathParts.push(this.getHierarchyBaseUrl()); 89 | pathParts.push(this.name); 90 | } else if (version.equalOrHigher('1.4')) { 91 | pathParts.push(this.name); 92 | } else if (version.upTo('1.3')) { 93 | pathParts.push(this.name); 94 | } 95 | 96 | return pathParts.join('/'); 97 | } 98 | 99 | /** 100 | * Gets the url of the hierarchy file for this node. 101 | */ 102 | getHierarchyUrl(): string { 103 | return `${this.pcoGeometry.octreeDir}/${this.getHierarchyBaseUrl()}/${this.name}.hrc`; 104 | } 105 | 106 | /** 107 | * Adds the specified node as a child of the current node. 108 | * 109 | * @param child 110 | * The node which is to be added as a child. 111 | */ 112 | addChild(child: PointCloudOctreeGeometryNode): void { 113 | (this.children as any)[child.index] = child; 114 | this.isLeafNode = false; 115 | child.parent = this; 116 | } 117 | 118 | /** 119 | * Calls the specified callback for the current node (if includeSelf is set to true) and all its 120 | * children. 121 | * 122 | * @param cb 123 | * The function which is to be called for each node. 124 | */ 125 | traverse(cb: (node: PointCloudOctreeGeometryNode) => void, includeSelf = true): void { 126 | const stack: PointCloudOctreeGeometryNode[] = includeSelf ? [this] : []; 127 | 128 | let current: PointCloudOctreeGeometryNode | undefined; 129 | 130 | while ((current = stack.pop()) !== undefined) { 131 | cb(current); 132 | 133 | for (const child of current.children) { 134 | if (child !== null) { 135 | stack.push(child); 136 | } 137 | } 138 | } 139 | } 140 | 141 | load(): Promise { 142 | if (!this.canLoad()) { 143 | return Promise.resolve(); 144 | } 145 | 146 | this.loading = true; 147 | this.pcoGeometry.numNodesLoading++; 148 | this.pcoGeometry.needsUpdate = true; 149 | 150 | let promise: Promise; 151 | 152 | if ( 153 | this.pcoGeometry.loader.version.equalOrHigher('1.5') && 154 | this.level % this.pcoGeometry.hierarchyStepSize === 0 && 155 | this.hasChildren 156 | ) { 157 | promise = this.loadHierachyThenPoints(); 158 | } else { 159 | promise = this.loadPoints(); 160 | } 161 | 162 | return promise.catch(reason => { 163 | this.loading = false; 164 | this.failed = true; 165 | this.pcoGeometry.numNodesLoading--; 166 | throw reason; 167 | }); 168 | } 169 | 170 | private canLoad(): boolean { 171 | return ( 172 | !this.loading && 173 | !this.loaded && 174 | !this.pcoGeometry.disposed && 175 | !this.pcoGeometry.loader.disposed && 176 | this.pcoGeometry.numNodesLoading < this.pcoGeometry.maxNumNodesLoading 177 | ); 178 | } 179 | 180 | private loadPoints(): Promise { 181 | this.pcoGeometry.needsUpdate = true; 182 | return this.pcoGeometry.loader.load(this); 183 | } 184 | 185 | private loadHierachyThenPoints(): Promise { 186 | if (this.level % this.pcoGeometry.hierarchyStepSize !== 0) { 187 | return Promise.resolve(); 188 | } 189 | 190 | return Promise.resolve(this.pcoGeometry.loader.getUrl(this.getHierarchyUrl())) 191 | .then(url => this.pcoGeometry.xhrRequest(url, { mode: 'cors' })) 192 | .then(res => res.arrayBuffer()) 193 | .then(data => this.loadHierarchy(this, data)); 194 | } 195 | 196 | /** 197 | * Gets the url of the folder where the hierarchy is, relative to the octreeDir. 198 | */ 199 | private getHierarchyBaseUrl(): string { 200 | const hierarchyStepSize = this.pcoGeometry.hierarchyStepSize; 201 | const indices = this.name.substr(1); 202 | const numParts = Math.floor(indices.length / hierarchyStepSize); 203 | 204 | let path = 'r/'; 205 | for (let i = 0; i < numParts; i++) { 206 | path += `${indices.substr(i * hierarchyStepSize, hierarchyStepSize)}/`; 207 | } 208 | 209 | return path.slice(0, -1); 210 | } 211 | 212 | // tslint:disable:no-bitwise 213 | private loadHierarchy(node: PointCloudOctreeGeometryNode, buffer: ArrayBuffer) { 214 | const view = new DataView(buffer); 215 | 216 | const firstNodeData = this.getNodeData(node.name, 0, view); 217 | node.numPoints = firstNodeData.numPoints; 218 | 219 | // Nodes which need be visited. 220 | const stack: NodeData[] = [firstNodeData]; 221 | // Nodes which have already been decoded. We will take nodes from the stack and place them here. 222 | const decoded: NodeData[] = []; 223 | 224 | let offset = NODE_STRIDE; 225 | while (stack.length > 0) { 226 | const stackNodeData = stack.shift()!; 227 | 228 | // From the last bit, all the way to the 8th one from the right. 229 | let mask = 1; 230 | for (let i = 0; i < 8 && offset + 1 < buffer.byteLength; i++) { 231 | if ((stackNodeData.children & mask) !== 0) { 232 | const nodeData = this.getNodeData(stackNodeData.name + i, offset, view); 233 | 234 | decoded.push(nodeData); // Node is decoded. 235 | stack.push(nodeData); // Need to check its children. 236 | 237 | offset += NODE_STRIDE; // Move over to the next node in the buffer. 238 | } 239 | 240 | mask = mask * 2; 241 | } 242 | } 243 | 244 | node.pcoGeometry.needsUpdate = true; 245 | 246 | // Map containing all the nodes. 247 | const nodes = new Map(); 248 | nodes.set(node.name, node); 249 | decoded.forEach(nodeData => this.addNode(nodeData, node.pcoGeometry, nodes)); 250 | 251 | node.loadPoints(); 252 | } 253 | 254 | // tslint:enable:no-bitwise 255 | 256 | private getNodeData(name: string, offset: number, view: DataView): NodeData { 257 | const children = view.getUint8(offset); 258 | const numPoints = view.getUint32(offset + 1, true); 259 | return { children: children, numPoints: numPoints, name }; 260 | } 261 | 262 | addNode( 263 | { name, numPoints, children }: NodeData, 264 | pco: PointCloudOctreeGeometry, 265 | nodes: Map, 266 | ): void { 267 | const index = getIndexFromName(name); 268 | const parentName = name.substring(0, name.length - 1); 269 | const parentNode = nodes.get(parentName)!; 270 | const level = name.length - 1; 271 | const boundingBox = createChildAABB(parentNode.boundingBox, index); 272 | 273 | const node = new PointCloudOctreeGeometryNode(name, pco, boundingBox); 274 | node.level = level; 275 | node.numPoints = numPoints; 276 | node.hasChildren = children > 0; 277 | node.spacing = pco.spacing / Math.pow(2, level); 278 | 279 | parentNode.addChild(node); 280 | nodes.set(name, node); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/point-cloud-octree-geometry.ts: -------------------------------------------------------------------------------- 1 | import { Box3, Vector3 } from 'three'; 2 | import { BinaryLoader, XhrRequest } from './loading'; 3 | import { PointAttributes } from './point-attributes'; 4 | import { PointCloudOctreeGeometryNode } from './point-cloud-octree-geometry-node'; 5 | 6 | export class PointCloudOctreeGeometry { 7 | disposed: boolean = false; 8 | needsUpdate: boolean = true; 9 | root!: PointCloudOctreeGeometryNode; 10 | octreeDir: string = ''; 11 | hierarchyStepSize: number = -1; 12 | nodes: Record = {}; 13 | numNodesLoading: number = 0; 14 | maxNumNodesLoading: number = 3; 15 | spacing: number = 0; 16 | pointAttributes: PointAttributes = new PointAttributes([]); 17 | projection: any = null; 18 | url: string | null = null; 19 | 20 | constructor( 21 | public loader: BinaryLoader, 22 | public boundingBox: Box3, 23 | public tightBoundingBox: Box3, 24 | public offset: Vector3, 25 | public xhrRequest: XhrRequest, 26 | ) {} 27 | 28 | dispose(): void { 29 | this.loader.dispose(); 30 | this.root.traverse(node => node.dispose()); 31 | 32 | this.disposed = true; 33 | } 34 | 35 | addNodeLoadedCallback(callback: (node: PointCloudOctreeGeometryNode) => void): void { 36 | this.loader.callbacks.push(callback); 37 | } 38 | 39 | clearNodeLoadedCallbacks(): void { 40 | this.loader.callbacks = []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/point-cloud-octree-node.ts: -------------------------------------------------------------------------------- 1 | import { Box3, BufferGeometry, EventDispatcher, Object3D, Points, Sphere } from 'three'; 2 | import { PointCloudOctreeGeometryNode } from './point-cloud-octree-geometry-node'; 3 | import { IPointCloudTreeNode } from './types'; 4 | 5 | export class PointCloudOctreeNode extends EventDispatcher implements IPointCloudTreeNode { 6 | geometryNode: PointCloudOctreeGeometryNode; 7 | sceneNode: Points; 8 | pcIndex: number | undefined = undefined; 9 | boundingBoxNode: Object3D | null = null; 10 | readonly children: (IPointCloudTreeNode | null)[]; 11 | readonly loaded = true; 12 | readonly isTreeNode: boolean = true; 13 | readonly isGeometryNode: boolean = false; 14 | 15 | constructor(geometryNode: PointCloudOctreeGeometryNode, sceneNode: Points) { 16 | super(); 17 | 18 | this.geometryNode = geometryNode; 19 | this.sceneNode = sceneNode; 20 | this.children = geometryNode.children.slice(); 21 | } 22 | 23 | dispose(): void { 24 | this.geometryNode.dispose(); 25 | } 26 | 27 | disposeSceneNode(): void { 28 | const node = this.sceneNode; 29 | 30 | if (node.geometry instanceof BufferGeometry) { 31 | const attributes = node.geometry.attributes; 32 | 33 | // tslint:disable-next-line:forin 34 | for (const key in attributes) { 35 | if (key === 'position') { 36 | delete (attributes[key] as any).array; 37 | } 38 | 39 | delete attributes[key]; 40 | } 41 | 42 | node.geometry.dispose(); 43 | node.geometry = undefined as any; 44 | } 45 | } 46 | 47 | traverse(cb: (node: IPointCloudTreeNode) => void, includeSelf?: boolean): void { 48 | this.geometryNode.traverse(cb, includeSelf); 49 | } 50 | 51 | get id() { 52 | return this.geometryNode.id; 53 | } 54 | 55 | get name() { 56 | return this.geometryNode.name; 57 | } 58 | 59 | get level(): number { 60 | return this.geometryNode.level; 61 | } 62 | 63 | get isLeafNode(): boolean { 64 | return this.geometryNode.isLeafNode; 65 | } 66 | 67 | get numPoints(): number { 68 | return this.geometryNode.numPoints; 69 | } 70 | 71 | get index() { 72 | return this.geometryNode.index; 73 | } 74 | 75 | get boundingSphere(): Sphere { 76 | return this.geometryNode.boundingSphere; 77 | } 78 | 79 | get boundingBox(): Box3 { 80 | return this.geometryNode.boundingBox; 81 | } 82 | 83 | get spacing() { 84 | return this.geometryNode.spacing; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/point-cloud-octree-picker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | Camera, 4 | LinearFilter, 5 | NearestFilter, 6 | NoBlending, 7 | Points, 8 | Ray, 9 | RGBAFormat, 10 | Scene, 11 | Sphere, 12 | Vector3, 13 | Vector4, 14 | WebGLRenderer, 15 | WebGLRenderTarget, 16 | Color, 17 | } from 'three'; 18 | import { COLOR_BLACK, DEFAULT_PICK_WINDOW_SIZE } from './constants'; 19 | import { ClipMode, PointCloudMaterial, PointColorType } from './materials'; 20 | import { PointCloudOctree } from './point-cloud-octree'; 21 | import { PointCloudOctreeNode } from './point-cloud-octree-node'; 22 | import { PickPoint, PointCloudHit } from './types'; 23 | import { clamp } from './utils/math'; 24 | 25 | export interface PickParams { 26 | pickWindowSize: number; 27 | pickOutsideClipRegion: boolean; 28 | /** 29 | * If provided, the picking will use this pixel position instead of the `Ray` passed to the `pick` 30 | * method. 31 | */ 32 | pixelPosition: Vector3; 33 | /** 34 | * Function which gets called after a picking material has been created and setup and before the 35 | * point cloud is rendered into the picking render target. This gives applications a chance to 36 | * customize the renderTarget and the material. 37 | * 38 | * @param material 39 | * The pick material. 40 | * @param renterTarget 41 | * The render target used for picking. 42 | */ 43 | onBeforePickRender: (material: PointCloudMaterial, renterTarget: WebGLRenderTarget) => void; 44 | } 45 | 46 | interface IPickState { 47 | renderTarget: WebGLRenderTarget; 48 | material: PointCloudMaterial; 49 | scene: Scene; 50 | } 51 | 52 | interface RenderedNode { 53 | node: PointCloudOctreeNode; 54 | octree: PointCloudOctree; 55 | } 56 | 57 | export class PointCloudOctreePicker { 58 | private static readonly helperVec3 = new Vector3(); 59 | private static readonly helperSphere = new Sphere(); 60 | private static readonly clearColor = new Color(); 61 | private pickState: IPickState | undefined; 62 | 63 | dispose() { 64 | if (this.pickState) { 65 | this.pickState.material.dispose(); 66 | this.pickState.renderTarget.dispose(); 67 | } 68 | } 69 | 70 | pick( 71 | renderer: WebGLRenderer, 72 | camera: Camera, 73 | ray: Ray, 74 | octrees: PointCloudOctree[], 75 | params: Partial = {}, 76 | ): PickPoint | null { 77 | if (octrees.length === 0) { 78 | return null; 79 | } 80 | 81 | const pickState = this.pickState 82 | ? this.pickState 83 | : (this.pickState = PointCloudOctreePicker.getPickState()); 84 | 85 | const pickMaterial = pickState.material; 86 | 87 | const pixelRatio = renderer.getPixelRatio(); 88 | const width = Math.ceil(renderer.domElement.clientWidth * pixelRatio); 89 | const height = Math.ceil(renderer.domElement.clientHeight * pixelRatio); 90 | PointCloudOctreePicker.updatePickRenderTarget(this.pickState, width, height); 91 | 92 | const pixelPosition = PointCloudOctreePicker.helperVec3; // Use helper vector to prevent extra allocations. 93 | 94 | if (params.pixelPosition) { 95 | pixelPosition.copy(params.pixelPosition); 96 | } else { 97 | pixelPosition.addVectors(camera.position, ray.direction).project(camera); 98 | pixelPosition.x = (pixelPosition.x + 1) * width * 0.5; 99 | pixelPosition.y = (pixelPosition.y + 1) * height * 0.5; 100 | } 101 | 102 | const pickWndSize = Math.floor( 103 | (params.pickWindowSize || DEFAULT_PICK_WINDOW_SIZE) * pixelRatio, 104 | ); 105 | const halfPickWndSize = (pickWndSize - 1) / 2; 106 | const x = Math.floor(clamp(pixelPosition.x - halfPickWndSize, 0, width)); 107 | const y = Math.floor(clamp(pixelPosition.y - halfPickWndSize, 0, height)); 108 | 109 | PointCloudOctreePicker.prepareRender(renderer, x, y, pickWndSize, pickMaterial, pickState); 110 | 111 | const renderedNodes = PointCloudOctreePicker.render( 112 | renderer, 113 | camera, 114 | pickMaterial, 115 | octrees, 116 | ray, 117 | pickState, 118 | params, 119 | ); 120 | 121 | // Cleanup 122 | pickMaterial.clearVisibleNodeTextureOffsets(); 123 | 124 | // Read back image and decode hit point 125 | const pixels = PointCloudOctreePicker.readPixels(renderer, x, y, pickWndSize); 126 | const hit = PointCloudOctreePicker.findHit(pixels, pickWndSize); 127 | return PointCloudOctreePicker.getPickPoint(hit, renderedNodes); 128 | } 129 | 130 | private static prepareRender( 131 | renderer: WebGLRenderer, 132 | x: number, 133 | y: number, 134 | pickWndSize: number, 135 | pickMaterial: PointCloudMaterial, 136 | pickState: IPickState, 137 | ) { 138 | // Render the intersected nodes onto the pick render target, clipping to a small pick window. 139 | renderer.setScissor(x, y, pickWndSize, pickWndSize); 140 | renderer.setScissorTest(true); 141 | renderer.state.buffers.depth.setTest(pickMaterial.depthTest); 142 | renderer.state.buffers.depth.setMask(pickMaterial.depthWrite); 143 | renderer.state.setBlending(NoBlending); 144 | 145 | renderer.setRenderTarget(pickState.renderTarget); 146 | 147 | // Save the current clear color and clear the renderer with black color and alpha 0. 148 | renderer.getClearColor(this.clearColor); 149 | const oldClearAlpha = renderer.getClearAlpha(); 150 | renderer.setClearColor(COLOR_BLACK, 0); 151 | renderer.clear(true, true, true); 152 | renderer.setClearColor(this.clearColor, oldClearAlpha); 153 | } 154 | 155 | private static render( 156 | renderer: WebGLRenderer, 157 | camera: Camera, 158 | pickMaterial: PointCloudMaterial, 159 | octrees: PointCloudOctree[], 160 | ray: Ray, 161 | pickState: IPickState, 162 | params: Partial, 163 | ): RenderedNode[] { 164 | const renderedNodes: RenderedNode[] = []; 165 | for (const octree of octrees) { 166 | // Get all the octree nodes which intersect the picking ray. We only need to render those. 167 | const nodes = PointCloudOctreePicker.nodesOnRay(octree, ray); 168 | if (!nodes.length) { 169 | continue; 170 | } 171 | 172 | PointCloudOctreePicker.updatePickMaterial(pickMaterial, octree.material, params); 173 | pickMaterial.updateMaterial(octree, nodes, camera, renderer); 174 | 175 | if (params.onBeforePickRender) { 176 | params.onBeforePickRender(pickMaterial, pickState.renderTarget); 177 | } 178 | 179 | // Create copies of the nodes so we can render them differently than in the normal point cloud. 180 | pickState.scene.children = PointCloudOctreePicker.createTempNodes( 181 | octree, 182 | nodes, 183 | pickMaterial, 184 | renderedNodes.length, 185 | ); 186 | 187 | renderer.render(pickState.scene, camera); 188 | 189 | nodes.forEach(node => renderedNodes.push({ node, octree })); 190 | } 191 | return renderedNodes; 192 | } 193 | 194 | private static nodesOnRay(octree: PointCloudOctree, ray: Ray): PointCloudOctreeNode[] { 195 | const nodesOnRay: PointCloudOctreeNode[] = []; 196 | 197 | const rayClone = ray.clone(); 198 | for (const node of octree.visibleNodes) { 199 | const sphere = PointCloudOctreePicker.helperSphere 200 | .copy(node.boundingSphere) 201 | .applyMatrix4(octree.matrixWorld); 202 | 203 | if (rayClone.intersectsSphere(sphere)) { 204 | nodesOnRay.push(node); 205 | } 206 | } 207 | 208 | return nodesOnRay; 209 | } 210 | 211 | private static readPixels( 212 | renderer: WebGLRenderer, 213 | x: number, 214 | y: number, 215 | pickWndSize: number, 216 | ): Uint8Array { 217 | // Read the pixel from the pick render target. 218 | const pixels = new Uint8Array(4 * pickWndSize * pickWndSize); 219 | renderer.readRenderTargetPixels( 220 | renderer.getRenderTarget()!, 221 | x, 222 | y, 223 | pickWndSize, 224 | pickWndSize, 225 | pixels, 226 | ); 227 | renderer.setScissorTest(false); 228 | renderer.setRenderTarget(null!); 229 | return pixels; 230 | } 231 | 232 | private static createTempNodes( 233 | octree: PointCloudOctree, 234 | nodes: PointCloudOctreeNode[], 235 | pickMaterial: PointCloudMaterial, 236 | nodeIndexOffset: number, 237 | ): Points[] { 238 | const tempNodes: Points[] = []; 239 | for (let i = 0; i < nodes.length; i++) { 240 | const node = nodes[i]; 241 | const sceneNode = node.sceneNode; 242 | const tempNode = new Points(sceneNode.geometry, pickMaterial); 243 | tempNode.matrix = sceneNode.matrix; 244 | tempNode.matrixWorld = sceneNode.matrixWorld; 245 | tempNode.matrixAutoUpdate = false; 246 | tempNode.frustumCulled = false; 247 | const nodeIndex = nodeIndexOffset + i + 1; 248 | if (nodeIndex > 255) { 249 | console.error('More than 255 nodes for pick are not supported.'); 250 | } 251 | tempNode.onBeforeRender = PointCloudMaterial.makeOnBeforeRender(octree, node, nodeIndex); 252 | 253 | tempNodes.push(tempNode); 254 | } 255 | return tempNodes; 256 | } 257 | 258 | private static updatePickMaterial( 259 | pickMaterial: PointCloudMaterial, 260 | nodeMaterial: PointCloudMaterial, 261 | params: Partial, 262 | ): void { 263 | pickMaterial.pointSizeType = nodeMaterial.pointSizeType; 264 | pickMaterial.shape = nodeMaterial.shape; 265 | pickMaterial.size = nodeMaterial.size; 266 | pickMaterial.minSize = nodeMaterial.minSize; 267 | pickMaterial.maxSize = nodeMaterial.maxSize; 268 | pickMaterial.classification = nodeMaterial.classification; 269 | pickMaterial.useFilterByNormal = nodeMaterial.useFilterByNormal; 270 | pickMaterial.filterByNormalThreshold = nodeMaterial.filterByNormalThreshold; 271 | 272 | if (params.pickOutsideClipRegion) { 273 | pickMaterial.clipMode = ClipMode.DISABLED; 274 | } else { 275 | pickMaterial.clipMode = nodeMaterial.clipMode; 276 | pickMaterial.setClipBoxes( 277 | nodeMaterial.clipMode === ClipMode.CLIP_OUTSIDE ? nodeMaterial.clipBoxes : [], 278 | ); 279 | } 280 | } 281 | 282 | private static updatePickRenderTarget( 283 | pickState: IPickState, 284 | width: number, 285 | height: number, 286 | ): void { 287 | if (pickState.renderTarget.width === width && pickState.renderTarget.height === height) { 288 | return; 289 | } 290 | 291 | pickState.renderTarget.dispose(); 292 | pickState.renderTarget = PointCloudOctreePicker.makePickRenderTarget(); 293 | pickState.renderTarget.setSize(width, height); 294 | } 295 | 296 | private static makePickRenderTarget() { 297 | return new WebGLRenderTarget(1, 1, { 298 | minFilter: LinearFilter, 299 | magFilter: NearestFilter, 300 | format: RGBAFormat, 301 | }); 302 | } 303 | 304 | private static findHit(pixels: Uint8Array, pickWndSize: number): PointCloudHit | null { 305 | const ibuffer = new Uint32Array(pixels.buffer); 306 | 307 | // Find closest hit inside pixelWindow boundaries 308 | let min = Number.MAX_VALUE; 309 | let hit: PointCloudHit | null = null; 310 | for (let u = 0; u < pickWndSize; u++) { 311 | for (let v = 0; v < pickWndSize; v++) { 312 | const offset = u + v * pickWndSize; 313 | const distance = 314 | Math.pow(u - (pickWndSize - 1) / 2, 2) + Math.pow(v - (pickWndSize - 1) / 2, 2); 315 | 316 | const pcIndex = pixels[4 * offset + 3]; 317 | pixels[4 * offset + 3] = 0; 318 | const pIndex = ibuffer[offset]; 319 | 320 | if (pcIndex > 0 && distance < min) { 321 | hit = { 322 | pIndex: pIndex, 323 | pcIndex: pcIndex - 1, 324 | }; 325 | min = distance; 326 | } 327 | } 328 | } 329 | return hit; 330 | } 331 | 332 | private static getPickPoint(hit: PointCloudHit | null, nodes: RenderedNode[]): PickPoint | null { 333 | if (!hit) { 334 | return null; 335 | } 336 | 337 | const point: PickPoint = {}; 338 | 339 | const points = nodes[hit.pcIndex] && nodes[hit.pcIndex].node.sceneNode; 340 | if (!points) { 341 | return null; 342 | } 343 | 344 | point.pointCloud = nodes[hit.pcIndex].octree; 345 | 346 | const attributes: BufferAttribute[] = (points.geometry as any).attributes; 347 | 348 | for (const property in attributes) { 349 | if (!attributes.hasOwnProperty(property)) { 350 | continue; 351 | } 352 | 353 | const values = attributes[property]; 354 | 355 | // tslint:disable-next-line:prefer-switch 356 | if (property === 'position') { 357 | PointCloudOctreePicker.addPositionToPickPoint(point, hit, values, points); 358 | } else if (property === 'normal') { 359 | PointCloudOctreePicker.addNormalToPickPoint(point, hit, values, points); 360 | } else if (property === 'indices') { 361 | // TODO 362 | } else { 363 | if (values.itemSize === 1) { 364 | point[property] = values.array[hit.pIndex]; 365 | } else { 366 | const value: number[] = []; 367 | for (let j = 0; j < values.itemSize; j++) { 368 | value.push(values.array[values.itemSize * hit.pIndex + j]); 369 | } 370 | point[property] = value; 371 | } 372 | } 373 | } 374 | 375 | return point; 376 | } 377 | 378 | private static addPositionToPickPoint( 379 | point: PickPoint, 380 | hit: PointCloudHit, 381 | values: BufferAttribute, 382 | points: Points, 383 | ): void { 384 | point.position = new Vector3() 385 | .fromBufferAttribute(values, hit.pIndex) 386 | .applyMatrix4(points.matrixWorld); 387 | } 388 | 389 | private static addNormalToPickPoint( 390 | point: PickPoint, 391 | hit: PointCloudHit, 392 | values: BufferAttribute, 393 | points: Points, 394 | ): void { 395 | const normal = new Vector3().fromBufferAttribute(values, hit.pIndex); 396 | const normal4 = new Vector4(normal.x, normal.y, normal.z, 0).applyMatrix4(points.matrixWorld); 397 | normal.set(normal4.x, normal4.y, normal4.z); 398 | 399 | point.normal = normal; 400 | } 401 | 402 | private static getPickState() { 403 | const scene = new Scene(); 404 | scene.autoUpdate = false; 405 | 406 | const material = new PointCloudMaterial(); 407 | material.pointColorType = PointColorType.POINT_INDEX; 408 | 409 | return { 410 | renderTarget: PointCloudOctreePicker.makePickRenderTarget(), 411 | material: material, 412 | scene: scene, 413 | }; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/point-cloud-octree.ts: -------------------------------------------------------------------------------- 1 | import { OctreeGeometry } from './loading2/OctreeGeometry'; 2 | import { Box3, Camera, Object3D, Points, Ray, Sphere, Vector3, WebGLRenderer } from 'three'; 3 | import { DEFAULT_MIN_NODE_PIXEL_SIZE } from './constants'; 4 | import { PointCloudMaterial, PointSizeType } from './materials'; 5 | import { PointCloudOctreeGeometryNode } from './point-cloud-octree-geometry-node'; 6 | import { PointCloudOctreeNode } from './point-cloud-octree-node'; 7 | import { PickParams, PointCloudOctreePicker } from './point-cloud-octree-picker'; 8 | import { PointCloudTree } from './point-cloud-tree'; 9 | import { IPointCloudTreeNode, IPotree, PickPoint, PCOGeometry } from './types'; 10 | import { computeTransformedBoundingBox } from './utils/bounds'; 11 | 12 | export class PointCloudOctree extends PointCloudTree { 13 | potree: IPotree; 14 | disposed: boolean = false; 15 | pcoGeometry: PCOGeometry; 16 | boundingBox: Box3; 17 | boundingSphere: Sphere; 18 | material: PointCloudMaterial; 19 | level: number = 0; 20 | maxLevel: number = Infinity; 21 | /** 22 | * The minimum radius of a node's bounding sphere on the screen in order to be displayed. 23 | */ 24 | minNodePixelSize: number = DEFAULT_MIN_NODE_PIXEL_SIZE; 25 | root: IPointCloudTreeNode | null = null; 26 | boundingBoxNodes: Object3D[] = []; 27 | visibleNodes: PointCloudOctreeNode[] = []; 28 | visibleGeometry: PointCloudOctreeGeometryNode[] = []; 29 | numVisiblePoints: number = 0; 30 | showBoundingBox: boolean = false; 31 | private visibleBounds: Box3 = new Box3(); 32 | private picker: PointCloudOctreePicker | undefined; 33 | 34 | constructor( 35 | potree: IPotree, 36 | pcoGeometry: PCOGeometry, 37 | material?: PointCloudMaterial, 38 | ) { 39 | super(); 40 | 41 | this.name = ''; 42 | this.potree = potree; 43 | this.root = pcoGeometry.root; 44 | this.pcoGeometry = pcoGeometry; 45 | this.boundingBox = pcoGeometry.boundingBox; 46 | this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere()); 47 | 48 | this.position.copy(pcoGeometry.offset); 49 | this.updateMatrix(); 50 | 51 | this.material = material || (pcoGeometry instanceof OctreeGeometry) ? new PointCloudMaterial({newFormat: true}) : new PointCloudMaterial(); 52 | this.initMaterial(this.material); 53 | } 54 | 55 | private initMaterial(material: PointCloudMaterial): void { 56 | this.updateMatrixWorld(true); 57 | 58 | const { min, max } = computeTransformedBoundingBox( 59 | this.pcoGeometry.tightBoundingBox || this.getBoundingBoxWorld(), 60 | this.matrixWorld, 61 | ); 62 | 63 | const bWidth = max.z - min.z; 64 | material.heightMin = min.z - 0.2 * bWidth; 65 | material.heightMax = max.z + 0.2 * bWidth; 66 | } 67 | 68 | dispose(): void { 69 | if (this.root) { 70 | this.root.dispose(); 71 | } 72 | 73 | this.pcoGeometry.root.traverse((n:IPointCloudTreeNode) => this.potree.lru.remove(n)); 74 | this.pcoGeometry.dispose(); 75 | this.material.dispose(); 76 | 77 | this.visibleNodes = []; 78 | this.visibleGeometry = []; 79 | 80 | if (this.picker) { 81 | this.picker.dispose(); 82 | this.picker = undefined; 83 | } 84 | 85 | this.disposed = true; 86 | } 87 | 88 | get pointSizeType(): PointSizeType { 89 | return this.material.pointSizeType; 90 | } 91 | 92 | set pointSizeType(value: PointSizeType) { 93 | this.material.pointSizeType = value; 94 | } 95 | 96 | toTreeNode( 97 | geometryNode: PointCloudOctreeGeometryNode, 98 | parent?: PointCloudOctreeNode | null, 99 | ): PointCloudOctreeNode { 100 | const points = new Points(geometryNode.geometry, this.material); 101 | const node = new PointCloudOctreeNode(geometryNode, points); 102 | points.name = geometryNode.name; 103 | points.position.copy(geometryNode.boundingBox.min); 104 | points.frustumCulled = false; 105 | points.onBeforeRender = PointCloudMaterial.makeOnBeforeRender(this, node); 106 | 107 | if (parent) { 108 | parent.sceneNode.add(points); 109 | parent.children[geometryNode.index] = node; 110 | 111 | geometryNode.oneTimeDisposeHandlers.push(() => { 112 | node.disposeSceneNode(); 113 | parent.sceneNode.remove(node.sceneNode); 114 | // Replace the tree node (rendered and in the GPU) with the geometry node. 115 | parent.children[geometryNode.index] = geometryNode; 116 | }); 117 | } else { 118 | this.root = node; 119 | this.add(points); 120 | } 121 | 122 | return node; 123 | } 124 | 125 | updateVisibleBounds() { 126 | const bounds = this.visibleBounds; 127 | bounds.min.set(Infinity, Infinity, Infinity); 128 | bounds.max.set(-Infinity, -Infinity, -Infinity); 129 | 130 | for (const node of this.visibleNodes) { 131 | if (node.isLeafNode) { 132 | bounds.expandByPoint(node.boundingBox.min); 133 | bounds.expandByPoint(node.boundingBox.max); 134 | } 135 | } 136 | } 137 | 138 | updateBoundingBoxes(): void { 139 | if (!this.showBoundingBox || !this.parent) { 140 | return; 141 | } 142 | // Above: If we're not showing the bounding box or we don't have a parent, we can't update it. 143 | 144 | let bbRoot: any = this.parent.getObjectByName('bbroot'); 145 | if (!bbRoot) { 146 | bbRoot = new Object3D(); 147 | bbRoot.name = 'bbroot'; 148 | this.parent.add(bbRoot); 149 | } 150 | // Above: If we don't have a root object, we need to create one. 151 | 152 | const visibleBoxes: (Object3D | null)[] = []; 153 | for (const node of this.visibleNodes) { 154 | if (node.boundingBoxNode !== undefined && node.isLeafNode) { 155 | visibleBoxes.push(node.boundingBoxNode); 156 | } 157 | } 158 | 159 | bbRoot.children = visibleBoxes; 160 | } 161 | 162 | updateMatrixWorld(force: boolean): void { 163 | if (this.matrixAutoUpdate === true) { 164 | this.updateMatrix(); 165 | } 166 | 167 | if (this.matrixWorldNeedsUpdate === true || force === true) { 168 | if (!this.parent) { 169 | this.matrixWorld.copy(this.matrix); 170 | } else { 171 | this.matrixWorld.multiplyMatrices(this.parent.matrixWorld, this.matrix); 172 | } 173 | 174 | this.matrixWorldNeedsUpdate = false; 175 | 176 | force = true; 177 | } 178 | } 179 | 180 | hideDescendants(object: Object3D): void { 181 | const toHide: Object3D[] = []; 182 | addVisibleChildren(object); 183 | 184 | while (toHide.length > 0) { 185 | const objToHide = toHide.shift()!; 186 | objToHide.visible = false; 187 | addVisibleChildren(objToHide); 188 | } 189 | 190 | function addVisibleChildren(obj: Object3D) { 191 | for (const child of obj.children) { 192 | if (child.visible) { 193 | toHide.push(child); 194 | } 195 | } 196 | } 197 | } 198 | 199 | moveToOrigin(): void { 200 | this.position.set(0, 0, 0); // Reset, then the matrix will be updated in getBoundingBoxWorld() 201 | this.position.set(0, 0, 0).sub(this.getBoundingBoxWorld().getCenter(new Vector3())); 202 | } 203 | 204 | moveToGroundPlane(): void { 205 | this.position.y += -this.getBoundingBoxWorld().min.y; 206 | } 207 | 208 | getBoundingBoxWorld(): Box3 { 209 | this.updateMatrixWorld(true); 210 | return computeTransformedBoundingBox(this.boundingBox, this.matrixWorld); 211 | } 212 | 213 | getVisibleExtent() { 214 | return this.visibleBounds.applyMatrix4(this.matrixWorld); 215 | } 216 | 217 | pick( 218 | renderer: WebGLRenderer, 219 | camera: Camera, 220 | ray: Ray, 221 | params: Partial = {}, 222 | ): PickPoint | null { 223 | this.picker = this.picker || new PointCloudOctreePicker(); 224 | return this.picker.pick(renderer, camera, ray, [this], params); 225 | } 226 | 227 | get progress() { 228 | return this.visibleGeometry.length === 0 229 | ? 0 230 | : this.visibleNodes.length / this.visibleGeometry.length; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/point-cloud-tree.ts: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three'; 2 | import { IPointCloudTreeNode } from './types'; 3 | 4 | export class PointCloudTree extends Object3D { 5 | root: IPointCloudTreeNode | null = null; 6 | 7 | initialized() { 8 | return this.root !== null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/potree.ts: -------------------------------------------------------------------------------- 1 | import { OctreeGeometry } from './loading2/OctreeGeometry'; 2 | import { loadOctree } from './loading2/load-octree'; 3 | import { 4 | Box3, 5 | Camera, 6 | Frustum, 7 | Matrix4, 8 | OrthographicCamera, 9 | PerspectiveCamera, 10 | Ray, 11 | Vector2, 12 | Vector3, 13 | WebGLRenderer, 14 | } from 'three'; 15 | import { 16 | DEFAULT_POINT_BUDGET, 17 | MAX_LOADS_TO_GPU, 18 | MAX_NUM_NODES_LOADING, 19 | PERSPECTIVE_CAMERA, 20 | } from './constants'; 21 | import { FEATURES } from './features'; 22 | import { GetUrlFn, loadPOC } from './loading'; 23 | import { ClipMode } from './materials'; 24 | import { PointCloudOctree } from './point-cloud-octree'; 25 | import { PointCloudOctreeGeometryNode } from './point-cloud-octree-geometry-node'; 26 | import { PointCloudOctreeNode } from './point-cloud-octree-node'; 27 | import { PickParams, PointCloudOctreePicker } from './point-cloud-octree-picker'; 28 | import { isGeometryNode, isTreeNode } from './type-predicates'; 29 | import { IPointCloudTreeNode, IPotree, IVisibilityUpdateResult, PickPoint } from './types'; 30 | import { BinaryHeap } from './utils/binary-heap'; 31 | import { Box3Helper } from './utils/box3-helper'; 32 | import { LRU } from './utils/lru'; 33 | 34 | 35 | export class QueueItem { 36 | constructor( 37 | public pointCloudIndex: number, 38 | public weight: number, 39 | public node: IPointCloudTreeNode, 40 | public parent?: IPointCloudTreeNode | null, 41 | ) {} 42 | } 43 | 44 | export class Potree implements IPotree { 45 | private static picker: PointCloudOctreePicker | undefined; 46 | private _pointBudget: number = DEFAULT_POINT_BUDGET; 47 | private _rendererSize: Vector2 = new Vector2(); 48 | 49 | maxNumNodesLoading: number = MAX_NUM_NODES_LOADING; 50 | features = FEATURES; 51 | lru = new LRU(this._pointBudget); 52 | 53 | async loadPointCloud( 54 | url: string, 55 | getUrl: GetUrlFn, 56 | xhrRequest = (input: RequestInfo, init?: RequestInit) => fetch(input, init), 57 | ): Promise { 58 | if (url === "cloud.js") { 59 | return await loadPOC(url, getUrl, xhrRequest).then(geometry => new PointCloudOctree(this, geometry)); 60 | } else if (url === "metadata.json") { 61 | // throw new Error("Not implemented") 62 | return await loadOctree(url, getUrl, xhrRequest).then((geometry:OctreeGeometry) => new PointCloudOctree(this, geometry)); 63 | } 64 | throw new Error("Unsupported file type"); 65 | } 66 | 67 | updatePointClouds( 68 | pointClouds: PointCloudOctree[], 69 | camera: Camera, 70 | renderer: WebGLRenderer, 71 | ): IVisibilityUpdateResult { 72 | const result = this.updateVisibility(pointClouds, camera, renderer); 73 | 74 | for (let i = 0; i < pointClouds.length; i++) { 75 | const pointCloud = pointClouds[i]; 76 | if (pointCloud.disposed) { 77 | continue; 78 | } 79 | 80 | pointCloud.material.updateMaterial(pointCloud, pointCloud.visibleNodes, camera, renderer); 81 | pointCloud.updateVisibleBounds(); 82 | pointCloud.updateBoundingBoxes(); 83 | } 84 | 85 | this.lru.freeMemory(); 86 | 87 | return result; 88 | } 89 | 90 | static pick( 91 | pointClouds: PointCloudOctree[], 92 | renderer: WebGLRenderer, 93 | camera: Camera, 94 | ray: Ray, 95 | params: Partial = {}, 96 | ): PickPoint | null { 97 | Potree.picker = Potree.picker || new PointCloudOctreePicker(); 98 | return Potree.picker.pick(renderer, camera, ray, pointClouds, params); 99 | } 100 | 101 | get pointBudget(): number { 102 | return this._pointBudget; 103 | } 104 | 105 | set pointBudget(value: number) { 106 | if (value !== this._pointBudget) { 107 | this._pointBudget = value; 108 | this.lru.pointBudget = value; 109 | this.lru.freeMemory(); 110 | } 111 | } 112 | 113 | private updateVisibility( 114 | pointClouds: PointCloudOctree[], 115 | camera: Camera, 116 | renderer: WebGLRenderer, 117 | ): IVisibilityUpdateResult { 118 | let numVisiblePoints = 0; 119 | 120 | const visibleNodes: PointCloudOctreeNode[] = []; 121 | const unloadedGeometry: PointCloudOctreeGeometryNode[] = []; 122 | 123 | // calculate object space frustum and cam pos and setup priority queue 124 | const { frustums, cameraPositions, priorityQueue } = this.updateVisibilityStructures( 125 | pointClouds, 126 | camera, 127 | ); 128 | 129 | let loadedToGPUThisFrame = 0; 130 | let exceededMaxLoadsToGPU = false; 131 | let nodeLoadFailed = false; 132 | let queueItem: QueueItem | undefined; 133 | 134 | while ((queueItem = priorityQueue.pop()) !== undefined) { 135 | let node = queueItem.node; 136 | 137 | // If we will end up with too many points, we stop right away. 138 | if (numVisiblePoints + node.numPoints > this.pointBudget) { 139 | break; 140 | } 141 | 142 | const pointCloudIndex = queueItem.pointCloudIndex; 143 | const pointCloud = pointClouds[pointCloudIndex]; 144 | 145 | const maxLevel = pointCloud.maxLevel !== undefined ? pointCloud.maxLevel : Infinity; 146 | 147 | if ( 148 | node.level > maxLevel || 149 | !frustums[pointCloudIndex].intersectsBox(node.boundingBox) || 150 | this.shouldClip(pointCloud, node.boundingBox) 151 | ) { 152 | continue; 153 | } 154 | 155 | numVisiblePoints += node.numPoints; 156 | pointCloud.numVisiblePoints += node.numPoints; 157 | 158 | const parentNode = queueItem.parent; 159 | 160 | if (isGeometryNode(node) && (!parentNode || isTreeNode(parentNode))) { 161 | if (node.loaded && loadedToGPUThisFrame < MAX_LOADS_TO_GPU) { 162 | node = pointCloud.toTreeNode(node, parentNode); 163 | loadedToGPUThisFrame++; 164 | } else if (!node.failed) { 165 | if (node.loaded && loadedToGPUThisFrame >= MAX_LOADS_TO_GPU) { 166 | exceededMaxLoadsToGPU = true; 167 | } 168 | unloadedGeometry.push(node); 169 | pointCloud.visibleGeometry.push(node); 170 | } else { 171 | nodeLoadFailed = true; 172 | continue; 173 | } 174 | } 175 | 176 | if (isTreeNode(node)) { 177 | this.updateTreeNodeVisibility(pointCloud, node, visibleNodes); 178 | pointCloud.visibleGeometry.push(node.geometryNode); 179 | } 180 | 181 | const halfHeight = 182 | 0.5 * renderer.getSize(this._rendererSize).height * renderer.getPixelRatio(); 183 | 184 | this.updateChildVisibility( 185 | queueItem, 186 | priorityQueue, 187 | pointCloud, 188 | node, 189 | cameraPositions[pointCloudIndex], 190 | camera, 191 | halfHeight, 192 | ); 193 | } // end priority queue loop 194 | 195 | const numNodesToLoad = Math.min(this.maxNumNodesLoading, unloadedGeometry.length); 196 | const nodeLoadPromises: Promise[] = []; 197 | for (let i = 0; i < numNodesToLoad; i++) { 198 | nodeLoadPromises.push(unloadedGeometry[i].load()); 199 | } 200 | 201 | return { 202 | visibleNodes: visibleNodes, 203 | numVisiblePoints: numVisiblePoints, 204 | exceededMaxLoadsToGPU: exceededMaxLoadsToGPU, 205 | nodeLoadFailed: nodeLoadFailed, 206 | nodeLoadPromises: nodeLoadPromises, 207 | }; 208 | } 209 | 210 | private updateTreeNodeVisibility( 211 | pointCloud: PointCloudOctree, 212 | node: PointCloudOctreeNode, 213 | visibleNodes: IPointCloudTreeNode[], 214 | ): void { 215 | this.lru.touch(node.geometryNode); 216 | 217 | const sceneNode = node.sceneNode; 218 | sceneNode.visible = true; 219 | sceneNode.material = pointCloud.material; 220 | sceneNode.updateMatrix(); 221 | sceneNode.matrixWorld.multiplyMatrices(pointCloud.matrixWorld, sceneNode.matrix); 222 | 223 | visibleNodes.push(node); 224 | pointCloud.visibleNodes.push(node); 225 | 226 | this.updateBoundingBoxVisibility(pointCloud, node); 227 | } 228 | 229 | private updateChildVisibility( 230 | queueItem: QueueItem, 231 | priorityQueue: BinaryHeap, 232 | pointCloud: PointCloudOctree, 233 | node: IPointCloudTreeNode, 234 | cameraPosition: Vector3, 235 | camera: Camera, 236 | halfHeight: number, 237 | ): void { 238 | const children = node.children; 239 | for (let i = 0; i < children.length; i++) { 240 | const child = children[i]; 241 | if (child === null) { 242 | continue; 243 | } 244 | 245 | const sphere = child.boundingSphere; 246 | const distance = sphere.center.distanceTo(cameraPosition); 247 | const radius = sphere.radius; 248 | 249 | let projectionFactor = 0.0; 250 | 251 | if (camera.type === PERSPECTIVE_CAMERA) { 252 | const perspective = camera as PerspectiveCamera; 253 | const fov = (perspective.fov * Math.PI) / 180.0; 254 | const slope = Math.tan(fov / 2.0); 255 | projectionFactor = halfHeight / (slope * distance); 256 | } else { 257 | const orthographic = camera as OrthographicCamera; 258 | projectionFactor = (2 * halfHeight) / (orthographic.top - orthographic.bottom); 259 | } 260 | 261 | const screenPixelRadius = radius * projectionFactor; 262 | 263 | // Don't add the node if it'll be too small on the screen. 264 | if (screenPixelRadius < pointCloud.minNodePixelSize) { 265 | continue; 266 | } 267 | 268 | // Nodes which are larger will have priority in loading/displaying. 269 | const weight = distance < radius ? Number.MAX_VALUE : screenPixelRadius + 1 / distance; 270 | 271 | priorityQueue.push(new QueueItem(queueItem.pointCloudIndex, weight, child, node)); 272 | } 273 | } 274 | 275 | private updateBoundingBoxVisibility( 276 | pointCloud: PointCloudOctree, 277 | node: PointCloudOctreeNode, 278 | ): void { 279 | if (pointCloud.showBoundingBox && !node.boundingBoxNode) { 280 | const boxHelper = new Box3Helper(node.boundingBox); 281 | boxHelper.matrixAutoUpdate = false; 282 | pointCloud.boundingBoxNodes.push(boxHelper); 283 | node.boundingBoxNode = boxHelper; 284 | node.boundingBoxNode.matrix.copy(pointCloud.matrixWorld); 285 | } else if (pointCloud.showBoundingBox && node.boundingBoxNode) { 286 | node.boundingBoxNode.visible = true; 287 | node.boundingBoxNode.matrix.copy(pointCloud.matrixWorld); 288 | } else if (!pointCloud.showBoundingBox && node.boundingBoxNode) { 289 | node.boundingBoxNode.visible = false; 290 | } 291 | } 292 | 293 | private shouldClip(pointCloud: PointCloudOctree, boundingBox: Box3): boolean { 294 | const material = pointCloud.material; 295 | 296 | if (material.numClipBoxes === 0 || material.clipMode !== ClipMode.CLIP_OUTSIDE) { 297 | return false; 298 | } 299 | 300 | const box2 = boundingBox.clone(); 301 | pointCloud.updateMatrixWorld(true); 302 | box2.applyMatrix4(pointCloud.matrixWorld); 303 | 304 | const clipBoxes = material.clipBoxes; 305 | for (let i = 0; i < clipBoxes.length; i++) { 306 | const clipMatrixWorld = clipBoxes[i].matrix; 307 | const clipBoxWorld = new Box3( 308 | new Vector3(-0.5, -0.5, -0.5), 309 | new Vector3(0.5, 0.5, 0.5), 310 | ).applyMatrix4(clipMatrixWorld); 311 | if (box2.intersectsBox(clipBoxWorld)) { 312 | return false; 313 | } 314 | } 315 | 316 | return true; 317 | } 318 | 319 | private updateVisibilityStructures = (() => { 320 | const frustumMatrix = new Matrix4(); 321 | const inverseWorldMatrix = new Matrix4(); 322 | const cameraMatrix = new Matrix4(); 323 | 324 | return ( 325 | pointClouds: PointCloudOctree[], 326 | camera: Camera, 327 | ): { 328 | frustums: Frustum[]; 329 | cameraPositions: Vector3[]; 330 | priorityQueue: BinaryHeap; 331 | } => { 332 | const frustums: Frustum[] = []; 333 | const cameraPositions: Vector3[] = []; 334 | const priorityQueue = new BinaryHeap(x => 1 / x.weight); 335 | 336 | for (let i = 0; i < pointClouds.length; i++) { 337 | const pointCloud = pointClouds[i]; 338 | 339 | if (!pointCloud.initialized()) { 340 | continue; 341 | } 342 | 343 | pointCloud.numVisiblePoints = 0; 344 | pointCloud.visibleNodes = []; 345 | pointCloud.visibleGeometry = []; 346 | 347 | camera.updateMatrixWorld(false); 348 | 349 | // Furstum in object space. 350 | const inverseViewMatrix = camera.matrixWorldInverse; 351 | const worldMatrix = pointCloud.matrixWorld; 352 | frustumMatrix 353 | .identity() 354 | .multiply(camera.projectionMatrix) 355 | .multiply(inverseViewMatrix) 356 | .multiply(worldMatrix); 357 | frustums.push(new Frustum().setFromProjectionMatrix(frustumMatrix)); 358 | 359 | // Camera position in object space 360 | inverseWorldMatrix.copy(worldMatrix).invert(); 361 | cameraMatrix 362 | .identity() 363 | .multiply(inverseWorldMatrix) 364 | .multiply(camera.matrixWorld); 365 | cameraPositions.push(new Vector3().setFromMatrixPosition(cameraMatrix)); 366 | 367 | if (pointCloud.visible && pointCloud.root !== null) { 368 | const weight = Number.MAX_VALUE; 369 | priorityQueue.push(new QueueItem(i, weight, pointCloud.root)); 370 | } 371 | 372 | // Hide any previously visible nodes. We will later show only the needed ones. 373 | if (isTreeNode(pointCloud.root)) { 374 | pointCloud.hideDescendants(pointCloud?.root?.sceneNode); 375 | } 376 | 377 | for (const boundingBoxNode of pointCloud.boundingBoxNodes) { 378 | boundingBoxNode.visible = false; 379 | } 380 | } 381 | 382 | return { frustums, cameraPositions, priorityQueue }; 383 | }; 384 | })(); 385 | } 386 | -------------------------------------------------------------------------------- /src/type-predicates.ts: -------------------------------------------------------------------------------- 1 | import { PointCloudOctreeGeometryNode } from './point-cloud-octree-geometry-node'; 2 | // import { PointCloudOctreeNode } from './point-cloud-octree-node'; 3 | 4 | export function isGeometryNode(node?: any): node is PointCloudOctreeGeometryNode { // "node is" in this case 5 | return node !== undefined && node !== null && node.isGeometryNode; 6 | } 7 | 8 | // export function isTreeNode(node?: any): node is PointCloudOctreeNode { // Problem is here! Pnext modified isTreeNode to be one function when it's normally attached as a method to the root node. 9 | // return node !== undefined && node !== null && node.isTreeNode; 10 | // } 11 | 12 | export function isTreeNode(node?: any) { 13 | return node !== undefined && node !== null && node.isTreeNode; 14 | } 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { OctreeGeometry } from './loading2/OctreeGeometry'; 2 | import { PointCloudOctreeGeometry } from "./point-cloud-octree-geometry"; 3 | import { Box3, Camera, Sphere, Vector3, WebGLRenderer } from 'three'; 4 | import { GetUrlFn, XhrRequest } from './loading/types'; 5 | import { PointCloudOctree } from './point-cloud-octree'; 6 | import { LRU } from './utils/lru'; 7 | 8 | export interface IPointCloudTreeNode { 9 | id: number; 10 | name: string; 11 | level: number; 12 | index: number; 13 | spacing: number; 14 | boundingBox: Box3; 15 | boundingSphere: Sphere; 16 | loaded: boolean; 17 | numPoints: number; 18 | readonly children: ReadonlyArray; 19 | readonly isLeafNode: boolean; 20 | // This probably needs isTreeNode and isGeometryNode as readonly properties too? 21 | 22 | dispose(): void; 23 | 24 | traverse(cb: (node: IPointCloudTreeNode) => void, includeSelf?: boolean): void; 25 | } 26 | 27 | export interface IVisibilityUpdateResult { 28 | visibleNodes: IPointCloudTreeNode[]; 29 | numVisiblePoints: number; 30 | /** 31 | * True when a node has been loaded but was not added to the scene yet. 32 | * Make sure to call updatePointClouds() again on the next frame. 33 | */ 34 | exceededMaxLoadsToGPU: boolean; 35 | /** 36 | * True when at least one node in view has failed to load. 37 | */ 38 | nodeLoadFailed: boolean; 39 | /** 40 | * Promises for loading nodes, will reject when loading fails. 41 | */ 42 | nodeLoadPromises: Promise[]; 43 | } 44 | 45 | export interface IPotree { 46 | pointBudget: number; 47 | maxNumNodesLoading: number; 48 | lru: LRU; 49 | 50 | loadPointCloud(url: string, getUrl: GetUrlFn, xhrRequest?: XhrRequest): Promise; 51 | 52 | updatePointClouds( 53 | pointClouds: PointCloudOctree[], 54 | camera: Camera, 55 | renderer: WebGLRenderer, 56 | ): IVisibilityUpdateResult; 57 | } 58 | 59 | export interface PickPoint { 60 | position?: Vector3; 61 | normal?: Vector3; 62 | pointCloud?: PointCloudOctree; 63 | [property: string]: any; 64 | } 65 | 66 | export interface PointCloudHit { 67 | pIndex: number; 68 | pcIndex: number; 69 | } 70 | 71 | export type PCOGeometry = PointCloudOctreeGeometry | OctreeGeometry; 72 | -------------------------------------------------------------------------------- /src/utils/binary-heap.d.ts: -------------------------------------------------------------------------------- 1 | export class BinaryHeap { 2 | constructor(scoreFunction: (node: T) => number); 3 | push(node: T): void; 4 | pop(): T | undefined; 5 | remove(node: T): void; 6 | size(): number; 7 | bubbleUp(n: number): void; 8 | sinkDown(n: number): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/binary-heap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * from: http://eloquentjavascript.net/1st_edition/appendix2.html 3 | * 4 | */ 5 | 6 | export function BinaryHeap(scoreFunction) { 7 | this.content = []; 8 | this.scoreFunction = scoreFunction; 9 | } 10 | 11 | BinaryHeap.prototype = { 12 | push: function(element) { 13 | // Add the new element to the end of the array. 14 | this.content.push(element); 15 | // Allow it to bubble up. 16 | this.bubbleUp(this.content.length - 1); 17 | }, 18 | 19 | pop: function() { 20 | // Store the first element so we can return it later. 21 | var result = this.content[0]; 22 | // Get the element at the end of the array. 23 | var end = this.content.pop(); 24 | // If there are any elements left, put the end element at the 25 | // start, and let it sink down. 26 | if (this.content.length > 0) { 27 | this.content[0] = end; 28 | this.sinkDown(0); 29 | } 30 | return result; 31 | }, 32 | 33 | remove: function(node) { 34 | var length = this.content.length; 35 | // To remove a value, we must search through the array to find 36 | // it. 37 | for (var i = 0; i < length; i++) { 38 | if (this.content[i] != node) continue; 39 | // When it is found, the process seen in 'pop' is repeated 40 | // to fill up the hole. 41 | var end = this.content.pop(); 42 | // If the element we popped was the one we needed to remove, 43 | // we're done. 44 | if (i == length - 1) break; 45 | // Otherwise, we replace the removed element with the popped 46 | // one, and allow it to float up or sink down as appropriate. 47 | this.content[i] = end; 48 | this.bubbleUp(i); 49 | this.sinkDown(i); 50 | break; 51 | } 52 | }, 53 | 54 | size: function() { 55 | return this.content.length; 56 | }, 57 | 58 | bubbleUp: function(n) { 59 | // Fetch the element that has to be moved. 60 | var element = this.content[n], 61 | score = this.scoreFunction(element); 62 | // When at 0, an element can not go up any further. 63 | while (n > 0) { 64 | // Compute the parent element's index, and fetch it. 65 | var parentN = Math.floor((n + 1) / 2) - 1, 66 | parent = this.content[parentN]; 67 | // If the parent has a lesser score, things are in order and we 68 | // are done. 69 | if (score >= this.scoreFunction(parent)) break; 70 | 71 | // Otherwise, swap the parent with the current element and 72 | // continue. 73 | this.content[parentN] = element; 74 | this.content[n] = parent; 75 | n = parentN; 76 | } 77 | }, 78 | 79 | sinkDown: function(n) { 80 | // Look up the target element and its score. 81 | var length = this.content.length, 82 | element = this.content[n], 83 | elemScore = this.scoreFunction(element); 84 | 85 | while (true) { 86 | // Compute the indices of the child elements. 87 | var child2N = (n + 1) * 2, 88 | child1N = child2N - 1; 89 | // This is used to store the new position of the element, 90 | // if any. 91 | var swap = null; 92 | // If the first child exists (is inside the array)... 93 | if (child1N < length) { 94 | // Look it up and compute its score. 95 | var child1 = this.content[child1N], 96 | child1Score = this.scoreFunction(child1); 97 | // If the score is less than our element's, we need to swap. 98 | if (child1Score < elemScore) swap = child1N; 99 | } 100 | // Do the same checks for the other child. 101 | if (child2N < length) { 102 | var child2 = this.content[child2N], 103 | child2Score = this.scoreFunction(child2); 104 | if (child2Score < (swap == null ? elemScore : child1Score)) swap = child2N; 105 | } 106 | 107 | // No need to swap further, we are done. 108 | if (swap == null) break; 109 | 110 | // Otherwise, swap and continue. 111 | this.content[n] = this.content[swap]; 112 | this.content[swap] = element; 113 | n = swap; 114 | } 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /src/utils/bounds.ts: -------------------------------------------------------------------------------- 1 | import { Box3, Matrix4, Vector3 } from 'three'; 2 | 3 | /** 4 | * adapted from mhluska at https://github.com/mrdoob/three.js/issues/1561 5 | */ 6 | export function computeTransformedBoundingBox(box: Box3, transform: Matrix4): Box3 { 7 | return new Box3().setFromPoints([ 8 | new Vector3(box.min.x, box.min.y, box.min.z).applyMatrix4(transform), 9 | new Vector3(box.min.x, box.min.y, box.min.z).applyMatrix4(transform), 10 | new Vector3(box.max.x, box.min.y, box.min.z).applyMatrix4(transform), 11 | new Vector3(box.min.x, box.max.y, box.min.z).applyMatrix4(transform), 12 | new Vector3(box.min.x, box.min.y, box.max.z).applyMatrix4(transform), 13 | new Vector3(box.min.x, box.max.y, box.max.z).applyMatrix4(transform), 14 | new Vector3(box.max.x, box.max.y, box.min.z).applyMatrix4(transform), 15 | new Vector3(box.max.x, box.min.y, box.max.z).applyMatrix4(transform), 16 | new Vector3(box.max.x, box.max.y, box.max.z).applyMatrix4(transform), 17 | ]); 18 | } 19 | 20 | export function createChildAABB(aabb: Box3, index: number): Box3 { 21 | const min = aabb.min.clone(); 22 | const max = aabb.max.clone(); 23 | const size = new Vector3().subVectors(max, min); 24 | 25 | // tslint:disable-next-line:no-bitwise 26 | if ((index & 0b0001) > 0) { 27 | min.z += size.z / 2; 28 | } else { 29 | max.z -= size.z / 2; 30 | } 31 | 32 | // tslint:disable-next-line:no-bitwise 33 | if ((index & 0b0010) > 0) { 34 | min.y += size.y / 2; 35 | } else { 36 | max.y -= size.y / 2; 37 | } 38 | 39 | // tslint:disable-next-line:no-bitwise 40 | if ((index & 0b0100) > 0) { 41 | min.x += size.x / 2; 42 | } else { 43 | max.x -= size.x / 2; 44 | } 45 | 46 | return new Box3(min, max); 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/box3-helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Box3, 3 | BufferAttribute, 4 | BufferGeometry, 5 | Color, 6 | LineBasicMaterial, 7 | LineSegments, 8 | } from 'three'; 9 | 10 | /** 11 | * 12 | * code adapted from three.js BoxHelper.js 13 | * https://github.com/mrdoob/three.js/blob/dev/src/helpers/BoxHelper.js 14 | * 15 | * @author mrdoob / http://mrdoob.com/ 16 | * @author Mugen87 / http://github.com/Mugen87 17 | * @author mschuetz / http://potree.org 18 | */ 19 | 20 | export class Box3Helper extends LineSegments { 21 | constructor(box: Box3, color: Color = new Color(0xffff00)) { 22 | // prettier-ignore 23 | const indices = new Uint16Array([ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ]); 24 | // prettier-ignore 25 | const positions = new Float32Array([ 26 | box.min.x, box.min.y, box.min.z, 27 | box.max.x, box.min.y, box.min.z, 28 | box.max.x, box.min.y, box.max.z, 29 | box.min.x, box.min.y, box.max.z, 30 | box.min.x, box.max.y, box.min.z, 31 | box.max.x, box.max.y, box.min.z, 32 | box.max.x, box.max.y, box.max.z, 33 | box.min.x, box.max.y, box.max.z 34 | ]); 35 | 36 | const geometry = new BufferGeometry(); 37 | geometry.setIndex(new BufferAttribute(indices, 1)); 38 | geometry.setAttribute('position', new BufferAttribute(positions, 3)); 39 | 40 | const material = new LineBasicMaterial({ color: color }); 41 | 42 | super(geometry, material); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/lru.ts: -------------------------------------------------------------------------------- 1 | import { IPointCloudTreeNode } from '../types'; 2 | 3 | export type Node = IPointCloudTreeNode; 4 | 5 | export class LRUItem { 6 | next: LRUItem | null = null; 7 | previous: LRUItem | null = null; 8 | constructor(public node: Node) {} 9 | } 10 | 11 | /** 12 | * A doubly-linked-list of the least recently used elements. 13 | */ 14 | export class LRU { 15 | // the least recently used item 16 | first: LRUItem | null = null; 17 | // the most recently used item 18 | last: LRUItem | null = null; 19 | numPoints: number = 0; 20 | 21 | private items = new Map(); 22 | 23 | constructor(public pointBudget: number = 1_000_000) {} 24 | 25 | get size(): number { 26 | return this.items.size; 27 | } 28 | 29 | has(node: Node): boolean { 30 | return this.items.has(node.id); 31 | } 32 | 33 | /** 34 | * Makes the specified the most recently used item. if the list does not contain node, it will 35 | * be added. 36 | */ 37 | touch(node: Node) { 38 | if (!node.loaded) { 39 | return; 40 | } 41 | 42 | const item = this.items.get(node.id); 43 | if (item) { 44 | this.touchExisting(item); 45 | } else { 46 | this.addNew(node); 47 | } 48 | } 49 | 50 | private addNew(node: Node): void { 51 | const item = new LRUItem(node); 52 | item.previous = this.last; 53 | this.last = item; 54 | if (item.previous) { 55 | item.previous.next = item; 56 | } 57 | 58 | if (!this.first) { 59 | this.first = item; 60 | } 61 | 62 | this.items.set(node.id, item); 63 | this.numPoints += node.numPoints; 64 | } 65 | 66 | private touchExisting(item: LRUItem): void { 67 | if (!item.previous) { 68 | // handle touch on first element 69 | if (item.next) { 70 | this.first = item.next; 71 | this.first.previous = null; 72 | item.previous = this.last; 73 | item.next = null; 74 | this.last = item; 75 | 76 | if (item.previous) { 77 | item.previous.next = item; 78 | } 79 | } 80 | } else if (!item.next) { 81 | // handle touch on last element 82 | } else { 83 | // handle touch on any other element 84 | item.previous.next = item.next; 85 | item.next.previous = item.previous; 86 | item.previous = this.last; 87 | item.next = null; 88 | this.last = item; 89 | 90 | if (item.previous) { 91 | item.previous.next = item; 92 | } 93 | } 94 | } 95 | 96 | remove(node: Node) { 97 | const item = this.items.get(node.id); 98 | if (!item) { 99 | return; 100 | } 101 | 102 | if (this.items.size === 1) { 103 | this.first = null; 104 | this.last = null; 105 | } else { 106 | if (!item.previous) { 107 | this.first = item.next; 108 | this.first!.previous = null; 109 | } 110 | 111 | if (!item.next) { 112 | this.last = item.previous; 113 | this.last!.next = null; 114 | } 115 | 116 | if (item.previous && item.next) { 117 | item.previous.next = item.next; 118 | item.next.previous = item.previous; 119 | } 120 | } 121 | 122 | this.items.delete(node.id); 123 | this.numPoints -= node.numPoints; 124 | } 125 | 126 | getLRUItem(): Node | undefined { 127 | return this.first ? this.first.node : undefined; 128 | } 129 | 130 | freeMemory(): void { 131 | if (this.items.size <= 1) { 132 | return; 133 | } 134 | 135 | while (this.numPoints > this.pointBudget * 2) { 136 | const node = this.getLRUItem(); 137 | if (node) { 138 | this.disposeSubtree(node); 139 | } 140 | } 141 | } 142 | 143 | disposeSubtree(node: Node): void { 144 | // Collect all the nodes which are to be disposed and removed. 145 | const nodesToDispose: Node[] = [node]; 146 | node.traverse(n => { 147 | if (n.loaded) { 148 | nodesToDispose.push(n); 149 | } 150 | }); 151 | 152 | // Dispose of all the nodes in one go. 153 | for (const n of nodesToDispose) { 154 | n.dispose(); 155 | this.remove(n); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | export function clamp(value: number, min: number, max: number): number { 2 | return Math.min(Math.max(min, value), max); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { IPointCloudTreeNode } from '../types'; 2 | 3 | export function getIndexFromName(name: string) { 4 | return parseInt(name.charAt(name.length - 1), 10); 5 | } 6 | 7 | /** 8 | * When passed to `[].sort`, sorts the array by level and index: r, r0, r3, r4, r01, r07, r30, ... 9 | */ 10 | export function byLevelAndIndex(a: IPointCloudTreeNode, b: IPointCloudTreeNode) { 11 | const na = a.name; 12 | const nb = b.name; 13 | if (na.length !== nb.length) { 14 | return na.length - nb.length; 15 | } else if (na < nb) { 16 | return -1; 17 | } else if (na > nb) { 18 | return 1; 19 | } else { 20 | return 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export class Version { 2 | version: string; 3 | versionMajor: number; 4 | versionMinor: number = 0; 5 | 6 | constructor(version: string) { 7 | this.version = version; 8 | 9 | const vmLength = version.indexOf('.') === -1 ? version.length : version.indexOf('.'); 10 | this.versionMajor = parseInt(version.substr(0, vmLength), 10); 11 | this.versionMinor = parseInt(version.substr(vmLength + 1), 10); 12 | if (isNaN(this.versionMinor)) { 13 | this.versionMinor = 0; 14 | } 15 | } 16 | 17 | newerThan(version: string): boolean { 18 | const v = new Version(version); 19 | 20 | if (this.versionMajor > v.versionMajor) { 21 | return true; 22 | } else if (this.versionMajor === v.versionMajor && this.versionMinor > v.versionMinor) { 23 | return true; 24 | } else { 25 | return false; 26 | } 27 | } 28 | 29 | equalOrHigher(version: string): boolean { 30 | const v = new Version(version); 31 | 32 | if (this.versionMajor > v.versionMajor) { 33 | return true; 34 | } else if (this.versionMajor === v.versionMajor && this.versionMinor >= v.versionMinor) { 35 | return true; 36 | } else { 37 | return false; 38 | } 39 | } 40 | 41 | upTo(version: string): boolean { 42 | return !this.newerThan(version); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/workers/GreyhoundBinaryDecoderWorker.js: -------------------------------------------------------------------------------- 1 | /* global onmessage:true postMessage:false Module */ 2 | /* exported onmessage */ 3 | // http://jsperf.com/uint8array-vs-dataview3/3 4 | function CustomView(buffer) { 5 | this.buffer = buffer; 6 | this.u8 = new Uint8Array(buffer); 7 | 8 | let tmp = new ArrayBuffer(4); 9 | let tmpf = new Float32Array(tmp); 10 | let tmpu8 = new Uint8Array(tmp); 11 | 12 | this.getUint32 = function(i) { 13 | return (this.u8[i + 3] << 24) | (this.u8[i + 2] << 16) | (this.u8[i + 1] << 8) | this.u8[i]; 14 | }; 15 | 16 | this.getUint16 = function(i) { 17 | return (this.u8[i + 1] << 8) | this.u8[i]; 18 | }; 19 | 20 | this.getFloat = function(i) { 21 | tmpu8[0] = this.u8[i + 0]; 22 | tmpu8[1] = this.u8[i + 1]; 23 | tmpu8[2] = this.u8[i + 2]; 24 | tmpu8[3] = this.u8[i + 3]; 25 | 26 | return tmpf[0]; 27 | }; 28 | 29 | this.getUint8 = function(i) { 30 | return this.u8[i]; 31 | }; 32 | } 33 | 34 | let decompress = function(schema, input, numPoints) { 35 | let x = new Module.DynamicLASZip(); 36 | 37 | let abInt = new Uint8Array(input); 38 | let buf = Module._malloc(input.byteLength); 39 | 40 | Module.HEAPU8.set(abInt, buf); 41 | x.open(buf, input.byteLength); 42 | 43 | let pointSize = 0; 44 | 45 | schema.forEach(function(f) { 46 | pointSize += f.size; 47 | if (f.type === 'floating') x.addFieldFloating(f.size); 48 | else if (f.type === 'unsigned') x.addFieldUnsigned(f.size); 49 | else if (f.type === 'signed') x.addFieldSigned(f.size); 50 | else { 51 | if (PRODUCTION) { 52 | throw new Error(); 53 | } else { 54 | throw new Error('Unrecognized field desc:', f); 55 | } 56 | } 57 | }); 58 | 59 | let out = Module._malloc(numPoints * pointSize); 60 | 61 | for (let i = 0; i < numPoints; i++) { 62 | x.getPoint(out + i * pointSize); 63 | } 64 | 65 | let ret = new Uint8Array(numPoints * pointSize); 66 | ret.set(Module.HEAPU8.subarray(out, out + numPoints * pointSize)); 67 | 68 | Module._free(out); 69 | Module._free(buf); 70 | 71 | return ret.buffer; 72 | }; 73 | 74 | // Potree = {}; 75 | 76 | onmessage = function(event) { 77 | let NUM_POINTS_BYTES = 4; 78 | 79 | let buffer = event.data.buffer; 80 | let numPoints = new DataView( 81 | buffer, 82 | buffer.byteLength - NUM_POINTS_BYTES, 83 | NUM_POINTS_BYTES, 84 | ).getUint32(0, true); 85 | buffer = buffer.slice(0, buffer.byteLength - NUM_POINTS_BYTES); 86 | buffer = decompress(event.data.schema, buffer, numPoints); 87 | 88 | let pointAttributes = event.data.pointAttributes; 89 | let cv = new CustomView(buffer); 90 | let version = new Potree.Version(event.data.version); 91 | let nodeOffset = event.data.offset; 92 | let scale = event.data.scale; 93 | 94 | let tightBoxMin = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]; 95 | let tightBoxMax = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY]; 96 | let mean = [0, 0, 0]; 97 | 98 | let attributeBuffers = {}; 99 | let inOffset = 0; 100 | for (let pointAttribute of pointAttributes.attributes) { 101 | if (pointAttribute.name === Potree.PointAttribute.POSITION_CARTESIAN.name) { 102 | let buff = new ArrayBuffer(numPoints * 4 * 3); 103 | let positions = new Float32Array(buff); 104 | 105 | for (let j = 0; j < numPoints; j++) { 106 | let ux = cv.getUint32(inOffset + j * pointAttributes.byteSize + 0); 107 | let uy = cv.getUint32(inOffset + j * pointAttributes.byteSize + 4); 108 | let uz = cv.getUint32(inOffset + j * pointAttributes.byteSize + 8); 109 | 110 | let x = scale * ux + nodeOffset[0]; 111 | let y = scale * uy + nodeOffset[1]; 112 | let z = scale * uz + nodeOffset[2]; 113 | 114 | positions[3 * j + 0] = x; 115 | positions[3 * j + 1] = y; 116 | positions[3 * j + 2] = z; 117 | 118 | mean[0] += x / numPoints; 119 | mean[1] += y / numPoints; 120 | mean[2] += z / numPoints; 121 | 122 | tightBoxMin[0] = Math.min(tightBoxMin[0], x); 123 | tightBoxMin[1] = Math.min(tightBoxMin[1], y); 124 | tightBoxMin[2] = Math.min(tightBoxMin[2], z); 125 | 126 | tightBoxMax[0] = Math.max(tightBoxMax[0], x); 127 | tightBoxMax[1] = Math.max(tightBoxMax[1], y); 128 | tightBoxMax[2] = Math.max(tightBoxMax[2], z); 129 | } 130 | 131 | attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; 132 | } else if (pointAttribute.name === Potree.PointAttribute.COLOR_PACKED.name) { 133 | let buff = new ArrayBuffer(numPoints * 4); 134 | let colors = new Uint8Array(buff); 135 | let div = event.data.normalize.color ? 256 : 1; 136 | 137 | for (let j = 0; j < numPoints; j++) { 138 | let r = cv.getUint16(inOffset + j * pointAttributes.byteSize + 0) / div; 139 | let g = cv.getUint16(inOffset + j * pointAttributes.byteSize + 2) / div; 140 | let b = cv.getUint16(inOffset + j * pointAttributes.byteSize + 4) / div; 141 | 142 | colors[4 * j + 0] = r; 143 | colors[4 * j + 1] = g; 144 | colors[4 * j + 2] = b; 145 | } 146 | 147 | attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; 148 | } else if (pointAttribute.name === Potree.PointAttribute.INTENSITY.name) { 149 | let buff = new ArrayBuffer(numPoints * 4); 150 | let intensities = new Float32Array(buff); 151 | 152 | for (let j = 0; j < numPoints; j++) { 153 | let intensity = cv.getUint16(inOffset + j * pointAttributes.byteSize, true); 154 | intensities[j] = intensity; 155 | } 156 | 157 | attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; 158 | } else if (pointAttribute.name === Potree.PointAttribute.CLASSIFICATION.name) { 159 | let buff = new ArrayBuffer(numPoints); 160 | let classifications = new Uint8Array(buff); 161 | 162 | for (let j = 0; j < numPoints; j++) { 163 | let classification = cv.getUint8(inOffset + j * pointAttributes.byteSize); 164 | classifications[j] = classification; 165 | } 166 | 167 | attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute }; 168 | } 169 | 170 | inOffset += pointAttribute.byteSize; 171 | } 172 | 173 | { 174 | // add indices 175 | let buff = new ArrayBuffer(numPoints * 4); 176 | let indices = new Uint32Array(buff); 177 | 178 | for (let i = 0; i < numPoints; i++) { 179 | indices[i] = i; 180 | } 181 | 182 | attributeBuffers[Potree.PointAttribute.INDICES.name] = { 183 | buffer: buff, 184 | attribute: Potree.PointAttribute.INDICES, 185 | }; 186 | } 187 | 188 | let message = { 189 | numPoints: numPoints, 190 | mean: mean, 191 | attributeBuffers: attributeBuffers, 192 | tightBoundingBox: { min: tightBoxMin, max: tightBoxMax }, 193 | }; 194 | 195 | let transferables = []; 196 | for (let property in message.attributeBuffers) { 197 | transferables.push(message.attributeBuffers[property].buffer); 198 | } 199 | transferables.push(buffer); 200 | 201 | postMessage(message, transferables); 202 | }; 203 | -------------------------------------------------------------------------------- /src/workers/LASDecoderWorker.js: -------------------------------------------------------------------------------- 1 | // let pointFormatReaders = { 2 | // 0: function(dv) { 3 | // return { 4 | // "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 5 | // "intensity": dv.getUint16(12, true), 6 | // "classification": dv.getUint8(16, true) 7 | // }; 8 | // }, 9 | // 1: function(dv) { 10 | // return { 11 | // "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 12 | // "intensity": dv.getUint16(12, true), 13 | // "classification": dv.getUint8(16, true) 14 | // }; 15 | // }, 16 | // 2: function(dv) { 17 | // return { 18 | // "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 19 | // "intensity": dv.getUint16(12, true), 20 | // "classification": dv.getUint8(16, true), 21 | // "color": [dv.getUint16(20, true), dv.getUint16(22, true), dv.getUint16(24, true)] 22 | // }; 23 | // }, 24 | // 3: function(dv) { 25 | // return { 26 | // "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 27 | // "intensity": dv.getUint16(12, true), 28 | // "classification": dv.getUint8(16, true), 29 | // "color": [dv.getUint16(28, true), dv.getUint16(30, true), dv.getUint16(32, true)] 30 | // }; 31 | // } 32 | // }; 33 | // 34 | // 35 | 36 | function readUsingTempArrays(event) { 37 | if (!PRODUCTION) { 38 | performance.mark('laslaz-start'); 39 | } 40 | 41 | let buffer = event.data.buffer; 42 | let numPoints = event.data.numPoints; 43 | let sourcePointSize = event.data.pointSize; 44 | let pointFormatID = event.data.pointFormatID; 45 | let scale = event.data.scale; 46 | let offset = event.data.offset; 47 | 48 | let temp = new ArrayBuffer(4); 49 | let tempUint8 = new Uint8Array(temp); 50 | let tempUint16 = new Uint16Array(temp); 51 | let tempInt32 = new Int32Array(temp); 52 | let sourceUint8 = new Uint8Array(buffer); 53 | let sourceView = new DataView(buffer); 54 | 55 | let targetPointSize = 20; 56 | let targetBuffer = new ArrayBuffer(numPoints * targetPointSize); 57 | let targetView = new DataView(targetBuffer); 58 | 59 | let tightBoundingBox = { 60 | min: [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], 61 | max: [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], 62 | }; 63 | 64 | let mean = [0, 0, 0]; 65 | 66 | let pBuff = new ArrayBuffer(numPoints * 3 * 4); 67 | let cBuff = new ArrayBuffer(numPoints * 4); 68 | let iBuff = new ArrayBuffer(numPoints * 4); 69 | let clBuff = new ArrayBuffer(numPoints); 70 | let rnBuff = new ArrayBuffer(numPoints); 71 | let nrBuff = new ArrayBuffer(numPoints); 72 | let psBuff = new ArrayBuffer(numPoints * 2); 73 | 74 | let positions = new Float32Array(pBuff); 75 | let colors = new Uint8Array(cBuff); 76 | let intensities = new Float32Array(iBuff); 77 | let classifications = new Uint8Array(clBuff); 78 | let returnNumbers = new Uint8Array(rnBuff); 79 | let numberOfReturns = new Uint8Array(nrBuff); 80 | let pointSourceIDs = new Uint16Array(psBuff); 81 | 82 | for (let i = 0; i < numPoints; i++) { 83 | // POSITION 84 | tempUint8[0] = sourceUint8[i * sourcePointSize + 0]; 85 | tempUint8[1] = sourceUint8[i * sourcePointSize + 1]; 86 | tempUint8[2] = sourceUint8[i * sourcePointSize + 2]; 87 | tempUint8[3] = sourceUint8[i * sourcePointSize + 3]; 88 | let x = tempInt32[0]; 89 | 90 | tempUint8[0] = sourceUint8[i * sourcePointSize + 4]; 91 | tempUint8[1] = sourceUint8[i * sourcePointSize + 5]; 92 | tempUint8[2] = sourceUint8[i * sourcePointSize + 6]; 93 | tempUint8[3] = sourceUint8[i * sourcePointSize + 7]; 94 | let y = tempInt32[0]; 95 | 96 | tempUint8[0] = sourceUint8[i * sourcePointSize + 8]; 97 | tempUint8[1] = sourceUint8[i * sourcePointSize + 9]; 98 | tempUint8[2] = sourceUint8[i * sourcePointSize + 10]; 99 | tempUint8[3] = sourceUint8[i * sourcePointSize + 11]; 100 | let z = tempInt32[0]; 101 | 102 | x = x * scale[0] + offset[0] - event.data.mins[0]; 103 | y = y * scale[1] + offset[1] - event.data.mins[1]; 104 | z = z * scale[2] + offset[2] - event.data.mins[2]; 105 | 106 | positions[3 * i + 0] = x; 107 | positions[3 * i + 1] = y; 108 | positions[3 * i + 2] = z; 109 | 110 | mean[0] += x / numPoints; 111 | mean[1] += y / numPoints; 112 | mean[2] += z / numPoints; 113 | 114 | tightBoundingBox.min[0] = Math.min(tightBoundingBox.min[0], x); 115 | tightBoundingBox.min[1] = Math.min(tightBoundingBox.min[1], y); 116 | tightBoundingBox.min[2] = Math.min(tightBoundingBox.min[2], z); 117 | 118 | tightBoundingBox.max[0] = Math.max(tightBoundingBox.max[0], x); 119 | tightBoundingBox.max[1] = Math.max(tightBoundingBox.max[1], y); 120 | tightBoundingBox.max[2] = Math.max(tightBoundingBox.max[2], z); 121 | 122 | // INTENSITY 123 | tempUint8[0] = sourceUint8[i * sourcePointSize + 12]; 124 | tempUint8[1] = sourceUint8[i * sourcePointSize + 13]; 125 | let intensity = tempUint16[0]; 126 | intensities[i] = intensity; 127 | 128 | // RETURN NUMBER, stored in the first 3 bits - 00000111 129 | let returnNumber = sourceUint8[i * sourcePointSize + 14] & 0b111; 130 | returnNumbers[i] = returnNumber; 131 | 132 | // NUMBER OF RETURNS, stored in 00111000 133 | numberOfReturns[i] = (sourceUint8[i * pointSize + 14] & 0b111000) >> 3; 134 | 135 | debugger; 136 | 137 | // CLASSIFICATION 138 | let classification = sourceUint8[i * sourcePointSize + 15]; 139 | classifications[i] = classification; 140 | 141 | // POINT SOURCE ID 142 | tempUint8[0] = sourceUint8[i * sourcePointSize + 18]; 143 | tempUint8[1] = sourceUint8[i * sourcePointSize + 19]; 144 | let pointSourceID = tempUint16[0]; 145 | pointSourceIDs[i] = pointSourceID; 146 | 147 | // COLOR, if available 148 | if (pointFormatID === 2) { 149 | tempUint8[0] = sourceUint8[i * sourcePointSize + 20]; 150 | tempUint8[1] = sourceUint8[i * sourcePointSize + 21]; 151 | let r = tempUint16[0]; 152 | 153 | tempUint8[0] = sourceUint8[i * sourcePointSize + 22]; 154 | tempUint8[1] = sourceUint8[i * sourcePointSize + 23]; 155 | let g = tempUint16[0]; 156 | 157 | tempUint8[0] = sourceUint8[i * sourcePointSize + 24]; 158 | tempUint8[1] = sourceUint8[i * sourcePointSize + 25]; 159 | let b = tempUint16[0]; 160 | 161 | r = r / 256; 162 | g = g / 256; 163 | b = b / 256; 164 | colors[4 * i + 0] = r; 165 | colors[4 * i + 1] = g; 166 | colors[4 * i + 2] = b; 167 | } 168 | } 169 | 170 | let indices = new ArrayBuffer(numPoints * 4); 171 | let iIndices = new Uint32Array(indices); 172 | for (let i = 0; i < numPoints; i++) { 173 | iIndices[i] = i; 174 | } 175 | 176 | if (!PRODUCTION) { 177 | performance.mark('laslaz-end'); 178 | performance.measure('laslaz', 'laslaz-start', 'laslaz-end'); 179 | 180 | let measure = performance.getEntriesByType('measure')[0]; 181 | let dpp = (1000 * measure.duration) / numPoints; 182 | let debugMessage = `${measure.duration.toFixed(3)} ms, ${numPoints} points, ${dpp.toFixed( 183 | 3, 184 | )} micros / point`; 185 | console.log(debugMessage); 186 | 187 | performance.clearMarks(); 188 | performance.clearMeasures(); 189 | } 190 | 191 | let message = { 192 | mean: mean, 193 | position: pBuff, 194 | color: cBuff, 195 | intensity: iBuff, 196 | classification: clBuff, 197 | returnNumber: rnBuff, 198 | numberOfReturns: nrBuff, 199 | pointSourceID: psBuff, 200 | tightBoundingBox: tightBoundingBox, 201 | indices: indices, 202 | }; 203 | 204 | let transferables = [ 205 | message.position, 206 | message.color, 207 | message.intensity, 208 | message.classification, 209 | message.returnNumber, 210 | message.numberOfReturns, 211 | message.pointSourceID, 212 | message.indices, 213 | ]; 214 | 215 | debugger; 216 | 217 | postMessage(message, transferables); 218 | } 219 | 220 | function readUsingDataView(event) { 221 | if (!PRODUCTION) { 222 | performance.mark('laslaz-start'); 223 | } 224 | 225 | let buffer = event.data.buffer; 226 | let numPoints = event.data.numPoints; 227 | let sourcePointSize = event.data.pointSize; 228 | let pointFormatID = event.data.pointFormatID; 229 | let scale = event.data.scale; 230 | let offset = event.data.offset; 231 | 232 | let sourceUint8 = new Uint8Array(buffer); 233 | let sourceView = new DataView(buffer); 234 | 235 | let targetPointSize = 40; 236 | let targetBuffer = new ArrayBuffer(numPoints * targetPointSize); 237 | let targetView = new DataView(targetBuffer); 238 | 239 | let tightBoundingBox = { 240 | min: [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], 241 | max: [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], 242 | }; 243 | 244 | let mean = [0, 0, 0]; 245 | 246 | let pBuff = new ArrayBuffer(numPoints * 3 * 4); 247 | let cBuff = new ArrayBuffer(numPoints * 4); 248 | let iBuff = new ArrayBuffer(numPoints * 4); 249 | let clBuff = new ArrayBuffer(numPoints); 250 | let rnBuff = new ArrayBuffer(numPoints); 251 | let nrBuff = new ArrayBuffer(numPoints); 252 | let psBuff = new ArrayBuffer(numPoints * 2); 253 | 254 | let positions = new Float32Array(pBuff); 255 | let colors = new Uint8Array(cBuff); 256 | let intensities = new Float32Array(iBuff); 257 | let classifications = new Uint8Array(clBuff); 258 | let returnNumbers = new Uint8Array(rnBuff); 259 | let numberOfReturns = new Uint8Array(nrBuff); 260 | let pointSourceIDs = new Uint16Array(psBuff); 261 | 262 | for (let i = 0; i < numPoints; i++) { 263 | // POSITION 264 | let ux = sourceView.getInt32(i * sourcePointSize + 0, true); 265 | let uy = sourceView.getInt32(i * sourcePointSize + 4, true); 266 | let uz = sourceView.getInt32(i * sourcePointSize + 8, true); 267 | 268 | x = ux * scale[0] + offset[0] - event.data.mins[0]; 269 | y = uy * scale[1] + offset[1] - event.data.mins[1]; 270 | z = uz * scale[2] + offset[2] - event.data.mins[2]; 271 | 272 | //x = ux * scale[0]; 273 | //y = uy * scale[1]; 274 | //z = uz * scale[2]; 275 | 276 | positions[3 * i + 0] = x; 277 | positions[3 * i + 1] = y; 278 | positions[3 * i + 2] = z; 279 | 280 | mean[0] += x / numPoints; 281 | mean[1] += y / numPoints; 282 | mean[2] += z / numPoints; 283 | 284 | tightBoundingBox.min[0] = Math.min(tightBoundingBox.min[0], x); 285 | tightBoundingBox.min[1] = Math.min(tightBoundingBox.min[1], y); 286 | tightBoundingBox.min[2] = Math.min(tightBoundingBox.min[2], z); 287 | 288 | tightBoundingBox.max[0] = Math.max(tightBoundingBox.max[0], x); 289 | tightBoundingBox.max[1] = Math.max(tightBoundingBox.max[1], y); 290 | tightBoundingBox.max[2] = Math.max(tightBoundingBox.max[2], z); 291 | 292 | // INTENSITY 293 | let intensity = sourceView.getUint16(i * sourcePointSize + 12, true); 294 | intensities[i] = intensity; 295 | 296 | // RETURN NUMBER, stored in the first 3 bits - 00000111 297 | // number of returns stored in next 3 bits - 00111000 298 | let returnNumberAndNumberOfReturns = sourceView.getUint8(i * sourcePointSize + 14, true); 299 | let returnNumber = returnNumberAndNumberOfReturns & 0b0111; 300 | let numberOfReturn = (returnNumberAndNumberOfReturns & 0b00111000) >> 3; 301 | returnNumbers[i] = returnNumber; 302 | numberOfReturns[i] = numberOfReturn; 303 | 304 | // CLASSIFICATION 305 | let classification = sourceView.getUint8(i * sourcePointSize + 15, true); 306 | classifications[i] = classification; 307 | 308 | // POINT SOURCE ID 309 | let pointSourceID = sourceView.getUint16(i * sourcePointSize + 18, true); 310 | pointSourceIDs[i] = pointSourceID; 311 | 312 | // COLOR, if available 313 | if (pointFormatID === 2) { 314 | let r = sourceView.getUint16(i * sourcePointSize + 20, true) / 256; 315 | let g = sourceView.getUint16(i * sourcePointSize + 22, true) / 256; 316 | let b = sourceView.getUint16(i * sourcePointSize + 24, true) / 256; 317 | 318 | colors[4 * i + 0] = r; 319 | colors[4 * i + 1] = g; 320 | colors[4 * i + 2] = b; 321 | colors[4 * i + 3] = 255; 322 | } 323 | } 324 | 325 | let indices = new ArrayBuffer(numPoints * 4); 326 | let iIndices = new Uint32Array(indices); 327 | for (let i = 0; i < numPoints; i++) { 328 | iIndices[i] = i; 329 | } 330 | 331 | let message = { 332 | mean: mean, 333 | position: pBuff, 334 | color: cBuff, 335 | intensity: iBuff, 336 | classification: clBuff, 337 | returnNumber: rnBuff, 338 | numberOfReturns: nrBuff, 339 | pointSourceID: psBuff, 340 | tightBoundingBox: tightBoundingBox, 341 | indices: indices, 342 | }; 343 | 344 | let transferables = [ 345 | message.position, 346 | message.color, 347 | message.intensity, 348 | message.classification, 349 | message.returnNumber, 350 | message.numberOfReturns, 351 | message.pointSourceID, 352 | message.indices, 353 | ]; 354 | 355 | postMessage(message, transferables); 356 | } 357 | 358 | onmessage = readUsingDataView; 359 | -------------------------------------------------------------------------------- /src/workers/LazLoaderWorker.js: -------------------------------------------------------------------------------- 1 | /* global onmessage:true postMessage:false Module */ 2 | /* exported onmessage */ 3 | // laz-loader-worker.js 4 | // 5 | 6 | // importScripts('laz-perf.js'); 7 | 8 | let instance = null; // laz-perf instance 9 | 10 | function readAs(buf, Type, offset, count) { 11 | count = count === undefined || count === 0 ? 1 : count; 12 | let sub = buf.slice(offset, offset + Type.BYTES_PER_ELEMENT * count); 13 | 14 | let r = new Type(sub); 15 | if (count === undefined || count === 1) { 16 | return r[0]; 17 | } 18 | 19 | let ret = []; 20 | for (let i = 0; i < count; i++) { 21 | ret.push(r[i]); 22 | } 23 | 24 | return ret; 25 | } 26 | 27 | function parseLASHeader(arraybuffer) { 28 | let o = {}; 29 | 30 | o.pointsOffset = readAs(arraybuffer, Uint32Array, 32 * 3); 31 | o.pointsFormatId = readAs(arraybuffer, Uint8Array, 32 * 3 + 8); 32 | o.pointsStructSize = readAs(arraybuffer, Uint16Array, 32 * 3 + 8 + 1); 33 | o.pointsCount = readAs(arraybuffer, Uint32Array, 32 * 3 + 11); 34 | 35 | let start = 32 * 3 + 35; 36 | o.scale = readAs(arraybuffer, Float64Array, start, 3); 37 | start += 24; // 8*3 38 | o.offset = readAs(arraybuffer, Float64Array, start, 3); 39 | start += 24; 40 | 41 | let bounds = readAs(arraybuffer, Float64Array, start, 6); 42 | start += 48; // 8*6; 43 | o.maxs = [bounds[0], bounds[2], bounds[4]]; 44 | o.mins = [bounds[1], bounds[3], bounds[5]]; 45 | 46 | return o; 47 | } 48 | 49 | function handleEvent(msg) { 50 | switch (msg.type) { 51 | case 'open': 52 | try { 53 | instance = new Module.LASZip(); 54 | let abInt = new Uint8Array(msg.arraybuffer); 55 | let buf = Module._malloc(msg.arraybuffer.byteLength); 56 | 57 | instance.arraybuffer = msg.arraybuffer; 58 | instance.buf = buf; 59 | Module.HEAPU8.set(abInt, buf); 60 | instance.open(buf, msg.arraybuffer.byteLength); 61 | 62 | instance.readOffset = 0; 63 | 64 | postMessage({ type: 'open', status: 1 }); 65 | } catch (e) { 66 | postMessage({ type: 'open', status: 0, details: e }); 67 | } 68 | break; 69 | 70 | case 'header': 71 | if (!instance) { 72 | if (PRODUCTION) { 73 | throw new Error(); 74 | } else { 75 | throw new Error('You need to open the file before trying to read header'); 76 | } 77 | } 78 | 79 | let header = parseLASHeader(instance.arraybuffer); 80 | header.pointsFormatId &= 0x3f; 81 | instance.header = header; 82 | postMessage({ type: 'header', status: 1, header: header }); 83 | break; 84 | 85 | case 'read': 86 | if (!instance) { 87 | if (PRODUCTION) { 88 | throw new Error(); 89 | } else { 90 | throw new Error('You need to open the file before trying to read stuff'); 91 | } 92 | } 93 | 94 | // msg.start 95 | let count = msg.count; 96 | let skip = msg.skip; 97 | let o = instance; 98 | 99 | if (!o.header) { 100 | if (PRODUCTION) { 101 | throw new Error(); 102 | } else { 103 | throw new Error( 104 | 'You need to query header before reading, I maintain state that way, sorry :(', 105 | ); 106 | } 107 | } 108 | 109 | let pointsToRead = Math.min(count * skip, o.header.pointsCount - o.readOffset); 110 | let bufferSize = Math.ceil(pointsToRead / skip); 111 | let pointsRead = 0; 112 | 113 | let thisBuf = new Uint8Array(bufferSize * o.header.pointsStructSize); 114 | let bufRead = Module._malloc(o.header.pointsStructSize); 115 | for (let i = 0; i < pointsToRead; i++) { 116 | o.getPoint(bufRead); 117 | 118 | if (i % skip === 0) { 119 | let a = new Uint8Array(Module.HEAPU8.buffer, bufRead, o.header.pointsStructSize); 120 | thisBuf.set(a, pointsRead * o.header.pointsStructSize, o.header.pointsStructSize); 121 | pointsRead++; 122 | } 123 | 124 | o.readOffset++; 125 | } 126 | 127 | postMessage({ 128 | type: 'header', 129 | status: 1, 130 | buffer: thisBuf.buffer, 131 | count: pointsRead, 132 | hasMoreData: o.readOffset < o.header.pointsCount, 133 | }); 134 | 135 | break; 136 | 137 | case 'close': 138 | if (instance !== null) { 139 | instance.delete(); 140 | instance = null; 141 | } 142 | postMessage({ type: 'close', status: 1 }); 143 | break; 144 | } 145 | } 146 | 147 | onmessage = function(event) { 148 | try { 149 | handleEvent(event.data); 150 | } catch (e) { 151 | postMessage({ type: event.data.type, status: 0, details: e }); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /src/workers/binary-decoder-worker-internal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Potree.js http://potree.org 3 | * Potree License: https://github.com/potree/potree/blob/1.5/LICENSE 4 | */ 5 | 6 | import { 7 | IPointAttribute, 8 | IPointAttributes, 9 | PointAttributeName, 10 | POINT_ATTRIBUTES, 11 | } from '../point-attributes'; 12 | import { Version } from '../version'; 13 | import { CustomArrayView } from './custom-array-view'; 14 | 15 | // IE11 does not have Math.sign(), this has been adapted from CoreJS es6.math.sign.js for TypeScript 16 | const mathSign = 17 | Math.sign || 18 | function(x: number): number { 19 | // tslint:disable-next-line:triple-equals 20 | return (x = +x) == 0 || x != x ? x : x < 0 ? -1 : 1; 21 | }; 22 | 23 | interface DecodedAttribute { 24 | buffer: ArrayBuffer; 25 | attribute: IPointAttribute; 26 | } 27 | 28 | interface Ctx { 29 | attributeBuffers: Record; 30 | currentOffset: number; 31 | data: CustomArrayView; 32 | mean: [number, number, number]; 33 | nodeOffset: [number, number, number]; 34 | numPoints: number; 35 | pointAttributes: IPointAttributes; 36 | scale: number; 37 | tightBoxMax: [number, number, number]; 38 | tightBoxMin: [number, number, number]; 39 | transferables: ArrayBuffer[]; 40 | version: Version; 41 | } 42 | 43 | export function handleMessage(event: MessageEvent) { 44 | const buffer = event.data.buffer; 45 | const pointAttributes: IPointAttributes = event.data.pointAttributes; 46 | 47 | const ctx: Ctx = { 48 | attributeBuffers: {}, 49 | currentOffset: 0, 50 | data: new CustomArrayView(buffer), 51 | mean: [0, 0, 0], 52 | nodeOffset: event.data.offset, 53 | numPoints: event.data.buffer.byteLength / pointAttributes.byteSize, 54 | pointAttributes, 55 | scale: event.data.scale, 56 | tightBoxMax: [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], 57 | tightBoxMin: [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], 58 | transferables: [], 59 | version: new Version(event.data.version), 60 | }; 61 | 62 | for (const pointAttribute of ctx.pointAttributes.attributes) { 63 | decodeAndAddAttribute(pointAttribute, ctx); 64 | ctx.currentOffset += pointAttribute.byteSize; 65 | } 66 | 67 | const indices = new ArrayBuffer(ctx.numPoints * 4); 68 | const iIndices = new Uint32Array(indices); 69 | for (let i = 0; i < ctx.numPoints; i++) { 70 | iIndices[i] = i; 71 | } 72 | 73 | if (!ctx.attributeBuffers[PointAttributeName.CLASSIFICATION]) { 74 | addEmptyClassificationBuffer(ctx); 75 | } 76 | 77 | const message = { 78 | buffer: buffer, 79 | mean: ctx.mean, 80 | attributeBuffers: ctx.attributeBuffers, 81 | tightBoundingBox: { min: ctx.tightBoxMin, max: ctx.tightBoxMax }, 82 | indices, 83 | }; 84 | 85 | // console.log("old", message) 86 | postMessage(message, ctx.transferables as any); 87 | } 88 | 89 | function addEmptyClassificationBuffer(ctx: Ctx): void { 90 | const buffer = new ArrayBuffer(ctx.numPoints * 4); 91 | const classifications = new Float32Array(buffer); 92 | 93 | for (let i = 0; i < ctx.numPoints; i++) { 94 | classifications[i] = 0; 95 | } 96 | 97 | ctx.attributeBuffers[PointAttributeName.CLASSIFICATION] = { 98 | buffer, 99 | attribute: POINT_ATTRIBUTES.CLASSIFICATION, 100 | }; 101 | } 102 | 103 | function decodeAndAddAttribute(attribute: IPointAttribute, ctx: Ctx): void { 104 | const decodedAttribute = decodePointAttribute(attribute, ctx); 105 | if (decodedAttribute === undefined) { 106 | return; 107 | } 108 | 109 | ctx.attributeBuffers[decodedAttribute.attribute.name] = decodedAttribute; 110 | ctx.transferables.push(decodedAttribute.buffer); 111 | } 112 | 113 | function decodePointAttribute(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute | undefined { 114 | switch (attribute.name) { 115 | case PointAttributeName.POSITION_CARTESIAN: 116 | return decodePositionCartesian(attribute, ctx); 117 | case PointAttributeName.COLOR_PACKED: 118 | return decodeColor(attribute, ctx); 119 | case PointAttributeName.INTENSITY: 120 | return decodeIntensity(attribute, ctx); 121 | case PointAttributeName.CLASSIFICATION: 122 | return decodeClassification(attribute, ctx); 123 | case PointAttributeName.NORMAL_SPHEREMAPPED: 124 | return decodeNormalSphereMapped(attribute, ctx); 125 | case PointAttributeName.NORMAL_OCT16: 126 | return decodeNormalOct16(attribute, ctx); 127 | case PointAttributeName.NORMAL: 128 | return decodeNormal(attribute, ctx); 129 | default: 130 | return undefined; 131 | } 132 | } 133 | 134 | function decodePositionCartesian(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 135 | const buffer = new ArrayBuffer(ctx.numPoints * 4 * 3); 136 | const positions = new Float32Array(buffer); 137 | 138 | for (let i = 0; i < ctx.numPoints; i++) { 139 | let x; 140 | let y; 141 | let z; 142 | 143 | if (ctx.version.newerThan('1.3')) { 144 | x = ctx.data.getUint32(ctx.currentOffset + i * ctx.pointAttributes.byteSize + 0) * ctx.scale; 145 | y = ctx.data.getUint32(ctx.currentOffset + i * ctx.pointAttributes.byteSize + 4) * ctx.scale; 146 | z = ctx.data.getUint32(ctx.currentOffset + i * ctx.pointAttributes.byteSize + 8) * ctx.scale; 147 | } else { 148 | x = ctx.data.getFloat32(i * ctx.pointAttributes.byteSize + 0) + ctx.nodeOffset[0]; 149 | y = ctx.data.getFloat32(i * ctx.pointAttributes.byteSize + 4) + ctx.nodeOffset[1]; 150 | z = ctx.data.getFloat32(i * ctx.pointAttributes.byteSize + 8) + ctx.nodeOffset[2]; 151 | } 152 | 153 | positions[3 * i + 0] = x; 154 | positions[3 * i + 1] = y; 155 | positions[3 * i + 2] = z; 156 | 157 | ctx.mean[0] += x / ctx.numPoints; 158 | ctx.mean[1] += y / ctx.numPoints; 159 | ctx.mean[2] += z / ctx.numPoints; 160 | 161 | ctx.tightBoxMin[0] = Math.min(ctx.tightBoxMin[0], x); 162 | ctx.tightBoxMin[1] = Math.min(ctx.tightBoxMin[1], y); 163 | ctx.tightBoxMin[2] = Math.min(ctx.tightBoxMin[2], z); 164 | 165 | ctx.tightBoxMax[0] = Math.max(ctx.tightBoxMax[0], x); 166 | ctx.tightBoxMax[1] = Math.max(ctx.tightBoxMax[1], y); 167 | ctx.tightBoxMax[2] = Math.max(ctx.tightBoxMax[2], z); 168 | } 169 | 170 | return { buffer, attribute }; 171 | } 172 | 173 | function decodeColor(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 174 | const buffer = new ArrayBuffer(ctx.numPoints * 3); 175 | const colors = new Uint8Array(buffer); 176 | 177 | for (let i = 0; i < ctx.numPoints; i++) { 178 | colors[3 * i + 0] = ctx.data.getUint8(ctx.currentOffset + i * ctx.pointAttributes.byteSize + 0); 179 | colors[3 * i + 1] = ctx.data.getUint8(ctx.currentOffset + i * ctx.pointAttributes.byteSize + 1); 180 | colors[3 * i + 2] = ctx.data.getUint8(ctx.currentOffset + i * ctx.pointAttributes.byteSize + 2); 181 | } 182 | 183 | return { buffer, attribute }; 184 | } 185 | 186 | function decodeIntensity(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 187 | const buffer = new ArrayBuffer(ctx.numPoints * 4); 188 | const intensities = new Float32Array(buffer); 189 | 190 | for (let i = 0; i < ctx.numPoints; i++) { 191 | intensities[i] = ctx.data.getUint16(ctx.currentOffset + i * ctx.pointAttributes.byteSize); 192 | } 193 | 194 | return { buffer, attribute }; 195 | } 196 | 197 | function decodeClassification(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 198 | const buffer = new ArrayBuffer(ctx.numPoints); 199 | const classifications = new Uint8Array(buffer); 200 | 201 | for (let j = 0; j < ctx.numPoints; j++) { 202 | classifications[j] = ctx.data.getUint8(ctx.currentOffset + j * ctx.pointAttributes.byteSize); 203 | } 204 | 205 | return { buffer, attribute }; 206 | } 207 | 208 | function decodeNormalSphereMapped(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 209 | const buffer = new ArrayBuffer(ctx.numPoints * 4 * 3); 210 | const normals = new Float32Array(buffer); 211 | 212 | for (let j = 0; j < ctx.numPoints; j++) { 213 | const bx = ctx.data.getUint8(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 0); 214 | const by = ctx.data.getUint8(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 1); 215 | 216 | const ex = bx / 255; 217 | const ey = by / 255; 218 | 219 | let nx = ex * 2 - 1; 220 | let ny = ey * 2 - 1; 221 | let nz = 1; 222 | const nw = -1; 223 | 224 | const l = nx * -nx + ny * -ny + nz * -nw; 225 | nz = l; 226 | nx = nx * Math.sqrt(l); 227 | ny = ny * Math.sqrt(l); 228 | 229 | nx = nx * 2; 230 | ny = ny * 2; 231 | nz = nz * 2 - 1; 232 | 233 | normals[3 * j + 0] = nx; 234 | normals[3 * j + 1] = ny; 235 | normals[3 * j + 2] = nz; 236 | } 237 | 238 | return { buffer, attribute }; 239 | } 240 | 241 | function decodeNormalOct16(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 242 | const buff = new ArrayBuffer(ctx.numPoints * 4 * 3); 243 | const normals = new Float32Array(buff); 244 | 245 | for (let j = 0; j < ctx.numPoints; j++) { 246 | const bx = ctx.data.getUint8(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 0); 247 | const by = ctx.data.getUint8(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 1); 248 | 249 | const u = (bx / 255) * 2 - 1; 250 | const v = (by / 255) * 2 - 1; 251 | 252 | let z = 1 - Math.abs(u) - Math.abs(v); 253 | 254 | let x = 0; 255 | let y = 0; 256 | if (z >= 0) { 257 | x = u; 258 | y = v; 259 | } else { 260 | x = -(v / mathSign(v) - 1) / mathSign(u); 261 | y = -(u / mathSign(u) - 1) / mathSign(v); 262 | } 263 | 264 | const length = Math.sqrt(x * x + y * y + z * z); 265 | x = x / length; 266 | y = y / length; 267 | z = z / length; 268 | 269 | normals[3 * j + 0] = x; 270 | normals[3 * j + 1] = y; 271 | normals[3 * j + 2] = z; 272 | } 273 | 274 | return { buffer: buff, attribute }; 275 | } 276 | 277 | function decodeNormal(attribute: IPointAttribute, ctx: Ctx): DecodedAttribute { 278 | const buffer = new ArrayBuffer(ctx.numPoints * 4 * 3); 279 | const normals = new Float32Array(buffer); 280 | 281 | for (let j = 0; j < ctx.numPoints; j++) { 282 | const x = ctx.data.getFloat32(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 0); 283 | const y = ctx.data.getFloat32(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 4); 284 | const z = ctx.data.getFloat32(ctx.currentOffset + j * ctx.pointAttributes.byteSize + 8); 285 | 286 | normals[3 * j + 0] = x; 287 | normals[3 * j + 1] = y; 288 | normals[3 * j + 2] = z; 289 | } 290 | 291 | return { buffer, attribute }; 292 | } 293 | -------------------------------------------------------------------------------- /src/workers/binary-decoder.worker.js: -------------------------------------------------------------------------------- 1 | import { handleMessage } from './binary-decoder-worker-internal'; 2 | 3 | /*eslint-disable */ 4 | onmessage = handleMessage; 5 | -------------------------------------------------------------------------------- /src/workers/custom-array-view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Potree.js http://potree.org 3 | * Potree License: https://github.com/potree/potree/blob/1.5/LICENSE 4 | */ 5 | 6 | // http://jsperf.com/uint8array-vs-dataview3/3 7 | // tslint:disable:no-bitwise 8 | export class CustomArrayView { 9 | private u8: Uint8Array; 10 | private tmp = new ArrayBuffer(4); 11 | private tmpf = new Float32Array(this.tmp); 12 | private tmpu8 = new Uint8Array(this.tmp); 13 | 14 | constructor(buffer: ArrayBuffer) { 15 | this.u8 = new Uint8Array(buffer); 16 | } 17 | 18 | getUint32(i: number) { 19 | return (this.u8[i + 3] << 24) | (this.u8[i + 2] << 16) | (this.u8[i + 1] << 8) | this.u8[i]; 20 | } 21 | 22 | getUint16(i: number): number { 23 | return (this.u8[i + 1] << 8) | this.u8[i]; 24 | } 25 | 26 | getFloat32(i: number): number { 27 | const tmpu8 = this.tmpu8; 28 | const u8 = this.u8; 29 | const tmpf = this.tmpf; 30 | 31 | tmpu8[0] = u8[i + 0]; 32 | tmpu8[1] = u8[i + 1]; 33 | tmpu8[2] = u8[i + 2]; 34 | tmpu8[3] = u8[i + 3]; 35 | 36 | return tmpf[0]; 37 | } 38 | 39 | getUint8(i: number): number { 40 | return this.u8[i]; 41 | } 42 | } 43 | // tslint:enable:no-bitwise 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./src/", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationDir": "build/declarations", 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "strict": true, 11 | // "noImplicitAny": true, 12 | // "noImplicitReturns": true, 13 | // "noImplicitThis": true, 14 | // "noUnusedLocals": true, 15 | // "noUnusedParameters": true, 16 | // "alwaysStrict": true, 17 | "target": "es6", 18 | "experimentalDecorators": true, 19 | "typeRoots": ["node_modules/@types"], 20 | "lib": ["es2020", "dom"], 21 | "plugins": [] 22 | }, 23 | "include": ["src/**/*.ts"], 24 | "exclude": ["build", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "rulesDirectory": [], 4 | "rules": { 5 | "adjacent-overload-signatures": true, 6 | "arrow-return-shorthand": true, 7 | "class-name": true, 8 | "comment-format": [true, "check-space"], 9 | "curly": true, 10 | "eofline": true, 11 | "forin": true, 12 | "import-spacing": true, 13 | "indent": [true, "spaces"], 14 | "jsdoc-format": true, 15 | "label-position": true, 16 | "max-line-length": [true, 140], 17 | "member-access": false, 18 | "new-parens": true, 19 | "no-angle-bracket-type-assertion": true, 20 | "no-arg": true, 21 | "no-bitwise": true, 22 | "no-boolean-literal-compare": false, 23 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 24 | "no-consecutive-blank-lines": [true], 25 | "no-construct": true, 26 | "no-debugger": true, 27 | "no-default-export": true, 28 | "no-duplicate-imports": true, 29 | "no-duplicate-variable": true, 30 | "no-duplicate-super": true, 31 | "no-duplicate-switch-case": true, 32 | "no-empty": false, 33 | "no-eval": true, 34 | "no-inferrable-types": [false], 35 | "no-invalid-template-strings": true, 36 | "no-invalid-this": true, 37 | "no-shadowed-variable": true, 38 | "no-sparse-arrays": true, 39 | "no-string-literal": false, 40 | "no-string-throw": true, 41 | "no-switch-case-fall-through": true, 42 | "no-trailing-whitespace": true, 43 | "no-unbound-method": false, 44 | "no-unnecessary-initializer": true, 45 | "no-unused-expression": true, 46 | "no-unused-variable": [ 47 | false, 48 | { 49 | "ignore-pattern": "^_" 50 | } 51 | ], 52 | "no-use-before-declare": false, 53 | "no-var-keyword": true, 54 | "object-literal-key-quotes": [true, "as-needed"], 55 | "object-literal-sort-keys": false, 56 | "one-line": [ 57 | true, 58 | "check-open-brace", 59 | "check-catch", 60 | "check-else", 61 | "check-whitespace", 62 | "check-finally" 63 | ], 64 | "one-variable-per-declaration": [true, "ignore-for-loop"], 65 | "ordered-imports": [true], 66 | "prefer-const": true, 67 | "prefer-method-signature": false, 68 | "prefer-object-spread": true, 69 | "prefer-switch": true, 70 | "prefer-template": true, 71 | "quotemark": [true, "single", "avoid-escape"], 72 | "radix": true, 73 | "semicolon": [true, "always", "ignore-bound-class-methods"], 74 | "trailing-comma": false, 75 | "triple-equals": [true, "allow-null-check"], 76 | "typedef-whitespace": [ 77 | true, 78 | { 79 | "call-signature": "nospace", 80 | "index-signature": "nospace", 81 | "parameter": "nospace", 82 | "property-declaration": "nospace", 83 | "variable-declaration": "nospace" 84 | } 85 | ], 86 | "unified-signatures": true, 87 | "use-isnan": true, 88 | "variable-name": [true, "ban-keywords"], 89 | "whitespace": [ 90 | true, 91 | "check-branch", 92 | "check-decl", 93 | "check-operator", 94 | "check-separator", 95 | "check-type" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import glsl from 'vite-plugin-glsl' 2 | import { defineConfig } from 'vite' 3 | import { resolve } from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [glsl()], 7 | build: { 8 | lib: { 9 | entry: resolve(__dirname, 'src/index.ts'), 10 | name: 'potree-loader', 11 | fileName: (format) => `potree-loader.${format}.js`, 12 | }, 13 | 14 | sourcemap: true, 15 | }, 16 | resolve: { 17 | alias: { 18 | 'three': resolve('./node_modules/three') 19 | } 20 | } 21 | }) --------------------------------------------------------------------------------