├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── bundle_ray.js ├── bundle_tree.js ├── octree.html ├── octree.js ├── raycaster.html └── raycaster.js ├── index.js ├── lib ├── bounds3.js └── treeNode.js ├── package.json ├── perf ├── index.js └── results.txt └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Andrei Kashcha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yaot [![Build Status](https://travis-ci.org/anvaka/yaot.svg)](https://travis-ci.org/anvaka/yaot) 2 | 3 | Octree in javascript. Extremely fast module to query points in 3D space. Can 4 | be used to find points under mouse cursor in 3D scene. Compare: [hit-test 5 | speed with native three.js vs octree](https://www.youtube.com/watch?v=9Z-Yzb-WSKg) 6 | 7 | # usage 8 | 9 | This module is best suited for static scenes, where points are not changed 10 | over time. To get started initialize the tree: 11 | 12 | ``` js 13 | // First we need to create the tree: 14 | var createTree = require('yaot'); 15 | 16 | var tree = createTree(); 17 | var points = [ 18 | 0, 0, 0, // First point at 0, 0, 0 19 | 10, 0, 0 // second point at 10, 0, 0 20 | ] 21 | tree.init(points); 22 | 23 | // Now we are ready to query it: 24 | // Which points lie inside sphere with center at 0, 0, 0 and radius 2? 25 | var matches = tree.intersectSphere(0, 0, 0, 2); 26 | // matches[0] === 0 -> the point at first index of `points` array is there! 27 | 28 | // Let's extend our sphere: 29 | var matches = tree.intersectSphere(0, 0, 0, 20); 30 | // matches[0] === 0 -> Point at index 0 is here too 31 | // matches[1] === 3 -> Point at index 3 from `points` array also inisde 32 | ``` 33 | 34 | You can also query points which lies inside octants intersected by a ray. This 35 | is very useful when you want to know which points lie under mouse cursor. 36 | 37 | ``` js 38 | var rayOrigin = { 39 | x: 1, y: 0, z: 0 40 | }; 41 | var rayDirection = { 42 | x: -1, y: 0, z: 0 43 | }; 44 | var matches = tree.intersectRay(rayOrigin, rayDirection) 45 | 46 | // If you want to limit where ray starts checking against intersection 47 | // you can pass option `near` argumnet: 48 | var near = 10; // by default it is 0, but could be made bigger! 49 | var matches10PixelsAway = tree.intersectRay(rayOrigin, rayDirection, near); 50 | 51 | // You can also limit upper bound by setting `far` argument: 52 | var far = 100; // By default it is positive infinity, which matches all. 53 | var matchesPointsBetween10And100Pixels = 54 | tree.intersectRay(rayOrigin, rayDirection, near, far); 55 | ``` 56 | 57 | To see how to use it with three.js please read about demo below. 58 | 59 | # demo 60 | 61 | A three.js demo is available [here](http://anvaka.github.io/yaot/demo/octree.html) ([src](https://github.com/anvaka/yaot/blob/master/demo/octree.js#L104)). 62 | You can compare its performance to native three.js [`raycaster.intersectObjects()` 63 | method](http://anvaka.github.io/yaot/demo/raycaster.html) ([src](https://github.com/anvaka/yaot/blob/master/demo/raycaster.js#L103)). 64 | Open dev console on both pages to see the timers. Octree solution is 42 times faster than 65 | native `raycaster.intersectObjects()`. 66 | 67 | Keep in mind that raycaster is generalized solution which works with any three.js 68 | objects, while octree is very much specialized. 69 | 70 | This module is also used in the [code galaxies](http://anvaka.github.io/pm/). 71 | Source code is [here](https://github.com/anvaka/unrender/blob/master/lib/hit-test.js). 72 | 73 | # install 74 | 75 | With [npm](https://npmjs.org) do: 76 | 77 | ``` 78 | npm install yaot 79 | ``` 80 | 81 | # license 82 | 83 | MIT 84 | -------------------------------------------------------------------------------- /demo/octree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hit testing with octree and three.js 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 |
21 | 22 | 23 | Fork me on GitHub 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/octree.js: -------------------------------------------------------------------------------- 1 | var THREE = require('three'); 2 | var tree = require('../')(); 3 | 4 | var container, stats, particleArray; 5 | 6 | var camera, scene, renderer, mouse = {x: 0, y: 0}; 7 | var raycaster = new THREE.Raycaster(); 8 | raycaster.params.PointCloud.threshold = 10; 9 | 10 | init(); 11 | 12 | function init() { 13 | container = document.getElementById('container'); 14 | camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 5, 3500); 15 | camera.position.z = 2750; 16 | 17 | scene = new THREE.Scene(); 18 | scene.fog = new THREE.Fog(0x050505, 2000, 3500); 19 | 20 | var particles = 500000; 21 | 22 | var geometry = new THREE.BufferGeometry(); 23 | 24 | var positions = new Float32Array(particles * 3); 25 | var colors = new Float32Array(particles * 3); 26 | 27 | var color = new THREE.Color(); 28 | 29 | var n = 1000, 30 | n2 = n / 2; // particles spread in the cube 31 | 32 | for (var i = 0; i < positions.length; i += 3) { 33 | 34 | // positions 35 | 36 | var x = Math.random() * n - n2; 37 | var y = Math.random() * n - n2; 38 | var z = Math.random() * n - n2; 39 | 40 | positions[i] = x; 41 | positions[i + 1] = y; 42 | positions[i + 2] = z; 43 | 44 | // colors 45 | 46 | var vx = (x / n) + 0.5; 47 | var vy = (y / n) + 0.5; 48 | var vz = (z / n) + 0.5; 49 | 50 | color.setRGB(vx, vy, vz); 51 | 52 | colors[i] = color.r; 53 | colors[i + 1] = color.g; 54 | colors[i + 2] = color.b; 55 | } 56 | 57 | tree.initAsync(positions, listenToMouse); 58 | 59 | geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3)); 60 | geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3)); 61 | 62 | geometry.computeBoundingSphere(); 63 | 64 | var material = new THREE.PointCloudMaterial({ 65 | size: 15, 66 | vertexColors: THREE.VertexColors 67 | }); 68 | 69 | particleSystem = new THREE.PointCloud(geometry, material); 70 | scene.add(particleSystem); 71 | particleArray = [particleSystem]; 72 | 73 | renderer = new THREE.WebGLRenderer({ 74 | antialias: false 75 | }); 76 | renderer.setClearColor(scene.fog.color); 77 | renderer.setPixelRatio(window.devicePixelRatio); 78 | renderer.setSize(window.innerWidth, window.innerHeight); 79 | 80 | container.appendChild(renderer.domElement); 81 | 82 | stats = new Stats(); 83 | stats.domElement.style.position = 'absolute'; 84 | stats.domElement.style.top = '0px'; 85 | container.appendChild(stats.domElement); 86 | 87 | window.addEventListener('resize', onWindowResize, false); 88 | } 89 | 90 | function listenToMouse() { 91 | animate(); 92 | document.body.addEventListener('mousemove', queryPoints); 93 | } 94 | 95 | function queryPoints(e) { 96 | mouse.x = (e.clientX / renderer.domElement.clientWidth) * 2 - 1; 97 | mouse.y = -(e.clientY / renderer.domElement.clientHeight) * 2 + 1; 98 | raycaster.setFromCamera(mouse, camera); 99 | 100 | var ray = raycaster.ray; 101 | console.time('ray'); 102 | 103 | //var items = raycaster.intersectObjects(particleArray); 104 | var items = tree.intersectRay(ray.origin, ray.direction); 105 | 106 | console.timeEnd('ray'); 107 | } 108 | 109 | function onWindowResize() { 110 | camera.aspect = window.innerWidth / window.innerHeight; 111 | camera.updateProjectionMatrix(); 112 | renderer.setSize(window.innerWidth, window.innerHeight); 113 | } 114 | 115 | function animate() { 116 | requestAnimationFrame(animate); 117 | render(); 118 | stats.update(); 119 | } 120 | 121 | function render() { 122 | var time = Date.now() * 0.001; 123 | particleSystem.rotation.x = time * 0.25; 124 | particleSystem.rotation.y = time * 0.5; 125 | renderer.render(scene, camera); 126 | } 127 | -------------------------------------------------------------------------------- /demo/raycaster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hit testing with native three.js raycaster 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 |
21 | 22 | 23 | Fork me on GitHub 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/raycaster.js: -------------------------------------------------------------------------------- 1 | var THREE = require('three'); 2 | var tree = require('../')(); 3 | 4 | var container, stats, particleArray; 5 | 6 | var camera, scene, renderer, mouse = {x: 0, y: 0}; 7 | var raycaster = new THREE.Raycaster(); 8 | raycaster.params.PointCloud.threshold = 10; 9 | 10 | init(); 11 | 12 | function init() { 13 | container = document.getElementById('container'); 14 | camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 5, 3500); 15 | camera.position.z = 2750; 16 | 17 | scene = new THREE.Scene(); 18 | scene.fog = new THREE.Fog(0x050505, 2000, 3500); 19 | 20 | var particles = 500000; 21 | 22 | var geometry = new THREE.BufferGeometry(); 23 | 24 | var positions = new Float32Array(particles * 3); 25 | var colors = new Float32Array(particles * 3); 26 | 27 | var color = new THREE.Color(); 28 | 29 | var n = 1000, 30 | n2 = n / 2; // particles spread in the cube 31 | 32 | for (var i = 0; i < positions.length; i += 3) { 33 | 34 | // positions 35 | 36 | var x = Math.random() * n - n2; 37 | var y = Math.random() * n - n2; 38 | var z = Math.random() * n - n2; 39 | 40 | positions[i] = x; 41 | positions[i + 1] = y; 42 | positions[i + 2] = z; 43 | 44 | // colors 45 | 46 | var vx = (x / n) + 0.5; 47 | var vy = (y / n) + 0.5; 48 | var vz = (z / n) + 0.5; 49 | 50 | color.setRGB(vx, vy, vz); 51 | 52 | colors[i] = color.r; 53 | colors[i + 1] = color.g; 54 | colors[i + 2] = color.b; 55 | } 56 | 57 | tree.initAsync(positions, listenToMouse); 58 | 59 | geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3)); 60 | geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3)); 61 | 62 | geometry.computeBoundingSphere(); 63 | 64 | var material = new THREE.PointCloudMaterial({ 65 | size: 15, 66 | vertexColors: THREE.VertexColors 67 | }); 68 | 69 | particleSystem = new THREE.PointCloud(geometry, material); 70 | scene.add(particleSystem); 71 | particleArray = [particleSystem]; 72 | 73 | renderer = new THREE.WebGLRenderer({ 74 | antialias: false 75 | }); 76 | renderer.setClearColor(scene.fog.color); 77 | renderer.setPixelRatio(window.devicePixelRatio); 78 | renderer.setSize(window.innerWidth, window.innerHeight); 79 | 80 | container.appendChild(renderer.domElement); 81 | 82 | stats = new Stats(); 83 | stats.domElement.style.position = 'absolute'; 84 | stats.domElement.style.top = '0px'; 85 | container.appendChild(stats.domElement); 86 | 87 | window.addEventListener('resize', onWindowResize, false); 88 | } 89 | 90 | function listenToMouse() { 91 | animate(); 92 | document.body.addEventListener('mousemove', queryPoints); 93 | } 94 | 95 | function queryPoints(e) { 96 | mouse.x = (e.clientX / renderer.domElement.clientWidth) * 2 - 1; 97 | mouse.y = -(e.clientY / renderer.domElement.clientHeight) * 2 + 1; 98 | raycaster.setFromCamera(mouse, camera); 99 | 100 | var ray = raycaster.ray; 101 | console.time('ray'); 102 | 103 | var items = raycaster.intersectObjects(particleArray); 104 | //var items = tree.intersectRay(ray.origin, ray.direction); 105 | 106 | console.timeEnd('ray'); 107 | } 108 | 109 | function onWindowResize() { 110 | camera.aspect = window.innerWidth / window.innerHeight; 111 | camera.updateProjectionMatrix(); 112 | renderer.setSize(window.innerWidth, window.innerHeight); 113 | } 114 | 115 | function animate() { 116 | requestAnimationFrame(animate); 117 | render(); 118 | stats.update(); 119 | } 120 | 121 | function render() { 122 | var time = Date.now() * 0.001; 123 | particleSystem.rotation.x = time * 0.25; 124 | particleSystem.rotation.y = time * 0.5; 125 | renderer.render(scene, camera); 126 | } 127 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents octree data structure 3 | * 4 | * https://en.wikipedia.org/wiki/Octree 5 | */ 6 | var Bounds3 = require('./lib/bounds3.js'); 7 | var TreeNode = require('./lib/treeNode.js'); 8 | var EmptyRegion = new Bounds3(); 9 | var asyncFor = require('rafor'); 10 | 11 | module.exports = createTree; 12 | 13 | function createTree(options) { 14 | options = options || {}; 15 | var noPoints = []; 16 | 17 | var root; 18 | var originalArray; 19 | var api = { 20 | /** 21 | * Initializes tree asynchronously. Very useful when you have millions 22 | * of points and do not want to block rendering thread for too long. 23 | * 24 | * @param {number[]} points array of points for which we are building the 25 | * tree. Flat sequence of (x, y, z) coordinates. Array length should be 26 | * multiple of 3. 27 | * 28 | * @param {Function=} doneCallback called when tree is initialized. The 29 | * callback will be called with single argument which represent current 30 | * tree. 31 | */ 32 | initAsync: initAsync, 33 | 34 | /** 35 | * Synchronous version of `initAsync()`. Should only be used for small 36 | * trees (less than 50-70k of points). 37 | * 38 | * @param {number[]} points array of points for which we are building the 39 | * tree. Flat sequence of (x, y, z) coordinates. Array length should be 40 | * multiple of 3. 41 | */ 42 | init: init, 43 | 44 | /** 45 | * Gets bounds of the root node. Bounds are represented by center of the 46 | * node (x, y, z) and `half` attribute - distance from the center to an 47 | * edge of the root node. 48 | */ 49 | bounds: getBounds, 50 | 51 | /** 52 | * Fires a ray from `rayOrigin` into `rayDirection` and collects all points 53 | * that lie in the octants intersected by the ray. 54 | * 55 | * This method implements An Efficient Parametric Algorithm for Octree Traversal 56 | * described in http://wscg.zcu.cz/wscg2000/Papers_2000/X31.pdf 57 | * 58 | * @param {Vector3} rayOrigin x,y,z coordinates where ray starts 59 | * @param {Vector3} rayDirection normalized x,y,z direction where ray shoots. 60 | * @param {number+} near minimum distance from the ray origin. 0 by default. 61 | * @param {number+} far maximum length of the ray. POSITIVE_INFINITY by default 62 | * 63 | * @return {Array} of indices in the source array. Each index represnts a start 64 | * of the x,y,z triplet of a point, that lies in the intersected octant. 65 | */ 66 | intersectRay: intersectRay, 67 | 68 | /** 69 | * Once you have collected points from the octants intersected by a ray 70 | * (`intersectRay()` method), it may be worth to query points from the surrouning 71 | * area. 72 | */ 73 | intersectSphere: intersectSphere, 74 | 75 | /** 76 | * Gets root node of the tree 77 | */ 78 | getRoot: getRoot 79 | }; 80 | 81 | return api; 82 | 83 | function getRoot() { 84 | return root; 85 | } 86 | 87 | function intersectSphere(cx, cy, cz, r) { 88 | if (!root) { 89 | // Most likely we are not initialized yet 90 | return noPoints; 91 | } 92 | var indices = []; 93 | var r2 = r * r; 94 | root.query(indices, originalArray, intersectCheck, preciseCheck); 95 | return indices; 96 | 97 | // http://stackoverflow.com/questions/4578967/cube-sphere-intersection-test 98 | function intersectCheck(candidate) { 99 | var dist = r2; 100 | var half = candidate.half; 101 | if (cx < candidate.x - half) dist -= sqr(cx - (candidate.x - half)); 102 | else if (cx > candidate.x + half) dist -= sqr(cx - (candidate.x + half)); 103 | 104 | if (cy < candidate.y - half) dist -= sqr(cy - (candidate.y - half)); 105 | else if (cy > candidate.y + half) dist -= sqr(cy - (candidate.y + half)); 106 | 107 | if (cz < candidate.z - half) dist -= sqr(cz - (candidate.z - half)); 108 | else if (cz > candidate.z + half) dist -= sqr(cz - (candidate.z + half)); 109 | return dist > 0; 110 | } 111 | 112 | function preciseCheck(x, y, z) { 113 | return sqr(x - cx) + sqr(y - cy) + sqr(z - cz) < r2; 114 | } 115 | } 116 | 117 | function sqr(x) { 118 | return x * x; 119 | } 120 | 121 | function intersectRay(rayOrigin, rayDirection, near, far) { 122 | if (!root) { 123 | // Most likely we are not initialized yet 124 | return noPoints; 125 | } 126 | 127 | if (near === undefined) near = 0; 128 | if (far === undefined) far = Number.POSITIVE_INFINITY; 129 | // we save as squar, to avoid expensive sqrt() operation 130 | near *= near; 131 | far *= far; 132 | 133 | var indices = []; 134 | root.query(indices, originalArray, intersectCheck, farEnough); 135 | return indices.sort(byDistanceToCamera); 136 | 137 | function intersectCheck(candidate) { 138 | // using http://wscg.zcu.cz/wscg2000/Papers_2000/X31.pdf 139 | var half = candidate.half; 140 | var t1 = (candidate.x - half - rayOrigin.x) / rayDirection.x, 141 | t2 = (candidate.x + half - rayOrigin.x) / rayDirection.x, 142 | t3 = (candidate.y + half - rayOrigin.y) / rayDirection.y, 143 | t4 = (candidate.y - half - rayOrigin.y) / rayDirection.y, 144 | t5 = (candidate.z - half - rayOrigin.z) / rayDirection.z, 145 | t6 = (candidate.z + half - rayOrigin.z) / rayDirection.z, 146 | tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)), 147 | tmin; 148 | 149 | if (tmax < 0) return false; 150 | 151 | tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); 152 | return tmin <= tmax && tmin <= far; 153 | } 154 | 155 | function farEnough(x, y, z) { 156 | var dist = (x - rayOrigin.x) * (x - rayOrigin.x) + 157 | (y - rayOrigin.y) * (y - rayOrigin.y) + 158 | (z - rayOrigin.z) * (z - rayOrigin.z); 159 | return near <= dist && dist <= far; 160 | } 161 | 162 | function byDistanceToCamera(idx0, idx1) { 163 | var x0 = rayOrigin[idx0]; 164 | var y0 = rayOrigin[idx0 + 1]; 165 | var z0 = rayOrigin[idx0 + 2]; 166 | var dist0 = (x0 - rayOrigin.x) * (x0 - rayOrigin.x) + 167 | (y0 - rayOrigin.y) * (y0 - rayOrigin.y) + 168 | (z0 - rayOrigin.z) * (z0 - rayOrigin.z); 169 | 170 | var x1 = rayOrigin[idx1]; 171 | var y1 = rayOrigin[idx1 + 1]; 172 | var z1 = rayOrigin[idx1 + 2]; 173 | 174 | var dist1 = (x1 - rayOrigin.x) * (x1 - rayOrigin.x) + 175 | (y1 - rayOrigin.y) * (y1 - rayOrigin.y) + 176 | (z1 - rayOrigin.z) * (z1 - rayOrigin.z); 177 | return dist0 - dist1; 178 | } 179 | } 180 | 181 | function init(points) { 182 | verifyPointsInvariant(points); 183 | originalArray = points; 184 | root = createRootNode(points); 185 | for (var i = 0; i < points.length; i += 3) { 186 | root.insert(i, originalArray, 0); 187 | } 188 | } 189 | 190 | function initAsync(points, doneCallback) { 191 | verifyPointsInvariant(points); 192 | 193 | var tempRoot = createRootNode(points); 194 | asyncFor(points, insertToRoot, doneInternal, { step: 3 }); 195 | 196 | function insertToRoot(element, i) { 197 | tempRoot.insert(i, points, 0); 198 | } 199 | 200 | function doneInternal() { 201 | originalArray = points; 202 | root = tempRoot; 203 | if (typeof doneCallback === 'function') { 204 | doneCallback(api); 205 | } 206 | } 207 | } 208 | 209 | function verifyPointsInvariant(points) { 210 | if (!points) throw new Error('Points array is required for quadtree to work'); 211 | if (typeof points.length !== 'number') throw new Error('Points should be array-like object'); 212 | if (points.length % 3 !== 0) throw new Error('Points array should consist of series of x,y,z coordinates and be multiple of 3'); 213 | } 214 | 215 | function getBounds() { 216 | if (!root) return EmptyRegion; 217 | return root.bounds; 218 | } 219 | 220 | function createRootNode(points) { 221 | // Edge case deserves empty region: 222 | if (points.length === 0) { 223 | var empty = new Bounds3(); 224 | return new TreeNode(empty); 225 | } 226 | 227 | // Otherwise let's figure out how big should be the root region 228 | var minX = Number.POSITIVE_INFINITY; 229 | var minY = Number.POSITIVE_INFINITY; 230 | var minZ = Number.POSITIVE_INFINITY; 231 | var maxX = Number.NEGATIVE_INFINITY; 232 | var maxY = Number.NEGATIVE_INFINITY; 233 | var maxZ = Number.NEGATIVE_INFINITY; 234 | for (var i = 0; i < points.length; i += 3) { 235 | var x = points[i], 236 | y = points[i + 1], 237 | z = points[i + 2]; 238 | if (x < minX) minX = x; 239 | if (x > maxX) maxX = x; 240 | if (y < minY) minY = y; 241 | if (y > maxY) maxY = y; 242 | if (z < minZ) minZ = z; 243 | if (z > maxZ) maxZ = z; 244 | } 245 | 246 | // Make bounds square: 247 | var side = Math.max(Math.max(maxX - minX, maxY - minY), maxZ - minZ); 248 | // since we need to have both sides inside the area, let's artificially 249 | // grow the root region: 250 | side += 2; 251 | minX -= 1; 252 | minY -= 1; 253 | minZ -= 1; 254 | var half = side / 2; 255 | 256 | var bounds = new Bounds3(minX + half, minY + half, minZ + half, half); 257 | return new TreeNode(bounds); 258 | } 259 | } 260 | 261 | -------------------------------------------------------------------------------- /lib/bounds3.js: -------------------------------------------------------------------------------- 1 | module.exports = Bounds3; 2 | 3 | function Bounds3(x, y, z, half) { 4 | this.x = typeof x === 'number' ? x : 0; 5 | this.y = typeof y === 'number' ? y : 0; 6 | this.z = typeof z === 'number' ? z : 0; 7 | this.half = typeof half === 'number' ? half : 0; 8 | } 9 | 10 | Bounds3.prototype.contains = function contains(x, y, z) { 11 | var half = this.half; 12 | return this.x - half <= x && x < this.x + half && 13 | this.y - half <= y && y < this.y + half && 14 | this.z - half <= z && z < this.z + half; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /lib/treeNode.js: -------------------------------------------------------------------------------- 1 | var Bounds3 = require('./bounds3.js'); 2 | var MAX_ITEMS = 4; 3 | 4 | module.exports = TreeNode; 5 | 6 | function TreeNode(bounds) { 7 | this.bounds = bounds; 8 | this.q0 = null; 9 | this.q1 = null; 10 | this.q2 = null; 11 | this.q3 = null; 12 | this.q4 = null; 13 | this.q5 = null; 14 | this.q6 = null; 15 | this.q7 = null; 16 | this.items = null; 17 | } 18 | 19 | TreeNode.prototype.subdivide = function subdivide() { 20 | var bounds = this.bounds; 21 | var quarter = bounds.half / 2; 22 | 23 | this.q0 = new TreeNode(new Bounds3(bounds.x - quarter, bounds.y - quarter, bounds.z - quarter, quarter)); 24 | this.q1 = new TreeNode(new Bounds3(bounds.x + quarter, bounds.y - quarter, bounds.z - quarter, quarter)); 25 | this.q2 = new TreeNode(new Bounds3(bounds.x - quarter, bounds.y + quarter, bounds.z - quarter, quarter)); 26 | this.q3 = new TreeNode(new Bounds3(bounds.x + quarter, bounds.y + quarter, bounds.z - quarter, quarter)); 27 | this.q4 = new TreeNode(new Bounds3(bounds.x - quarter, bounds.y - quarter, bounds.z + quarter, quarter)); 28 | this.q5 = new TreeNode(new Bounds3(bounds.x + quarter, bounds.y - quarter, bounds.z + quarter, quarter)); 29 | this.q6 = new TreeNode(new Bounds3(bounds.x - quarter, bounds.y + quarter, bounds.z + quarter, quarter)); 30 | this.q7 = new TreeNode(new Bounds3(bounds.x + quarter, bounds.y + quarter, bounds.z + quarter, quarter)); 31 | }; 32 | 33 | TreeNode.prototype.insert = function insert(idx, array, depth) { 34 | var isLeaf = this.q0 === null; 35 | if (isLeaf) { 36 | // TODO: this memory could be recycled to avoid GC 37 | if (this.items === null) { 38 | this.items = [idx]; 39 | } else { 40 | this.items.push(idx); 41 | } 42 | if (this.items.length >= MAX_ITEMS && depth < 16) { 43 | this.subdivide(); 44 | for (var i = 0; i < this.items.length; ++i) { 45 | this.insert(this.items[i], array, depth + 1); 46 | } 47 | this.items = null; 48 | } 49 | } else { 50 | var x = array[idx], 51 | y = array[idx + 1], 52 | z = array[idx + 2]; 53 | var bounds = this.bounds; 54 | var quadIdx = 0; // assume NW 55 | if (x > bounds.x) { 56 | quadIdx += 1; // nope, we are in E part 57 | } 58 | if (y > bounds.y) { 59 | quadIdx += 2; // Somewhere south. 60 | } 61 | if (z > bounds.z) { 62 | quadIdx += 4; // Somewhere far 63 | } 64 | 65 | var child = getChild(this, quadIdx); 66 | child.insert(idx, array, depth + 1); 67 | } 68 | }; 69 | 70 | TreeNode.prototype.query = function queryBounds(results, sourceArray, intersects, preciseCheck) { 71 | if (!intersects(this.bounds)) return; 72 | var items = this.items; 73 | var needsCheck = typeof preciseCheck === 'function'; 74 | if (items) { 75 | for (var i = 0; i < items.length; ++i) { 76 | var idx = items[i]; 77 | if (needsCheck) { 78 | if (preciseCheck(sourceArray[idx], sourceArray[idx + 1], sourceArray[idx + 2])) { 79 | results.push(idx); 80 | } 81 | } else { 82 | results.push(idx); 83 | } 84 | } 85 | } 86 | 87 | if (!this.q0) return; 88 | 89 | this.q0.query(results, sourceArray, intersects, preciseCheck); 90 | this.q1.query(results, sourceArray, intersects, preciseCheck); 91 | this.q2.query(results, sourceArray, intersects, preciseCheck); 92 | this.q3.query(results, sourceArray, intersects, preciseCheck); 93 | this.q4.query(results, sourceArray, intersects, preciseCheck); 94 | this.q5.query(results, sourceArray, intersects, preciseCheck); 95 | this.q6.query(results, sourceArray, intersects, preciseCheck); 96 | this.q7.query(results, sourceArray, intersects, preciseCheck); 97 | }; 98 | 99 | function getChild(node, idx) { 100 | if (idx === 0) return node.q0; 101 | if (idx === 1) return node.q1; 102 | if (idx === 2) return node.q2; 103 | if (idx === 3) return node.q3; 104 | if (idx === 4) return node.q4; 105 | if (idx === 5) return node.q5; 106 | if (idx === 6) return node.q6; 107 | if (idx === 7) return node.q7; 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yaot", 3 | "version": "1.1.3", 4 | "description": "Yet another octree", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap test/*.js", 8 | "start": "browserify demo/octree.js > demo/bundle_tree.js && browserify demo/raycaster.js > demo/bundle_ray.js", 9 | "perf": "npm version && node perf/index.js" 10 | }, 11 | "keywords": [ 12 | "quadtree", 13 | "octree", 14 | "oct", 15 | "tree", 16 | "octrie", 17 | "quadtrie" 18 | ], 19 | "author": "Andrei Kashcha", 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/anvaka/yaot" 24 | }, 25 | "dependencies": { 26 | "rafor": "^1.0.2" 27 | }, 28 | "devDependencies": { 29 | "benchmark": "^1.0.0", 30 | "tap": "^1.3.2", 31 | "three": "^0.71.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /perf/index.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var suite = new Benchmark.Suite; 3 | var createTree = require('../'); 4 | var queryTree = createQueryTree(100000); 5 | 6 | // add tests 7 | suite.add('init tree with 10k points', function() { 8 | createQueryTree(10000); 9 | }).add('Intersect sphere (r = 20) tree with 100k points', function() { 10 | var count = 100000; 11 | queryTree.intersectSphere( 12 | getRandomPoint(count), 13 | getRandomPoint(count), 14 | getRandomPoint(count), 15 | 20 16 | ); 17 | }).add('Intersect sphere (r = 200) tree with 100k points', function() { 18 | var count = 100000; 19 | queryTree.intersectSphere( 20 | getRandomPoint(count), 21 | getRandomPoint(count), 22 | getRandomPoint(count), 23 | 200 24 | ); 25 | }).add('Intersect sphere (r = 0) tree with 100k points', function() { 26 | var count = 100000; 27 | queryTree.intersectSphere( 28 | getRandomPoint(count), 29 | getRandomPoint(count), 30 | getRandomPoint(count), 31 | 0 32 | ); 33 | }).add('Intersect ray shot from the center into random direction', function() { 34 | var count = 100000; 35 | var rayDirection = { 36 | x: Math.random() - 0.5, 37 | y: Math.random() - 0.5, 38 | z: Math.random() - 0.5 39 | } 40 | queryTree.intersectRay({x : 0, y: 0, z: 0}, rayDirection); 41 | }).add('Intersect ray shot from the edge into center', function() { 42 | var count = 100000; 43 | var rayDirection = { 44 | x: -1, 45 | y: 0, 46 | z: 0 47 | } 48 | queryTree.intersectRay({x : 100000, y: 0, z: 0}, rayDirection); 49 | }).add('Intersect ray shot from the edge outside', function() { 50 | var count = 100000; 51 | var rayDirection = { 52 | x: 1, 53 | y: 0, 54 | z: 0 55 | } 56 | queryTree.intersectRay({x : 100000, y: 0, z: 0}, rayDirection); 57 | }) 58 | .on('cycle', function(event) { 59 | console.log(String(event.target)); 60 | }) 61 | .on('complete', function() { 62 | console.log('Done!'); 63 | }) 64 | .run({ 'async': true }); 65 | 66 | function createPoints(count) { 67 | var array = new Array(count * 3); 68 | for (var i = 0; i < count; ++i) { 69 | var idx = i * 3; 70 | array[idx] = getRandomPoint(count); 71 | array[idx + 1] = getRandomPoint(count); 72 | array[idx + 2] = getRandomPoint(count); 73 | } 74 | 75 | return array; 76 | } 77 | 78 | function getRandomPoint(count) { 79 | return 2 * Math.random() * count - count; 80 | } 81 | 82 | function createQueryTree(count) { 83 | var points = createPoints(count); 84 | var tree = createTree(); 85 | tree.init(points); 86 | return tree; 87 | } 88 | -------------------------------------------------------------------------------- /perf/results.txt: -------------------------------------------------------------------------------- 1 | 2 | > yaot@1.1.2 perf /Users/anvaka/projects/ngraphjs/experiments/yaot 3 | > npm version && node perf/index.js 4 | 5 | { yaot: '1.1.2', 6 | npm: '2.10.1', 7 | http_parser: '2.3', 8 | modules: '14', 9 | node: '0.12.4', 10 | openssl: '1.0.1m', 11 | uv: '1.5.0', 12 | v8: '3.28.71.19', 13 | zlib: '1.2.8' } 14 | init tree with 10k points x 186 ops/sec ±2.46% (78 runs sampled) 15 | Intersect sphere (r = 20) tree with 100k points x 336,607 ops/sec ±1.29% (91 runs sampled) 16 | Intersect sphere (r = 200) tree with 100k points x 295,587 ops/sec ±1.16% (82 runs sampled) 17 | Intersect sphere (r = 0) tree with 100k points x 6,421,144 ops/sec ±0.99% (86 runs sampled) 18 | Intersect ray shot from the center into random direction x 10,477 ops/sec ±1.30% (90 runs sampled) 19 | Intersect ray shot from the edge into center x 18,417 ops/sec ±1.74% (86 runs sampled) 20 | Intersect ray shot from the edge outside x 259,737 ops/sec ±1.40% (83 runs sampled) 21 | Done! 22 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var createTree = require('../'); 3 | 4 | test('it can query points', function (t) { 5 | var tree = createTree(); 6 | // two points: 7 | tree.init([ 8 | 0, 0, 0, 9 | 10, 0, 0 10 | ]); 11 | 12 | // sphere at 0, -1, 0 with radius 2 13 | var matches = tree.intersectSphere(0, -1, 0, 2); 14 | t.equals(matches.length, 1, 'Only one point intersects sphere'); 15 | t.equals(matches[0], 0, 'That point is and index 0'); 16 | 17 | matches = tree.intersectSphere(0, -3, 0, 2); 18 | t.equals(matches.length, 0, 'There are no points at 0, -3, 0'); 19 | 20 | matches = tree.intersectSphere(0, 0, 0, 20); 21 | t.equals(matches.length, 2, 'Sphere with r=20 captures all'); 22 | t.equals(matches[0], 0, 'First point is here'); 23 | t.equals(matches[1], 3, 'Second point is here'); 24 | 25 | t.end(); 26 | }); 27 | 28 | test('it can intersect ray', function(t) { 29 | var tree = createTree(); 30 | // two points: 31 | tree.init([ 32 | 0, 0, 0, 33 | 10, 0, 0 34 | ]); 35 | 36 | var rayOrigin = { 37 | x: 1, y: 0, z: 0 38 | }; 39 | var rayDirection = { 40 | x: -1, y: 0, z: 0 41 | }; 42 | var matches = tree.intersectRay(rayOrigin, rayDirection) 43 | t.equals(matches.length, 2, 'Ray intersects both points'); 44 | t.equals(matches[0], 0, 'First point is at index 0'); 45 | t.equals(matches[1], 3, 'Second point is at index 3'); 46 | 47 | // Let's shoot at the same direction, but put `near` after the first point: 48 | var near = 2; 49 | matches = tree.intersectRay(rayOrigin, rayDirection, near) 50 | t.equals(matches.length, 1, 'Ray intersects only one point'); 51 | t.equals(matches[0], 3, 'That point is at index 3'); 52 | 53 | // now let's limit far 54 | var far = 5; 55 | matches = tree.intersectRay(rayOrigin, rayDirection, near, far) 56 | t.equals(matches.length, 0, 'No points intersect'); 57 | 58 | // relax the near and far: 59 | near = 0.5; 60 | far = 15; 61 | matches = tree.intersectRay(rayOrigin, rayDirection, near, far) 62 | t.equals(matches.length, 2, 'Ray intersects both points with near and far'); 63 | t.equals(matches[0], 0, 'First point is at index 0'); 64 | t.equals(matches[1], 3, 'Second point is at index 3'); 65 | t.end(); 66 | }); 67 | --------------------------------------------------------------------------------