├── .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 [](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 |
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 |
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 |
--------------------------------------------------------------------------------