├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── gulpfile.js ├── index.html ├── package.json ├── src ├── voctopus.benchmark.js ├── voctopus.core.js ├── voctopus.kernel.asm.js └── voctopus.util.js ├── test ├── voctopus.core.test.js ├── voctopus.kernel.asm.test.js └── voctopus.util.test.js └── voctopus.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | node_modules 10 | dist 11 | coverage 12 | 13 | # Packages # 14 | ############ 15 | # it's better to unpack these files and commit the raw source 16 | # git has its own built in compression methods 17 | *.7z 18 | *.dmg 19 | *.gz 20 | *.iso 21 | *.jar 22 | *.rar 23 | *.tar 24 | *.zip 25 | 26 | # Logs and databases # 27 | ###################### 28 | *.log 29 | 30 | # OS generated files # 31 | ###################### 32 | .DS_Store? 33 | ehthumbs.db 34 | Icon? 35 | Thumbs.db 36 | 37 | # Netbeans # 38 | ############ 39 | nbproject 40 | 41 | # VIM # 42 | ####### 43 | *.swo 44 | *.swp 45 | tags 46 | .sync 47 | TAGS 48 | 49 | # Misc # 50 | ######## 51 | .goutputstream* 52 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "doc"] 2 | path = doc 3 | url = git@github.com:/nphyx/voctopus.wiki.git 4 | [submodule "docs"] 5 | path = docs 6 | url = git@github.com:/nphyx/voctopus.wiki.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Justen Robertson 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Voctopus 0.1.0 2 | ============== 3 | Voctopus is a Javascript implementation of a sparse voxel octree. The end goal is to create a data structure small and fast 4 | enough that it can be rendered directly by a shader in WebGL without meshification, using ray tracing techniques (or something 5 | analogous to that - I'm currently experimenting with a mix of ideas taken from cone tracing, path tracing and depth marching). 6 | The shader code is still a work in progress, and will be released as a separate repository along with the rest of the tools 7 | necessary to create and manipulate scenes. 8 | 9 | Architecture 10 | ------------ 11 | Voctopus has two main pieces: a low-level kernel written in asm.js and a high-level javascript API for use by humans. 12 | 13 | Memory management, access, traversal, and other core operations are handled by the kernel, yielding real-world performance 14 | of over one million (and up to 8 million) r/w operations per second on commodity hardware in a small memory footprint. See the 15 | [benchmarks](https://github.com/nphyx/voctopus/wiki/benchmark) for performance details. 16 | 17 | The kernel currently supports 32-bit voxels representing either a 31-bit subtree pointer or 24 bits of RGB color, a 4 bit alpha 18 | channel, and 3 bits reserved for rendering instructions (the remaining bit is a flag indicating whether the entry is a pointer 19 | or voxel data). The non-kernel code is agnostic, such that kernels with other data structures (e.g. for indexed color) could 20 | be supported. 21 | 22 | Todo 23 | ---- 24 | * prune dead octets and free for reallocation - incomplete 25 | * defragmentation (maybe not neccessary) 26 | * ray intersection - partially complete, weird test results 27 | 28 | Voctopus is still a work in progress! Neither the API nor the data structure should be considered stable. 29 | 30 | Current documentation is available in the [project wiki](https://github.com/nphyx/voctopus/wiki/voctopus.core). Examples and demos 31 | will follow as the code stabilizies - they're currently not in useful shape. 32 | 33 | License (MIT) 34 | ============= 35 | Copyright 2015 Justen Robertson / https://github.com/nphyx. 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var gulp = require("gulp"); 3 | // test suites 4 | var babelRegister = require("babel-core/register"); 5 | var exec = require("child_process").exec; 6 | var mocha = require("gulp-mocha"); 7 | var istanbul = require("gulp-babel-istanbul"); 8 | gulp.task("default", function(cb) { 9 | exec("browserify -t babelify voctopus.js | uglifyjs > dist/voctopus.ugly.js", function(err, stdout, stderr) { 10 | console.log(stderr); 11 | console.log(stdout); 12 | cb(err); 13 | }); 14 | }); 15 | 16 | gulp.task("kernel", function(cb) { 17 | exec("browserify -t babelify src/voctopus.kernel.asm.js > dist/voctopus.kernel.js", function(err, stdout, stderr) { 18 | console.log(stderr); 19 | console.log(stdout); 20 | cb(err); 21 | }); 22 | }); 23 | 24 | gulp.task("docs", function (cb) { 25 | exec("jsdox --templateDir docs/templates --output docs src/*.js", function(err, stdout, stderr) { 26 | console.log(stderr); 27 | console.log(stdout); 28 | cb(err); 29 | }); 30 | }); 31 | 32 | gulp.task("benchmark", function(cb) { 33 | exec("browserify -t babelify src/voctopus.benchmark.js | uglifyjs > dist/voctopus.benchmark.js && node dist/voctopus.benchmark.js | tee docs/benchmark.md", function(err, stdout, stderr) { 34 | //exec("browserify -t babelify src/voctopus.benchmark.js > dist/voctopus.benchmark.js && node dist/voctopus.benchmark.js | tee docs/benchmark.md", function(err, stdout, stderr) { 35 | console.log(stderr); 36 | console.log(stdout); 37 | cb(err); 38 | }); 39 | }); 40 | 41 | gulp.task("test", function() { 42 | return gulp.src(["test/*.js"]) 43 | .pipe(mocha({ 44 | bail:true, 45 | compilers: { 46 | js: babelRegister 47 | } 48 | })) 49 | }); 50 | 51 | gulp.task("test:core", function() { 52 | return gulp.src(["test/voctopus.core.test.js"]) 53 | .pipe(mocha({ 54 | bail:true, 55 | compilers: { 56 | js: babelRegister 57 | } 58 | })) 59 | }); 60 | 61 | gulp.task("test:util", function() { 62 | return gulp.src(["test/voctopus.util.test.js"]) 63 | .pipe(mocha({ 64 | bail:true, 65 | compilers: { 66 | js: babelRegister 67 | } 68 | })) 69 | }); 70 | 71 | gulp.task("test:schema", function() { 72 | return gulp.src(["test/voctopus.schema.test.js"]) 73 | .pipe(mocha({ 74 | bail:true, 75 | compilers: { 76 | js: babelRegister 77 | } 78 | })) 79 | }); 80 | 81 | gulp.task("test:kernel", function() { 82 | return gulp.src(["test/voctopus.kernel.asm.test.js"]) 83 | .pipe(mocha({ 84 | bail:true, 85 | compilers: { 86 | js: babelRegister 87 | } 88 | })) 89 | }); 90 | 91 | gulp.task("test:coverage", function(cb) { 92 | gulp.src(["src/*js"]) 93 | .pipe(istanbul()) 94 | .pipe(istanbul.hookRequire()) 95 | .on("finish", function() { 96 | gulp.src(["test/*.js"]) 97 | .pipe(mocha({ 98 | bail:true, 99 | compilers: { 100 | js:babelRegister 101 | } 102 | })) 103 | .pipe(istanbul.writeReports()) 104 | .on("end", cb) 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nphyx/voctopus", 3 | "version": "0.1.0", 4 | "repository": "https://github.com/nphyx/voctopus", 5 | "description": "An experimental implementation of Sparse Voxel Octrees in javascript.", 6 | "author": "Justen Robertson ", 7 | "main": "", 8 | "scripts": { 9 | "doc": "jsdoc -c doc/conf.json -d doc" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "babel-core": "^6.0.0", 14 | "babel-loader": "^6.0.0", 15 | "babelify": "^7.2.0", 16 | "gulp": "^3.9.1", 17 | "gulp-babel": "^6.0.0", 18 | "gulp-babel-istanbul": "^1.00.0", 19 | "gulp-inject-modules": "^0.1.1", 20 | "gulp-mocha": "^2.2.0", 21 | "gulp-rename": "^1.2.2", 22 | "jsdox": "*", 23 | "jshint": "*", 24 | "mocha": "*", 25 | "should": "^11.2.0" 26 | }, 27 | "jshintConfig": { 28 | "curly": false, 29 | "latedef": true, 30 | "newcap": true, 31 | "quotmark": "double", 32 | "undef": true, 33 | "unused": true, 34 | "globalstrict": true, 35 | "asi": true, 36 | "esnext": true, 37 | "browser": true, 38 | "devel": true, 39 | "node": true, 40 | "worker": true, 41 | "globals": { 42 | "chai": false, 43 | "describe": false, 44 | "define": false, 45 | "it": false, 46 | "xit": false, 47 | "before": false, 48 | "beforeEach": false, 49 | "after": false, 50 | "afterEach": false 51 | } 52 | }, 53 | "babel": { 54 | "presets": [ 55 | "es2015" 56 | ] 57 | }, 58 | "license": "MIT" 59 | } 60 | -------------------------------------------------------------------------------- /src/voctopus.benchmark.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Voctopus = require("./voctopus.core.js").Voctopus; 3 | const loop3D = require("./voctopus.util.js").loop3D; 4 | // use a lot of globals because it has less impact on run time 5 | const testList = ["voxel", "direct", "octet"]; 6 | var tmpvec = new Float32Array(3); 7 | var voc = new Voctopus(1); 8 | var tmpOctet = [ 9 | Object.create(voc.voxel), 10 | Object.create(voc.voxel), 11 | Object.create(voc.voxel), 12 | Object.create(voc.voxel), 13 | Object.create(voc.voxel), 14 | Object.create(voc.voxel), 15 | Object.create(voc.voxel), 16 | Object.create(voc.voxel) 17 | ]; 18 | /* Setup */ 19 | const tests = { 20 | voxel:{ 21 | read:(voc, pos, out) => voc.getVoxel(pos, out), 22 | write:(voc, pos, i) => voc.setVoxel(pos, {r:i,g:i+1,b:i+2,m:i+3}) 23 | }, 24 | direct:{ 25 | read:(voc, pos, out) => { 26 | let ptr = voc.traverse(pos); 27 | out = voc.get(ptr); 28 | }, 29 | write:(voc, pos, i) => { 30 | let ptr = voc.init(pos); 31 | voc.set(ptr, i, i+1, i+2, i+3, i+4>>4); 32 | } 33 | }, 34 | octet:{ 35 | read: (voc, pos, out) => { 36 | tmpvec[0] = pos[0]*2; tmpvec[1] = pos[1]*2; tmpvec[2] = pos[2]*2; 37 | voc.getOctet(tmpvec, undefined, out); 38 | }, 39 | write:(voc, pos, i) => { 40 | tmpvec[0] = pos[0]*2; tmpvec[1] = pos[1]*2; tmpvec[2] = pos[2]*2; 41 | tmpOctet[0].r = tmpOctet[0].g = tmpOctet[0].b = tmpOctet[0].a = i+0; 42 | tmpOctet[1].r = tmpOctet[1].g = tmpOctet[1].b = tmpOctet[1].a = i+1; 43 | tmpOctet[2].r = tmpOctet[2].g = tmpOctet[2].b = tmpOctet[2].a = i+2; 44 | tmpOctet[3].r = tmpOctet[3].g = tmpOctet[3].b = tmpOctet[3].a = i+3; 45 | tmpOctet[4].r = tmpOctet[4].g = tmpOctet[4].b = tmpOctet[4].a = i+4; 46 | tmpOctet[5].r = tmpOctet[5].g = tmpOctet[5].b = tmpOctet[5].a = i+5; 47 | tmpOctet[6].r = tmpOctet[6].g = tmpOctet[6].b = tmpOctet[6].a = i+6; 48 | tmpOctet[7].r = tmpOctet[7].g = tmpOctet[7].b = tmpOctet[7].a = i+7; 49 | voc.setOctet(tmpvec, tmpOctet); 50 | } 51 | } 52 | } 53 | 54 | /* util functions */ 55 | let fmt = (cellw, cells) => { 56 | return "|"+cells.map((cell) => ((" ").repeat(cellw)+cell).slice(-cellw)) 57 | .join("|")+"|"; 58 | } 59 | 60 | // make table dividers 61 | let divider = (cellw, cells) => cells.map((cell) => { 62 | switch(cell) { 63 | case "l": return ":"+(("-").repeat(cellw-2))+"-"; 64 | case "r": return "-"+(("-").repeat(cellw-2))+":"; 65 | case "c": return ":"+(("-").repeat(cellw-2))+":"; 66 | default: return ("-").repeat(cellw); 67 | } 68 | }); 69 | 70 | // range from a to b 71 | let range = (a, b) => { 72 | let arr = []; 73 | for(let x = a; x <= b; x++) arr.push(x); 74 | return arr; 75 | } 76 | 77 | // make table 78 | function table(cellw, rows) { 79 | let out = ""; 80 | out += "\n"+rows.map((row) => fmt(cellw, row)).join("\n"); 81 | return out; 82 | } 83 | 84 | 85 | // shortcut for Date().getTime(); 86 | let time = () => new Date().getTime(); 87 | 88 | // calculate time elapsed 89 | let elapsed = (start) => (time()-start)/1000 90 | let fmtTime = (time) => time.toFixed(3)+"s"; 91 | 92 | // timer wrapper 93 | let stopwatch = (cb) => { 94 | let start = time(); 95 | cb(); 96 | return fmtTime(elapsed(start)); 97 | } 98 | 99 | // calculate octets in voctopus 100 | let calcOctets = (voc) => { 101 | let usedBytes = voc.buffer.byteLength-(voc.buffer.byteLength-voc.nextOffset); 102 | return ~~(usedBytes / voc.octetSize - 1); 103 | } 104 | 105 | // bytes as mb 106 | let inMB = (b) => (b/1024/1024).toFixed(3); 107 | 108 | /** 109 | * Iterate through dmin to dmax, calling callback and putting the result in an array. 110 | * @param {int} dmin start iteration at 111 | * @param {int} dmax stop iteration at 112 | * @param {function} cb callback 113 | * @return {array} 114 | */ 115 | let iterd = (dmin, dmax, cb) => { 116 | let results = []; 117 | for(let d = dmin; d <= dmax; ++d) { 118 | results.push(cb(d)); 119 | } 120 | return results; 121 | } 122 | 123 | function cbInst(d) { 124 | return stopwatch(() => new Voctopus(d)); 125 | } 126 | 127 | function cbExpand(d) { 128 | let voc = new Voctopus(d); 129 | return stopwatch(() => { 130 | let res = 1; 131 | while(res) res = voc.expand(); 132 | }); 133 | } 134 | 135 | function cbInit(d) { 136 | let voc = new Voctopus(d); 137 | let size = Math.pow(2, d - 1); 138 | let start = time(); 139 | loop3D(size, {z:(pos) => voc.init(pos)}); 140 | return fmtTime(elapsed(start)); 141 | } 142 | 143 | function cbWalk(d) { 144 | let voc = new Voctopus(d); 145 | let size = Math.pow(2, d - 1); 146 | let start = time(); 147 | loop3D(size, {z:(pos) => voc.walk(pos, true)}); 148 | return fmtTime(elapsed(start)); 149 | } 150 | 151 | function loop(voc, read, write, dims, out) { 152 | var start, rtime, wtime, i = 0, count = 0; 153 | var pos = new Uint32Array(3); 154 | 155 | start = time(); 156 | for(pos[0] = 0; pos[0] < dims; ++pos[0]) { 157 | for(pos[1] = 0; pos[1] < dims; ++pos[1]) { 158 | i = 0; 159 | for(pos[2] = 0; pos[2] < dims; ++pos[2]) { 160 | write(voc, pos, i); 161 | ++i; 162 | ++count; 163 | } 164 | } 165 | } 166 | wtime = elapsed(start); 167 | 168 | start = time(); 169 | for(pos[0] = 0; pos[0] < dims; ++pos[0]) { 170 | for(pos[1] = 0; pos[1] < dims; ++pos[1]) { 171 | for(pos[2] = 0; pos[2] < dims; ++pos[2]) { 172 | read(voc, pos, out); 173 | } 174 | } 175 | } 176 | rtime = elapsed(start); 177 | return {rtime, wtime, count}; 178 | } 179 | 180 | function testMem(d) { 181 | var voc = new Voctopus(d); 182 | var out = [ 183 | Object.create(voc.voxel), 184 | Object.create(voc.voxel), 185 | Object.create(voc.voxel), 186 | Object.create(voc.voxel), 187 | Object.create(voc.voxel), 188 | Object.create(voc.voxel), 189 | Object.create(voc.voxel), 190 | Object.create(voc.voxel) 191 | ]; 192 | 193 | var {rtime, wtime, count} = loop(voc, tests.octet.read, tests.octet.write, voc.dimensions/2, out); 194 | 195 | // fudge for octet test 196 | count *= 8; 197 | return [d, voc.dimensions+"^3", fmtTime(rtime), fmtTime(wtime), count, calcOctets(voc), inMB(voc.view.byteLength)+"MB"]; 198 | } 199 | 200 | function testRW(testName, d) { 201 | var voc = new Voctopus(d); 202 | var res = 1, dims, out; 203 | // expand it first so it won't get slowed down arbitrarily 204 | while(res) res = voc.expand(); 205 | if(testName == "octet") { 206 | dims = Math.ceil(voc.dimensions/2); 207 | out = [ 208 | Object.create(voc.voxel), 209 | Object.create(voc.voxel), 210 | Object.create(voc.voxel), 211 | Object.create(voc.voxel), 212 | Object.create(voc.voxel), 213 | Object.create(voc.voxel), 214 | Object.create(voc.voxel), 215 | Object.create(voc.voxel) 216 | ]; 217 | } 218 | else { 219 | dims = voc.dimensions; 220 | out = Object.create(voc.voxel); 221 | } 222 | var {rtime, wtime, count} = loop(voc, tests[testName].read, tests[testName].write, dims, out); 223 | 224 | // fudge for octet test 225 | if(testName == "octet") { 226 | count *= 8; 227 | dims *= 2; 228 | } 229 | return [testName, fmtTime(rtime), fmtTime(wtime), Math.round((1/rtime)*count), Math.round((1/wtime)*count), dims+"^3", count]; 230 | } 231 | 232 | function benchmark() { 233 | let cellw = 8, i = 0, rows = [], depth = 8; 234 | console.log("\nBEGIN BENCHMARK"); 235 | console.log( "==============="); 236 | // Initialization benchmarks 237 | console.log("\nInitialization Tests\n--------------------"); 238 | rows.push(["Depth"].concat(range(3, depth))); 239 | rows.push(divider(cellw, new Array(depth-1).fill("r"))); 240 | rows.push(["Create"].concat(iterd(3, depth, cbInst.bind(null)))); 241 | rows.push(["Expand"].concat(iterd(3, depth, cbExpand.bind(null)))); 242 | rows.push(["Init"].concat(iterd(3, depth, cbInit.bind(null)))); 243 | rows.push(["Walk"].concat(iterd(3, depth, cbWalk.bind(null)))); 244 | console.log(table(cellw, rows)); 245 | 246 | const doTest = (testName) => rows.push(testRW(testName, depth)); 247 | // Read/Write Benchmarks 248 | console.log("\nR/W Tests Depth "+depth) 249 | console.log( "-----------------"); 250 | rows = []; 251 | rows.push(["Type", "Read", "Write", "R/s", "W/s", "Dims", "Voxels"]); 252 | rows.push(divider(cellw, ["r","r","r","r","r","r","r"])); 253 | testList.forEach(doTest); 254 | console.log(table(cellw, rows)); 255 | 256 | console.log("\nMemory Tests\n------------"); 257 | rows = []; 258 | rows.push(["Depth", "Dims", "Read", "Write", "Voxels", "Octets", "Memory"]); 259 | rows.push(divider(cellw, ["c","r","r","r","r","r","r"])); 260 | 261 | i = 5; 262 | for(let max = depth; i <= max; ++i) { 263 | rows.push(testMem(i)); 264 | } 265 | console.log(table(cellw, rows)); 266 | } 267 | 268 | console.log(` 269 | Benchmarks 270 | ========== 271 | This page includes some benchmarks run against the current version of Voctopus. 272 | They're useful as a way to measure the impact of code changes. A brief 273 | description of each test suite follows. 274 | 275 | Init Tests 276 | ---------- 277 | These benchmarks cover certain maintenance operations: 278 | * Create: the time it takes to instantiate a new Voctopus 279 | * Expand: how long it takes to expand a buffer to maximum size 280 | * Init: the total time to initialize every voxel in the octree to full depth 281 | * Walk: the total time to walk to each voxel in the tree 282 | 283 | R/W Tests 284 | --------- 285 | These tests measure how long reads and writes take using different interfaces. The 286 | octree is expanded to full size so that expansions won't interrupt r/w. 287 | 288 | * *Object*: how long it takes to read/write using the getVoxel and setVoxel methods 289 | * *Direct*: time to read/write using the direct getter/setter methods (Voc.set[field]) 290 | * *Octet*: time to read/write using the octet batch write (Voc.set[field]) 291 | 292 | Memory Tests 293 | ------------ 294 | These tests measure r/w speeds without expansion, and how much memory is consumed 295 | by Voctopus' on-demand buffer expansion. The direct-write interfaces are used here. 296 | `); 297 | 298 | benchmark(); 299 | -------------------------------------------------------------------------------- /src/voctopus.core.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*jshint strict:false */ 3 | /*jshint globalstrict:true */ 4 | /*jshint latedef:nofunc */ 5 | const {VoctopusKernel, VK_FO} = require("./voctopus.kernel.asm.js"); 6 | const {sump8, npot} = require("../src/voctopus.util"); 7 | const MAX_BUFFER = 1024*1024*1024*512; 8 | const create = Object.create; 9 | export const voxel = { 10 | r:0, 11 | g:0, 12 | b:0, 13 | a:0, 14 | } 15 | 16 | /** 17 | * Voctopus Core 18 | * ============= 19 | * 20 | * This contains the core Voctopus object. 21 | * 22 | * Quick definition of terms: 23 | * 24 | * * 1 octant = one voxel at a given depth 25 | * * 1 octet = 8 octants, or one tree node 26 | * 27 | * @module voctopus.core 28 | */ 29 | 30 | /** 31 | * The Voctopus constructor. Accepts a maximum depth value and a schema (see the schemas 32 | * module for more info). 33 | * @param {int} depth maximum depth of tree 34 | * @param {Array} schema data 35 | * @return {Voctopus} 36 | */ 37 | export function Voctopus(depth) { 38 | if(!depth) throw new Error("Voctopus#constructor must be given a depth"); 39 | var buffer, view, octantSize, octetSize, firstOffset, startSize, maxSize, dimensions, kernel; 40 | 41 | function octet() { 42 | return [ 43 | create(voxel), 44 | create(voxel), 45 | create(voxel), 46 | create(voxel), 47 | create(voxel), 48 | create(voxel), 49 | create(voxel), 50 | create(voxel) 51 | ]; 52 | } 53 | 54 | /** 55 | * calculate the size of a single octant based on the sum of lengths of properties 56 | * in the schema. the size of an octant is just the size of 8 octets 57 | */ 58 | octantSize = 8; // bytes for a single octet 59 | octetSize = octantSize * 8; 60 | firstOffset = VK_FO; 61 | maxSize = npot(firstOffset+sump8(depth)); 62 | dimensions = Math.pow(2, depth); 63 | 64 | // define public properties now 65 | Object.defineProperties(this, { 66 | "octetSize":{get: () => octetSize}, 67 | "octantSize":{get: () => octantSize}, 68 | "freedOctets":{value: [], enumerable: false}, 69 | "nextOffset":{get: () => kernel.getNextOffset()}, 70 | "firstOffset":{get: () => firstOffset}, 71 | "depth":{get: () => depth}, 72 | "buffer":{get: () => buffer}, 73 | "view":{get: () => view}, 74 | "maxSize":{get: () => maxSize}, 75 | "dimensions":{get: () => dimensions}, 76 | "voxel":{get: () => voxel} 77 | }); 78 | 79 | /** 80 | * Set up the ArrayBuffer as a power of two, so that it can be used as a WebGL 81 | * texture more efficiently. The minimum size should be keyed to the minimum octant 82 | * size times nine because that corresponds to a tree of depth 2, the minimum useful 83 | * tree. Optimistically assume that deeper trees will be mostly sparse, which should 84 | * be true for very large trees (and for small trees, expanding the buffer won't 85 | * be as expensive). 86 | */ 87 | startSize = npot(Math.max(0x10000, Math.min(maxSize/8, MAX_BUFFER))); 88 | 89 | // initialize the kernel 90 | buffer = new ArrayBuffer(startSize); 91 | view = new Uint8Array(buffer); 92 | kernel = new VoctopusKernel(buffer, depth-1); 93 | 94 | 95 | /** 96 | * Expands the internal storage buffer. This is a VERY EXPENSIVE OPERATION and 97 | * should be avoided until neccessary. 98 | * @return {bool} true if the voctopus was expanded, otherwise false 99 | */ 100 | this.expand = function() { 101 | var s, tmp, max; 102 | max = Math.min(MAX_BUFFER, this.maxSize); 103 | s = buffer.byteLength * 2; 104 | if(s > max) return false; 105 | tmp = new ArrayBuffer(s); 106 | tmp.transfer(buffer); 107 | kernel = new VoctopusKernel(tmp); 108 | buffer = tmp; 109 | view = new Uint8Array(buffer); 110 | return true; 111 | } 112 | 113 | /** 114 | * Gets the properties of an octant at the specified index. If the index is invalid 115 | * you'll get back garbage data, so use with care. 116 | * @param {int} index index to set 117 | * @param {voxel} out out param (defaults to instance of this.voxel) 118 | * @return {voxel} 119 | * @example 120 | * let voc = new Voctopus(5); 121 | * let index = voc.init([9,3,2]); // initializes the voxel at 9,3,2 and returns its index 122 | * voc.get(index); // {r:0,g:0,b:0,m:0} 123 | */ 124 | this.get = function(p, out = create(voxel)) { 125 | var raw = kernel.getOctant(p); 126 | out.r = kernel.rFrom(raw); 127 | out.g = kernel.gFrom(raw); 128 | out.b = kernel.bFrom(raw); 129 | out.a = kernel.aFrom(raw); 130 | return out; 131 | } 132 | 133 | /** 134 | * Sets the properties of an octant at the specified index. Be careful with using it 135 | * directly. If the index is off it will corrupt the octree. 136 | * @param {int} p index to write to 137 | * @param {int} r red channel 138 | * @param {int} g green channel 139 | * @param {int} b blue channel 140 | * @param {int} a alpha channel 141 | * @return {undefined} 142 | * @example 143 | * let voc = new Voctopus(5); 144 | * let index = voc.init([9,3,2]); // initializes the voxel at 9,3,2 and returns its index 145 | * voc.set(index, 232, 19, 224, 63); // r = 232, g = 19, b = 224, a = 64 146 | */ 147 | this.set = function(p, r, g, b, a) { 148 | var rgba = kernel.valFromRGBA(r,g,b,a); 149 | kernel.setOctant(p, rgba); 150 | return p; 151 | } 152 | 153 | this.getPointer = function(index) { 154 | return kernel.getP(index); 155 | } 156 | 157 | this.setPointer = function(index, pointer) { 158 | kernel.setP(index, pointer); 159 | } 160 | 161 | /** 162 | * Initializes a voxel at the supplied vector and branch depth, walking down the 163 | * tree and allocating voxels at each level until it hits the end. 164 | * @param {vector} v coordinate vector of the target voxel 165 | * @param {int} depth depth to initialize at (default max depth) 166 | * @return {int} index 167 | * @example 168 | * let voc = new Voctopus(5); 169 | * voc.init([9,3,2]); // 1536 (index) 170 | */ 171 | this.init = function(v, depth = this.depth) { 172 | kernel.prepareLookup(v[0], v[1], v[2], depth); 173 | return(kernel.initOctet()); 174 | } 175 | 176 | /** 177 | * Walks the octree from the root to the supplied position vector, building an 178 | * array of indices of each octet as it goes, then returns the array. Optionally 179 | * initializes octets when init = true. 180 | * @param {vector} v coordinate vector of the target voxel 181 | * @param {int} depth number of depth levels to walk through (default this.depth) 182 | * @param {int} p start pointer (defaults to start of root octet) 183 | * @return {array} indexes of octant at each branch 184 | * @example 185 | * let voc = new Voctopus(5); 186 | */ 187 | this.walk = function(v, depth = this.depth, p = kernel.getFirstOffset()) { 188 | const {step} = kernel; 189 | let stack = [p], c = 0, push = stack.push.bind(stack); 190 | kernel.prepareLookup(v[0], v[1], v[2], depth); 191 | for(let i = 0; i < depth; ++i) { 192 | c = step(); 193 | if(c) { 194 | push(c); 195 | p = c; 196 | } 197 | else return stack; 198 | } 199 | return stack; 200 | } 201 | 202 | /** 203 | * Gets the properties of a voxel at a given coordinate and (optional) depth. 204 | * @param {vector} v [x,y,z] position 205 | * @param {voxel} out out param (defaults to instance of this.voxel) 206 | * @param {depth} d max depth to read from (default max depth) 207 | * @return {voxel} 208 | * @example 209 | * var voc = new Voctopus(5, schemas.RGBM); 210 | * voc.getVoxel([13,22,1], 4); // {r:0,g:0,b:0,m:0} (index) 211 | */ 212 | this.getVoxel = function(v, out = create(this.voxel), d = this.depth) { 213 | kernel.prepareLookup(v[0], v[1], v[2], d); 214 | return this.get(kernel.seek(), out); 215 | } 216 | 217 | /** 218 | * Sets the properties of an octant at a given coordinate and (optional) depth. 219 | * @param {vector} v [x,y,z] position 220 | * @param {object} props a property object, members corresponding to the schema 221 | * @param {depth} d depth to write at (default max depth) 222 | * @return {index} pointer to the voxel that was set 223 | * @example 224 | * var voc = new Voctopus(5, schemas.RGBM); 225 | * voc.setVoxel([13,22,1], {r:122,g:187,b:1234,m:7}, 4); // 1536 (index) 226 | */ 227 | this.setVoxel = function(v, props, d = this.depth) { 228 | kernel.prepareLookup(v[0], v[1], v[2], d); 229 | return this.set(kernel.initOctet(), props.r, props.g, props.b, props.a); 230 | } 231 | 232 | 233 | /** 234 | * Steps through the tree until it finds the deepest voxel at the given coordinate, 235 | * up to the given depth. 236 | * @param {vector} v coordinate vector of the target voxel 237 | * @param {int} depth depth to initialize at (default this.depth - 1) 238 | * @return {int} index 239 | */ 240 | this.seek = function(v, depth = this.depth) { 241 | kernel.prepareLookup(v[0], v[1], v[2], depth); 242 | return kernel.seek(); 243 | } 244 | 245 | /** 246 | * Sets the data for each element in an octet. Pointers are managed automatically. 247 | * This can be a big performance boost when you have multiple voxels to write to the 248 | * same octet, since it avoids redundant traversal. Octet location is automatically 249 | * derived from the given vector so it doesn't have to point at the first voxel in 250 | * the octet. If the octet doesn't currently exist in the tree it will be initialized. 251 | * 252 | * @param {vector} v position vector for target octet 253 | * @param {array} data data to write, as an array of 8 objects (see example) 254 | * @param {depth} d depth to write at (default max depth) 255 | * @return {undefined} 256 | * @example 257 | * let voc = new Voctopus(6); 258 | * let data = array[ 259 | * {}, // members may be empty, but must be present so the indexes are correct 260 | * {r:210,g:12,b:14,a:15}, // can use all properties 261 | * {a:7}, // or 262 | * {g:82}, // any 263 | * {b:36}, // combination 264 | * {r:255}, // thereof 265 | * {}, 266 | * {} 267 | * ]; 268 | * // in this example, walk to the second-lowest depth to find the pointer 269 | * voc.set.octet([0,0,1], data); // and done! 270 | */ 271 | Voctopus.prototype.setOctet = function(v, data, d = this.depth) { 272 | let p = this.init(v, d); 273 | const set = this.set.bind(this); 274 | for(let i = 0; i < 8; ++i) { 275 | set(p+i, data[i].r, data[i].g, data[i].b, data[i].a); 276 | } 277 | } 278 | 279 | /** 280 | * Gets an array of each voxel in an octet. Pointers are managed automatically. 281 | * This can be a big performance boost when you have multiple voxels to write to the 282 | * same octet, since it avoids redundant traversal. Octet location is automatically 283 | * derived from the given vector so it doesn't have to point at the first voxel in 284 | * the octet. If the octet doesn't currently exist in the tree it will be initialized. 285 | * 286 | * @param {vector} v coordinates of voxel 287 | * @param {octet} out (optional) a collection of 8 voxels to store output 288 | * @param {depth} d (optional, default max depth) depth to write at 289 | * @return {array} array of 8 voxels ordered by identity (@see voctopus/util#octetIdentity) 290 | * @example 291 | * let voc = new Voctopus(5, schemas.I8M16P); 292 | * voc.getOctet([0,0,0], 3) 293 | * .map((voxel) => console.log(voxel)); // {m:0} (x8) 294 | */ 295 | Voctopus.prototype.getOctet = function(v, out = octet(), d = this.depth) { 296 | var p = this.seek(v, d); 297 | const get = this.get.bind(this); 298 | for(let i = 0; i < 8; ++i) { 299 | get(p+i, out[i]); 300 | } 301 | return out; 302 | } 303 | 304 | return this; 305 | } 306 | 307 | /** 308 | * Cast a ray into the octree, computing intersections along the path. 309 | * The accumulator function should be in the form `f(t, p) => bool`, where t is distance 310 | * traveled in units, p is a pointer to the voxel hit, and the return value is a boolean 311 | * indicating whether to halt traversal (true = halt, false = continue). 312 | * along ray, an 313 | * @param {vector} ro ray origin coordinates 314 | * @param {vector} rd unit vector of ray direction 315 | * @param {function} f accumulator callback 316 | * @return {bool} true if at least one voxel was hit, otherwise false 317 | * @example 318 | * var voc = new Voctopus(5); 319 | * voc.setVoxel([0,0,0], {r:232,g:0,b:232,m:0}, 0); // set the top,left,rear voxel 320 | * var ro = [16,16,-32]; // start centered 32 units back on z axis 321 | * var rd = [-0.30151, 0.30148, 0.90454]; // 30 deg left, 30 deg up, forward 322 | * var dist = 0; 323 | * var index = 0; 324 | * var cb = function(t, p) { 325 | * dist = t; 326 | * index = p; 327 | * return 1; // halt after first hit (you could return 0 to keep ) 328 | * } 329 | * voc.cast(ro, rd, cb); // 1, because at least one voxel was hit 330 | * p; // 40, pointer of the voxel at [0,0,0] 331 | * t; // 332 | * 333 | Voctopus.prototype.intersect = function(ro, rd) { 334 | // TODO: update these later on to support dynamic coordinates 335 | let end = this.dimensions - 1; 336 | let start = 0; 337 | // decompose vectors, saves time referencing 338 | let rdi = [1/rd[0], 1/rd[1], 1/rd[2]]; 339 | 340 | // find out if ray intersects outer bounding box 341 | if(rayAABB([start,start,start],[end,end,end], ro, rdi)) { 342 | // find first octant of intersection 343 | } 344 | 345 | // descend 346 | // if hit found, call accumulator 347 | // if result is zero, repeat for neighbor 348 | // return pointer 349 | } 350 | */ 351 | -------------------------------------------------------------------------------- /src/voctopus.kernel.asm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* jshint strict:false */ 3 | /* jshint unused:false */ 4 | 5 | // imul polyfill 6 | Math.imul = Math.imul || function(a, b) { 7 | var ah = (a >>> 16) & 0xffff; 8 | var al = a & 0xffff; 9 | var bh = (b >>> 16) & 0xffff; 10 | var bl = b & 0xffff; 11 | // the shift by 0 fixes the sign on the high part 12 | // the final |0 converts the unsigned value into a signed value 13 | return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)|0); 14 | }; 15 | 16 | // Restricted lib for use in voctopus kernel 17 | const kernelLib = { 18 | Math:Math, 19 | Int32Array: Int32Array, 20 | Uint32Array: Uint32Array, 21 | Float64Array:Float64Array, 22 | Uint8Array: Uint8Array 23 | } 24 | 25 | 26 | function VoctopusKernel32(stdlib, foreign, buffer) { 27 | "use asm"; 28 | // aliases 29 | var min = stdlib.Math.min; 30 | var max = stdlib.Math.max; 31 | var imul = stdlib.Math.imul; 32 | 33 | // heap for reading full octants and color values 34 | var heap32 = new stdlib.Uint32Array(buffer); 35 | // pointers for fixed locations 36 | var D_MAX = 0, // tree's preset maximum depth (set externally) 37 | P_FO = 4, // offset of root octet in tree (set externally) 38 | P_NO = 8, // next octet pointer 39 | P_CUR = 12, // current pointer during operations 40 | P_TMP = 16, // next pointer during traversals 41 | D_CUR = 20, // current depth during operations 42 | LOC_X = 24, // x destination for current op 43 | LOC_Y = 28, // y destination for current op 44 | LOC_Z = 32, // z destination for current op 45 | D_TGT = 36, // target depth 46 | D_INV = 40, // inverse of current depth 47 | V_TMP = 44, // tmp placeholder for a voxel 48 | STEPA = 48, // var used in stepIntoTree 49 | STEPB = 52, // var used in stepIntoTree 50 | TRAVA = 56, // var used in seek 51 | INITA = 60; // var used in initOctet 52 | // heap for reading 8-bit values from voxels 53 | var heap8 = new stdlib.Uint8Array(buffer); 54 | var PO_A = 0, 55 | PO_B = 1, 56 | PO_G = 2, 57 | PO_R = 3; 58 | // heap for doing floating point calculations 59 | var heapd = new stdlib.Float64Array(buffer); 60 | // pointers for heapd 61 | var DBLA = 64, 62 | DBLB = 72, 63 | DBLC = 80, 64 | DBLD = 88, 65 | DBLE = 96; 66 | var MASK_R = 0xff000000; 67 | var MASK_G = 0x00ff0000; 68 | var MASK_B = 0x0000ff00; 69 | var MASK_A = 0x000000f0; 70 | 71 | /** 72 | * The last bit of a value is a flag for a pointer, so this shifts a 73 | * number left and then sets the last bit to 1 to flag it as a pointer. 74 | * It has to be decoded in reverse. 75 | */ 76 | function makeP(n) { 77 | n = n|0; 78 | return (n << 1 | 1)|0; 79 | } 80 | 81 | /** 82 | * Sets maximum depth value. Maximum depth should not be modified during 83 | * runtime due to side effects not being tested for. If you need a larger 84 | * octree, copy the current tree into a larger one. Never, ever decrease 85 | * an octree's depth. 86 | * @param {int} n 87 | */ 88 | function setMaxDepth(n) { 89 | n = n|0; 90 | heap32[D_MAX>>2] = n; 91 | } 92 | 93 | /** 94 | * Accessor for max depth value. 95 | */ 96 | function getMaxDepth() { 97 | return heap32[D_MAX>>2]|0; 98 | } 99 | 100 | /** 101 | * Sets the current depth value. 102 | */ 103 | function setCurrentDepth(d) { 104 | d = d|0; 105 | heap32[D_CUR>>2] = d; 106 | } 107 | 108 | /** 109 | * Accessor for current depth value. 110 | */ 111 | function getCurrentDepth() { 112 | return heap32[D_CUR>>2]|0; 113 | } 114 | 115 | /** 116 | * Increments current depth value. Used during octree traversal. 117 | */ 118 | function incrementCurrentDepth() { 119 | setCurrentDepth((1|0) + (getCurrentDepth()|0)|0); 120 | calcDepthInverse(); 121 | } 122 | 123 | /** 124 | * Sets firstOffset value. This generally should not be changed during runtime. 125 | */ 126 | function setFirstOffset(n) { 127 | n = n|0; 128 | heap32[P_FO>>2] = n; 129 | } 130 | 131 | /** 132 | * Gets firstOffset value. 133 | */ 134 | function getFirstOffset() { 135 | return heap32[P_FO>>2]|0; 136 | } 137 | 138 | /** 139 | * Sets nextOffset value. This is generally handled through allocateOctet and 140 | * shouldn't be set manually. 141 | * @param {int} n 142 | */ 143 | function setNextOffset(n) { 144 | n = n|0; 145 | heap32[P_NO>>2] = n; 146 | } 147 | 148 | /** 149 | * Gets nextOffset value. This value is managed by allocateOctet, so you shouldn't 150 | * access it directly unless you need to read it without incrementing it. 151 | * @return {int} 152 | */ 153 | function getNextOffset() { 154 | return heap32[P_NO>>2]|0; 155 | } 156 | 157 | /** 158 | * Increments the next octet value, then returns the previous value. This is 159 | * used to keep track of the next available octet memory space. 160 | */ 161 | function allocateOctet() { 162 | heap32[P_NO>>2] = (8|0) + (heap32[P_NO>>2]|0); 163 | return (-8 + (heap32[P_NO>>2]|0))|0; 164 | } 165 | 166 | /** 167 | * Gets raw int value of octant at index i. 168 | * @param {int} i 32-bit index 169 | */ 170 | function getOctant(i) { 171 | i = i|0; 172 | return (heap32[i<<2>>2])|0; 173 | } 174 | 175 | /** 176 | * Sets octant at index i to value v. 177 | * @param {int} i 32-bit index 178 | * @param {int} v 32-bit value 179 | */ 180 | function setOctant(i, v) { 181 | i = i|0; 182 | v = v|0; 183 | heap32[i<<2>>2] = v; 184 | } 185 | 186 | /** 187 | * For debugging purposes. 188 | */ 189 | function getTRAVA() { 190 | return heap32[TRAVA>>2]|0; 191 | } 192 | 193 | /** 194 | * Decodes the red value from an rgba value. 195 | * @param {int} n 32-bit rgba value 196 | * @return {int} 8-bit color value 197 | */ 198 | function rFrom(n) { 199 | n = n|0; 200 | // have to be tricky here because js will assume a 32-bit int (not uint) 201 | return ((n >> 8) & MASK_G) >> 16; 202 | /* 203 | heap32[V_TMP>>2] = n; 204 | return heap8[V_TMP|0+PO_R|0]|0; 205 | */ 206 | } 207 | 208 | /** 209 | * Decodes the green value from an rgba value. 210 | * @param {int} n 32-bit rgba value 211 | * @return {int} 8-bit color value 212 | */ 213 | function gFrom(n) { 214 | n = n|0; 215 | return (n & MASK_G) >> 16; 216 | /* 217 | heap32[V_TMP>>2] = n; 218 | return heap8[V_TMP|0+PO_G|0]|0; 219 | */ 220 | } 221 | 222 | /** 223 | * Decodes the blue value from an rgba value. 224 | * @param {int} n 32-bit rgba value 225 | * @return {int} 8-bit color value 226 | */ 227 | function bFrom(n) { 228 | n = n|0; 229 | return (n & MASK_B) >> 8; 230 | /* 231 | heap32[V_TMP>>2] = n; 232 | return heap8[V_TMP|0+PO_B|0]|0; 233 | */ 234 | } 235 | 236 | /** 237 | * Decodes the alpha value from an rgba value. 238 | * @param {int} n 32-bit rgba value 239 | * @return {int} 4-bit alpha value 240 | */ 241 | function aFrom(n) { 242 | n = n|0; 243 | return (n & MASK_A) >> 4; 244 | /* 245 | heap32[V_TMP>>2] = n; 246 | return (heap8[V_TMP|0+PO_A|0] >> 4)|0; 247 | */ 248 | } 249 | 250 | /** 251 | * Determines whether a value is flagged as a pointer by checking the first bit. 252 | */ 253 | function isP(n) { 254 | n = n|0; 255 | return (n & 1)|0; 256 | } 257 | 258 | /** 259 | * Decode a pointer from a stored value. Returns 0 if the value wasn't 260 | * a pointer. Note this only works correctly if n is a value from the octree. 261 | * @param n {octant} an octant value from the octree 262 | * @return {int} a decoded pointer, or 0 if it wasn't a pointer 263 | */ 264 | function pFrom(n) { 265 | n = n|0; 266 | // this will return 0 if n is not an encoded pointer, without branching 267 | return ((~(0x7FFFFFFF+(n & 1)) & n) >> 1)|0; 268 | } 269 | 270 | /** 271 | * Reads a value from the index with the assumption that it's a pointer. 272 | * @param {int} i index to read from 273 | * @return {int} value decoded as a pointer 274 | */ 275 | function getP(i) { 276 | i = i|0; 277 | return (pFrom(getOctant(i)|0)|0)|0; 278 | } 279 | 280 | /** 281 | * Sets a value at the index as an encoded pointer. 282 | * @param {int} i index to read from 283 | * @param {int} n pointer to set 284 | */ 285 | function setP(i, p) { 286 | i = i|0; 287 | p = p|0; 288 | setOctant(i, makeP(p)|0); 289 | } 290 | 291 | /** 292 | * Sets the current pointer value, used during traversal. 293 | * Note that this value is *not* encoded as a pointer, because it's not 294 | * in the octree and converting it back and forth is computationally wasteful. 295 | * @param {int} p pointer to set 296 | */ 297 | function setCurrentPointer(p) { 298 | p = p|0; 299 | heap32[P_CUR>>2] = p; 300 | } 301 | 302 | function getCurrentPointer() { 303 | return heap32[P_CUR>>2]|0; 304 | } 305 | 306 | /** 307 | * Translates a set of rgba values to an unsigned 32-bit int. 308 | * @param {int} r 309 | * @param {int} g 310 | * @param {int} b 311 | * @param {int} a 312 | * @return int 313 | */ 314 | function valFromRGBA(r,g,b,a) { 315 | r = r|0; 316 | g = g|0; 317 | b = b|0; 318 | a = a|0; 319 | return (r << 24) + (g << 16) + (b << 8) + ((a & 63) << 4); 320 | } 321 | 322 | /** 323 | * Calculates inverse of current depth. Used in octantIdentity calculation. 324 | */ 325 | function calcDepthInverse() { 326 | heap32[D_INV>>2] = (heap32[D_MAX>>2]|0) - (heap32[D_CUR>>2]|0); 327 | } 328 | 329 | /** 330 | * Prepares internal variables for a voxel lookup. Called whenever these 331 | * values need to be refreshed during a voxel lookup or related calculations. 332 | * @param {int} x x-coordinate 333 | * @param {int} y y-coordinate 334 | * @param {int} z z-coordinate 335 | * @param {int} d target depth 336 | */ 337 | function prepareLookup(x, y, z, d) { 338 | x = x|0; 339 | y = y|0; 340 | z = z|0; 341 | d = d|0; 342 | heap32[LOC_X>>2] = x; 343 | heap32[LOC_Y>>2] = y; 344 | heap32[LOC_Z>>2] = z; 345 | heap32[D_TGT>>2] = d; 346 | heap32[P_CUR>>2] = heap32[P_FO>>2]|0; 347 | setCurrentDepth(0); 348 | } 349 | 350 | /** 351 | * Finds the identity of an octant for the prepared vector at the current 352 | * depth. 353 | * @return {int} 354 | */ 355 | function octantIdentity() { 356 | return (((heap32[LOC_Z>>2] >>> heap32[D_INV>>2]) & 1) << 2 | 357 | ((heap32[LOC_Y>>2] >>> heap32[D_INV>>2]) & 1) << 1 | 358 | ((heap32[LOC_X>>2] >>> heap32[D_INV>>2]) & 1) 359 | ); 360 | } 361 | 362 | /** 363 | * Derives the pointer for the octant at the current depth and vector 364 | * from the octet pointer. 365 | * @return {int} 366 | */ 367 | function octantPointer() { 368 | return ((heap32[P_CUR>>2]|0) + (octantIdentity()|0))|0; 369 | } 370 | 371 | function seek() { 372 | /* jshint -W041:false */ 373 | heap32[TRAVA>>2] = 0|0; 374 | while((heap32[D_CUR>>2]|0) < min((heap32[D_TGT>>2]|0), (heap32[D_MAX>>2]|0))) { 375 | incrementCurrentDepth(); 376 | heap32[TRAVA>>2] = getOctant(octantPointer()|0)|0; 377 | if(!(isP(heap32[TRAVA>>2]|0)|0)) return (heap32[TRAVA>>2])|0; 378 | heap32[P_CUR>>2] = pFrom(heap32[TRAVA>>2]|0)|0; 379 | } 380 | return (heap32[P_CUR>>2])|0; 381 | } 382 | 383 | /** 384 | * Initializes an octet at the depth and vector set using prepareLookup 385 | */ 386 | function initOctet() { 387 | heap32[INITA>>2] = 0|0; 388 | while((heap32[D_CUR>>2]|0) < min((heap32[D_TGT>>2]|0), (heap32[D_MAX>>2]|0))) { 389 | incrementCurrentDepth(); 390 | heap32[INITA>>2] = getOctant(octantPointer()|0)|0; 391 | if(!(isP(heap32[INITA>>2]|0)|0)) { 392 | heap32[INITA>>2] = makeP(allocateOctet()|0)|0; 393 | setOctant(octantPointer()|0, heap32[INITA>>2]|0); 394 | } 395 | heap32[P_CUR>>2] = pFrom(heap32[INITA>>2]|0)|0; 396 | } 397 | return heap32[P_CUR>>2]|0; 398 | } 399 | 400 | /** 401 | * Single step in traversal. Used when walking the tree. 402 | */ 403 | function step() { 404 | incrementCurrentDepth(); 405 | heap32[STEPA>>2] = getOctant(octantPointer()|0)|0; 406 | heap32[P_CUR>>2] = pFrom(heap32[STEPA>>2]|0)|0; 407 | return (octantPointer()|0)|0; 408 | } 409 | 410 | /** 411 | * Check the ray - box intersection of a voxel (which is an axis-aligned bounding box) 412 | * @param {double} bs[x,y,z] box start (top, left, back) 413 | * @param {double} be[x,y,z] box end (bottom, right, front) 414 | * @param {double} ro[x,y,z] ray origin 415 | * @param {double} rd[x,y,z] inverse of ray direction (inverse should be precalculated) 416 | * @return {int} 1 if there was a hit, otherwise 0 417 | */ 418 | function rayAABB(bsx, bsy, bsz, bex, bey, bez, rox, roy, roz, rdx, rdy, rdz) { 419 | bsx = +bsx; 420 | bsy = +bsy; 421 | bsz = +bsz; 422 | bex = +bex; 423 | bey = +bey; 424 | bez = +bez; 425 | rox = +rox; 426 | roy = +roy; 427 | roz = +roz; 428 | rdx = +rdx; 429 | rdy = +rdy; 430 | rdz = +rdz; 431 | 432 | heapd[DBLA>>3] = (bsx - rox)*rdx; 433 | heapd[DBLB>>3] = (bex - rox)*rdx; 434 | heapd[DBLC>>3] = (bsy - roy)*rdy; 435 | heapd[DBLD>>3] = (bey - roy)*rdy; 436 | heapd[DBLE>>3] = (bsz - roz)*rdz; 437 | heapd[DBLD>>3] = (bez - roz)*rdz; 438 | 439 | return ( 440 | +max( 441 | (+min(heapd[DBLA>>3], heapd[DBLB>>3])), 442 | (+min(heapd[DBLC>>3], heapd[DBLD>>3])), 443 | (+min(heapd[DBLD>>3], heapd[DBLE>>3])) 444 | ) >= 445 | +min( 446 | (+max(heapd[DBLA>>3], heapd[DBLB>>3])), 447 | (+max(heapd[DBLC>>3], heapd[DBLD>>3])), 448 | (+max(heapd[DBLD>>3], heapd[DBLE>>3])) 449 | ) 450 | )|0; 451 | } 452 | 453 | return { 454 | setMaxDepth:setMaxDepth, 455 | getMaxDepth:getMaxDepth, 456 | setFirstOffset:setFirstOffset, 457 | getFirstOffset:getFirstOffset, 458 | setNextOffset:setNextOffset, 459 | getNextOffset:getNextOffset, 460 | setCurrentDepth:setCurrentDepth, 461 | getCurrentDepth:getCurrentDepth, 462 | incrementCurrentDepth:incrementCurrentDepth, 463 | calcDepthInverse:calcDepthInverse, 464 | setCurrentPointer:setCurrentPointer, 465 | getCurrentPointer:getCurrentPointer, 466 | allocateOctet:allocateOctet, 467 | getTRAVA:getTRAVA, 468 | getOctant:getOctant, 469 | setOctant:setOctant, 470 | getP:getP, 471 | setP:setP, 472 | rFrom:rFrom, 473 | gFrom:gFrom, 474 | bFrom:bFrom, 475 | aFrom:aFrom, 476 | makeP:makeP, 477 | pFrom:pFrom, 478 | isP:isP, 479 | valFromRGBA:valFromRGBA, 480 | prepareLookup:prepareLookup, 481 | initOctet:initOctet, 482 | octantIdentity:octantIdentity, 483 | octantPointer:octantPointer, 484 | seek:seek, 485 | step:step, 486 | rayAABB:rayAABB 487 | }; 488 | } 489 | export const VK_FO = 100; // first offset 490 | export const VK_OS = 8; // octet size (in bytes) 491 | export const VoctopusKernel = function(buffer, depth) { 492 | var vk = new VoctopusKernel32(kernelLib, null, buffer); 493 | vk.setMaxDepth(depth); 494 | vk.setFirstOffset(VK_FO); 495 | vk.setNextOffset(VK_FO+VK_OS); 496 | return vk; 497 | } 498 | -------------------------------------------------------------------------------- /src/voctopus.util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * This module contains an assortment of export functions used internally in Voctopus, as well 4 | * as some export functions for analyzing octree properties for experimental purposes (they 5 | * help find "sweet spots" for initial octree memory allocations) 6 | * @module voctopus/util 7 | */ 8 | 9 | /** 10 | * Provide a 24 bit int implementation for DataViews. Note this 11 | * causes two reads/writes per call, meaning it's going to be 12 | * around half as fast as the native implementations. 13 | */ 14 | DataView.prototype.getUint24 = function(pos) { 15 | return (this.getUint16(pos) << 8) + this.getUint8(pos+2); 16 | } 17 | 18 | /** 19 | * Setter for Uint24. 20 | */ 21 | DataView.prototype.setUint24 = function(pos, val) { 22 | this.setUint16(pos, val >> 8); 23 | this.setUint8(pos+2, val & ~4294967040); 24 | } 25 | 26 | /** 27 | * Sum of powers of 8 up to n. Used in various calculations. 28 | * @param {int} n highest power of 8, up to 24 (after which precision errors become a problem) 29 | * @return {Float64} 30 | */ 31 | export function sump8(n) { 32 | return (Math.pow(8, n+1) -1) / 7; 33 | } 34 | 35 | /** 36 | * Find the cardinal identity (0-7) of an octant. Octets contain 8 octants, so in 37 | * x,y,z order they live at identities 0-7. 38 | * @param {vector} vector representing the absolute coordinate of a voxel at max depth 39 | * @param {int} dc current depth (zero-indexed) 40 | * @param {int} dm maximum depth 41 | * @return {int} identity 42 | */ 43 | export function octantIdentity(v, dc, dm) { 44 | let d = dm - 1 - dc; 45 | return (((v[2] >>> d) & 1) << 2 | 46 | ((v[1] >>> d) & 1) << 1 | 47 | ((v[0] >>> d) & 1) 48 | ); 49 | } 50 | 51 | /** 52 | * Figures out the maximum size of a voctopus octree, assuming it's 100% dense (e.g. filled with random noise). 53 | * @param {int} octantSize size of a single octant in the tree 54 | * @param {int} depth octree depth 55 | * @return {int} 56 | */ 57 | export function fullOctreeSize(octantSize, depth) { 58 | return sump8(depth)*octantSize; 59 | } 60 | 61 | /** 62 | * Discover the maximum addressable depth for an octree schema at 100% density. Note 63 | * that this is not very useful on its own because ArrayBuffers have a hard limit of 64 | * 2gb, and this can produce buffers sized in EXABYTES if used naively! 65 | * @param {int} octantSize (in bytes) 66 | * @return {int} depth 67 | */ 68 | export function maxAddressableOctreeDepth(octantSize) { 69 | var sum = 0, depth = 1; 70 | while(((sum+1)-(sum) === 1)) { 71 | ++depth; 72 | sum = sump8(depth)*octantSize; 73 | } 74 | return depth; 75 | } 76 | 77 | /** 78 | * Find the maximum density factor of an octree given an octantSize, depth, and memory limit. Voctopus figures out how much memory to allocate based on its octant size, depth and density factor. 79 | * @param {int} octantSize size of a single octant in bytes 80 | * @param {int} depth octree depth 81 | * @param {int} limit memory limit in bytes 82 | */ 83 | export function maxOctreeDensityFactor(octantSize, depth, limit) { 84 | return ~~(fullOctreeSize(octantSize, depth)/limit+1); 85 | } 86 | 87 | /** 88 | * Find the nearest power of two greater than a number. 89 | * @param {int} n number to test against 90 | * @return {int} 91 | */ 92 | export function npot(n) { 93 | n--; 94 | n |= n >> 1; 95 | n |= n >> 2; 96 | n |= n >> 4; 97 | n |= n >> 8; 98 | n |= n >> 16; 99 | n++; 100 | return n; 101 | } 102 | 103 | /** 104 | * Used to loop through every voxel in an octree and run callbacks. Used for 105 | * Voctopus testing. Three nested loops of X, Y, and Z axis, supporting a callback 106 | * for each iteration of each loop as supplied in the cbs parameter. 107 | * 108 | * @param {int} size size of a volume 109 | * @param {Object} cbs callback object {x:f(vector),y:f(vector),z:f(vector)} 110 | * @return {undefined} 111 | */ 112 | export function loop3D(size, cbs) { 113 | var cbx, cby, cbz, v = Uint32Array.of(0,0,0); 114 | cbx = typeof(cbs.x) === "function"?cbs.x:function(){}; 115 | cby = typeof(cbs.y) === "function"?cbs.y:function(){}; 116 | cbz = typeof(cbs.z) === "function"?cbs.z:function(){}; 117 | for(v[0] = 0; v[0] < size; ++v[0]) { 118 | v[1] = 0; 119 | v[2] = 0; 120 | cbx(v); 121 | for(v[1] = 0; v[1] < size; ++v[1]) { 122 | v[2] = 0; 123 | cby(v); 124 | for(v[2] = 0; v[2] < size; ++v[2]) { 125 | cbz(v); 126 | } 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * Find the coordinate space (size of a single octree axis) for a given depth. 133 | * @param {int} depth octree depth 134 | */ 135 | export function coordinateSpace(depth) { 136 | return Math.pow(2, depth); 137 | } 138 | 139 | /** 140 | * Check the ray - box intersection of a voxel (which is an axis-aligned bounding box) 141 | * @param {vector} bs box start (top, left, back) 142 | * @param {vector} be box end (bottom, right, front) 143 | * @param {vector} ro ray origin 144 | * @param {vector} rd inverse of ray direction (inverse should be precalculated) 145 | * @return bool true if there was a hit, otherwise false 146 | */ 147 | export function rayAABB(bs, be, ro, rd) { 148 | let min = Math.min, max = Math.max; 149 | // decompose vectors, saves time referencing 150 | // ray origin 151 | let ox = ro[0], oy = ro[1], oz = ro[2]; 152 | // ray direction 153 | let dx = rd[0], dy = rd[1], dz = rd[2]; 154 | 155 | let tx0 = (bs[0] - ox)*dx; 156 | let tx1 = (be[0] - ox)*dx; 157 | let ty0 = (bs[1] - oy)*dy; 158 | let ty1 = (be[1] - oy)*dy; 159 | let tz0 = (bs[2] - oz)*dz; 160 | let tz1 = (be[2] - oz)*dz; 161 | 162 | let tmin = max(min(tx0, tx1), min(ty0, ty1), min(tz0, tz1)); 163 | let tmax = min(max(tx0, tx1), max(ty0, ty1), max(tz0, tz1)); 164 | 165 | return tmax >= tmin; 166 | } 167 | 168 | /** 169 | * Polyfill for ArrayBuffer.transfer. Uses DataView setter/getters to make transfers 170 | * as fast as possible. Still slow with large buffers, but less slow than copying 171 | * byte by byte. This has been working for me so far but I'm not 100% sure there 172 | * 173 | * are no bugs or edge cases. 174 | * 175 | * @param {ArrayBuffer} buffer original arraybuffer 176 | * @return {undefined} 177 | */ 178 | if(typeof(ArrayBuffer.prototype.transfer) === "undefined") { 179 | ArrayBuffer.prototype.transfer = function transfer(old) { 180 | var dva, dvb, i, mod; 181 | dva = new DataView(this); 182 | dvb = new DataView(old); 183 | mod = this.byteLength%8+1; 184 | for(i = 0; i <= old.byteLength-mod; i+=8) dva.setFloat64(i, dvb.getFloat64(i)); 185 | mod = this.byteLength%4+1; 186 | if(i < old.byteLength-mod) { 187 | dva.setUint32(i, dvb.getUint32(i)); 188 | i += 4; 189 | } 190 | mod = this.byteLength%2+1; 191 | if(i < old.byteLength-mod) { 192 | dva.setUint16(i, dvb.getUint16(i)); 193 | i += 2; 194 | } 195 | if(i < old.byteLength) dva.setUint8(i, dvb.getUint8(i)); 196 | } 197 | } 198 | 199 | // memory mapping utility for debugging 200 | export function mmap(start, end, v) { 201 | const pad = (n) => "0".repeat(4).slice(0, 4-n.toString().length)+n; 202 | for(let i = start; i < end; i+=8) { 203 | console.log([i, i+1, i+2, i+3, i+4, i+5, i+6, i+7].map(pad).join(" ")); 204 | console.log([v[i], v[i+1], v[i+2], v[i+3], v[i+4], v[i+5], v[i+6], v[i+7]].map(pad).join(" ")); 205 | console.log(""); 206 | } 207 | } 208 | 209 | -------------------------------------------------------------------------------- /test/voctopus.core.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | require("should"); 3 | const {VK_FO, VK_OS} = require("../src/voctopus.kernel.asm.js"); 4 | const {voxel, Voctopus} = require("../src/voctopus.core"); 5 | const {loop3D, npot, sump8} = require("../src/voctopus.util.js"); 6 | const max = Math.max; 7 | 8 | 9 | describe("Voctopus", function() { 10 | var d, voc; 11 | const fo = VK_FO, os = VK_OS; 12 | beforeEach("set up a clean voctopus instance", function() { 13 | d = 5; 14 | voc = new Voctopus(d); 15 | }); 16 | it("should expose expected interfaces", function() { 17 | voc.should.have.property("freedOctets"); 18 | voc.should.have.property("nextOffset"); 19 | voc.should.have.property("buffer"); 20 | voc.should.have.property("depth"); 21 | voc.should.have.property("maxSize"); 22 | voc.should.have.property("octetSize"); 23 | voc.should.have.property("firstOffset"); 24 | voc.should.have.property("dimensions"); 25 | //voc.should.have.property("fields").eql(["m"]); 26 | (typeof(voc.get)).should.equal("function"); 27 | (typeof(voc.set)).should.equal("function"); 28 | (typeof(voc.getPointer)).should.equal("function"); 29 | (typeof(voc.setPointer)).should.equal("function"); 30 | (typeof(voc.getVoxel)).should.equal("function"); 31 | (typeof(voc.setVoxel)).should.equal("function"); 32 | (typeof(voc.init)).should.equal("function"); 33 | (typeof(voc.walk)).should.equal("function"); 34 | (typeof(voc.expand)).should.equal("function"); 35 | }); 36 | it("should complain if not supplied with a depth", function() { 37 | (() => new Voctopus()).should.throwError(); 38 | }); 39 | it("should correctly calculate the maximum size for a Voctopus", function() { 40 | for(let i = 1; i < 9; ++i) { 41 | voc = new Voctopus(i); 42 | voc.maxSize.should.eql(npot(fo+sump8(i))); 43 | } 44 | }); 45 | it("should correctly calculate the octree's dimensions", function() { 46 | for(var i = 1; i < 12; i++) { 47 | var voc = new Voctopus(i); 48 | voc.dimensions.should.eql(Math.pow(2, i)); 49 | } 50 | }); 51 | it("should initialize the root pointer to the correct offset", function() { 52 | var buf = new Uint32Array(voc.buffer); 53 | (buf[1]).should.eql(fo); 54 | }); 55 | it("should correctly implement pointer getters and setters", function() { 56 | voc.setPointer(fo, fo+os); 57 | voc.getPointer(fo).should.eql(fo+os); 58 | voc.setPointer(fo+os, fo+os*2); 59 | voc.getPointer(fo+os).should.eql(fo+os*2); 60 | }); 61 | it("should generate a buffer of the correct length", function() { 62 | // should make a buffer of max size if the max size is less than 73*octantSize 63 | let voc = new Voctopus(1); 64 | voc.buffer.byteLength.should.equal(max(0x10000, npot(fo+os*8))); 65 | voc = new Voctopus(2); 66 | voc.buffer.byteLength.should.equal(max(0x10000, npot(fo+os*8))); 67 | voc = new Voctopus(3); 68 | voc.buffer.byteLength.should.equal(max(0x10000, npot(fo+os*8))); 69 | // anything larger than this should start out at an eighth of the max size 70 | for(var i = 4; i < 10; i++) { 71 | voc = new Voctopus(i); 72 | voc.buffer.byteLength.should.eql(npot(max(0x10000, voc.maxSize/8)), "buffer is nearest power of two to one eighth of max length "+voc.maxSize); 73 | } 74 | }); 75 | it("should expand the buffer using expand", function() { 76 | var i, voc, ms; 77 | for(i = 4; i < 8; i++) { 78 | voc = null; 79 | voc = new Voctopus(i); 80 | ms = voc.maxSize; 81 | voc.buffer.byteLength.should.equal(npot(max(0x10000, ~~(ms/8)))); 82 | voc.expand(); 83 | voc.buffer.byteLength.should.equal(npot(max(0x10000, ~~(ms/4)))); 84 | voc.expand(); 85 | voc.buffer.byteLength.should.equal(npot(max(0x10000, ~~(ms/2)))); 86 | voc.expand(); 87 | voc.buffer.byteLength.should.equal(npot(max(0x10000, ~~(ms)))); 88 | voc.expand().should.be.false(); 89 | } 90 | }); 91 | it("should maintain integrity of the buffer during an expansion", function() { 92 | this.timeout(10000); 93 | var i, voc, size, index, count = 0, a, b, da, db; 94 | voc = new Voctopus(6); 95 | loop3D(size, { 96 | y:() => i = 0, z:(pos) => { 97 | index = voc.setVoxel(pos, {m:i}); 98 | i++; 99 | count++; 100 | } 101 | }); 102 | a = voc.buffer; 103 | voc.expand(); 104 | b = voc.buffer; 105 | da = new DataView(a); 106 | db = new DataView(b); 107 | let end = Math.pow(size+1, 3)*voc.octantSize; 108 | for(i = 0; i < end; i++) { 109 | da.getUint8(i).should.eql(db.getUint8(i)); 110 | } 111 | }); 112 | it("should initialize a coordinate's branch to a given depth with init", function() { 113 | // use a smaller tree for these tests because it's less math to reason about 114 | let vec = Float32Array.of(0,0,0); 115 | let expected = [fo, fo+os, fo+os*2, fo+os*3, fo+os*4]; 116 | // one at a time 117 | for(let i = 0; i < 5; ++i) { 118 | voc.init(vec, i).should.eql(expected[i]); 119 | } 120 | // now all at once 121 | voc = new Voctopus(4); 122 | voc.init(vec).should.eql(fo+os*3); 123 | 124 | // each of these are run twice to prove that it's deterministic, 125 | // and not just allocating a new one every time 126 | vec[0] = 1.0; 127 | voc.init(vec).should.eql(fo+os*4); 128 | voc.init(vec).should.eql(fo+os*4); 129 | 130 | // check each depth level change 131 | vec[0] = 2.0; 132 | voc.init(vec).should.eql(fo+os*6); 133 | voc.init(vec).should.eql(fo+os*6); 134 | 135 | vec[0] = 4.0; 136 | voc.init(vec).should.eql(fo+os*9); 137 | voc.init(vec).should.eql(fo+os*9); 138 | 139 | vec[1] = 1.0; 140 | voc.init(vec).should.eql(fo+os*10); 141 | voc.init(vec).should.eql(fo+os*10); 142 | 143 | vec[1] = 2.0; 144 | voc.init(vec).should.eql(fo+os*12); 145 | voc.init(vec).should.eql(fo+os*12); 146 | 147 | vec[1] = 4.0; 148 | voc.init(vec).should.eql(fo+os*15); 149 | voc.init(vec).should.eql(fo+os*15); 150 | 151 | vec[2] = 2.0; 152 | voc.init(vec).should.eql(fo+os*17); 153 | voc.init(vec).should.eql(fo+os*17); 154 | 155 | vec[2] = 4.0; 156 | voc.init(vec).should.eql(fo+os*20); 157 | voc.init(vec).should.eql(fo+os*20); 158 | 159 | vec[0] = 3.0; vec[1] = 7.0; vec[2] = 4.0; 160 | voc.init(vec).should.eql(fo+os*23); 161 | voc.init(vec).should.eql(fo+os*23); 162 | 163 | voc = new Voctopus(6); 164 | loop3D(16, {x:(pos) => { 165 | let posb = Array.prototype.slice.call(pos); 166 | let vec = []; 167 | for(let i in posb) posb[i] *= 2; 168 | for(let i = 0; i < 8; ++i) { 169 | // this is slow and dumb but it's easy to understand and just a test! 170 | let str = (("0").repeat(3)+(i >>> 0).toString(2)).slice(-3); 171 | vec[0] = posb[0]+parseInt(str.charAt(2)); 172 | vec[1] = posb[1]+parseInt(str.charAt(1)); 173 | vec[2] = posb[2]+parseInt(str.charAt(0)); 174 | voc.init(vec).should.eql(voc.nextOffset - os); 175 | } 176 | }}); 177 | }); 178 | it("should walk the octree, returning an array of pointers", function() { 179 | // use a smaller tree for these tests because it's less math to reason about 180 | voc = new Voctopus(4); 181 | let vec = Float32Array.of(0,0,0); 182 | voc.walk(vec).should.eql([fo]); 183 | let expected = [fo, fo+os, fo+os*2, fo+os*3]; 184 | 185 | // now all at once 186 | voc.init(vec); 187 | voc.walk(vec).should.eql(expected); 188 | 189 | vec[0] = 1.0; 190 | voc.init(vec); 191 | expected = [fo, fo+os, fo+os*2, fo+os*4+1]; 192 | voc.walk(vec).should.eql(expected); 193 | 194 | // check each depth level change 195 | vec[0] = 2.0; 196 | voc.init(vec); 197 | expected = [fo, fo+os, fo+os*5+1, fo+os*6]; 198 | voc.walk(vec).should.eql(expected); 199 | 200 | vec[0] = 4.0; 201 | voc.init(vec); 202 | expected = [fo, fo+os*7+1, fo+os*8, fo+os*9]; 203 | voc.walk(vec).should.eql(expected); 204 | 205 | vec[1] = 1.0; 206 | voc.init(vec); 207 | expected = [fo, fo+os*7+1, fo+os*8, fo+os*10+2]; 208 | voc.walk(vec).should.eql(expected); 209 | 210 | vec[1] = 2.0; 211 | voc.init(vec); 212 | expected = [fo, fo+os*7+1, fo+os*11+2, fo+os*12]; 213 | voc.walk(vec).should.eql(expected); 214 | 215 | vec[1] = 4.0; 216 | voc.init(vec); 217 | expected = [fo, fo+os*13+3, fo+os*14, fo+os*15]; 218 | voc.walk(vec).should.eql(expected); 219 | 220 | vec[2] = 2.0; 221 | voc.init(vec); 222 | expected = [fo, fo+os*13+3, fo+os*16+4, fo+os*17]; 223 | voc.walk(vec).should.eql(expected); 224 | 225 | vec[2] = 4.0; 226 | voc.init(vec); 227 | expected = [fo, fo+os*18+7, fo+os*19, fo+os*20]; 228 | voc.walk(vec).should.eql(expected); 229 | 230 | vec[0] = 3.0; vec[1] = 7.0; vec[2] = 4.0; 231 | voc.init(vec); 232 | expected = [fo, fo+os*21+6, fo+os*22+3, fo+os*23+3]; 233 | voc.walk(vec).should.eql(expected); 234 | }); 235 | it("should set voxel data at the right position with setVoxel", function() { 236 | let vec = [0,0,0]; 237 | let rgba = {r:128, g:222, b:235, a:15}; 238 | let ptr = (fo+os*4)<<2; 239 | let v = voc.view; 240 | // this should make a tree going down to 0,0 241 | voc.setVoxel(vec, rgba); 242 | // look at the raw data, since we haven't yet tested getVoxel 243 | let stack = voc.walk(vec, 4); 244 | stack.should.eql([fo, fo+os, fo+os*2, fo+os*3, fo+os*4]); 245 | v[ptr+3].should.eql(rgba.r, "voxel's r value is correct"); 246 | v[ptr+2].should.eql(rgba.g, "voxel's r value is correct"); 247 | v[ptr+1].should.eql(rgba.b, "voxel's r value is correct"); 248 | (v[ptr]).should.eql((15<<4), "voxel's r value is correct"); 249 | 250 | vec[0] = 1; 251 | rgba.r = 223; 252 | rgba.g = 108; 253 | rgba.b = 115; 254 | rgba.a = 12; 255 | 256 | voc.setVoxel(vec, rgba); 257 | ptr = (fo+os*5)<<2; 258 | v[ptr+3].should.eql(rgba.r, "voxel's r value is correct"); 259 | v[ptr+2].should.eql(rgba.g, "voxel's r value is correct"); 260 | v[ptr+1].should.eql(rgba.b, "voxel's r value is correct"); 261 | (v[ptr]).should.eql((12<<4), "voxel's r value is correct"); 262 | }); 263 | it("should get and set voxels", function() { 264 | voc = new Voctopus(3); 265 | var rgba = Object.create(voc.voxel); 266 | const cv = (c, n) => (c>>2)+n; 267 | var i = 0, c = 0, fy = () => i = 0, out = Object.create(voc.voxel); 268 | loop3D(voc.dimensions, { 269 | y:fy, z:(pos) => { 270 | rgba.r = cv(c,0); rgba.g = cv(c,1); rgba.b = cv(c,2); rgba.a = 15; 271 | voc.setVoxel(pos, rgba); 272 | voc.getVoxel(pos).should.eql(rgba); 273 | // using out param 274 | voc.getVoxel(pos, out).should.eql(rgba); 275 | if(c < 254) c++; 276 | else c = 0; 277 | } 278 | }); 279 | }); 280 | it("should support the out parameter for getVoxel", function() { 281 | voc = new Voctopus(3); 282 | let vx = Object.create(voc.voxel); 283 | let pos = [0,0,0]; 284 | vx.r = 255; 285 | vx.g = 128; 286 | vx.b = 92; 287 | vx.a = 4; 288 | voc.setVoxel(pos, vx); 289 | // change the values 290 | vx.r = 0; 291 | vx.g = 0; 292 | vx.b = 0; 293 | vx.a = 0; 294 | voc.getVoxel(pos, vx); 295 | vx.r.should.equal(255); 296 | vx.g.should.equal(128); 297 | vx.b.should.equal(92); 298 | vx.a.should.equal(4); 299 | }); 300 | it("should support the depth parameter for getVoxel and setVoxel", function() { 301 | voc = new Voctopus(3); 302 | let vx = Object.create(voc.voxel); 303 | vx.r = 255; vx.g = 128; vx.b = 92; vx.a = 4; 304 | let pos = [0,0,0]; 305 | voc.setVoxel(pos, vx, 1); 306 | voc.getVoxel(pos, undefined, 1).should.eql(vx); 307 | voc.getVoxel([0,0,1], undefined, 1).should.eql(vx); 308 | voc.getVoxel([0,1,0], undefined, 1).should.eql(vx); 309 | voc.getVoxel([0,1,1], undefined, 1).should.eql(vx); 310 | voc.getVoxel([1,0,0], undefined, 1).should.eql(vx); 311 | voc.getVoxel([1,0,1], undefined, 1).should.eql(vx); 312 | voc.getVoxel([1,1,0], undefined, 1).should.eql(vx); 313 | voc.getVoxel([1,1,1], undefined, 1).should.eql(vx); 314 | }); 315 | it("should fork a voxel when necessary", function() { 316 | voc = new Voctopus(3); 317 | let vx = Object.create(voc.voxel); 318 | vx.r = 255; vx.g = 128; vx.b = 92; vx.a = 4; 319 | let vx2 = Object.create(voc.voxel); 320 | vx2.r = 255; vx2.g = 128; vx2.b = 92; vx2.a = 4; 321 | let pos = [0,0,0]; 322 | voc.setVoxel(pos, vx, 1); 323 | voc.setVoxel([0,0,3], vx2, 2); 324 | voc.setVoxel([7,0,0], vx2, 3); 325 | voc.getVoxel(pos, undefined, 1).should.eql(vx); 326 | voc.getVoxel([0,0,1], undefined, 1).should.eql(vx); 327 | voc.getVoxel([0,1,0], undefined, 1).should.eql(vx); 328 | voc.getVoxel([0,1,1], undefined, 1).should.eql(vx); 329 | voc.getVoxel([1,0,0], undefined, 1).should.eql(vx); 330 | voc.getVoxel([1,0,1], undefined, 1).should.eql(vx); 331 | voc.getVoxel([1,1,0], undefined, 1).should.eql(vx); 332 | voc.getVoxel([1,1,1], undefined, 1).should.eql(vx); 333 | voc.getVoxel([0,0,3], undefined, 2).should.eql(vx2); 334 | voc.getVoxel([7,0,0], undefined, 3).should.eql(vx2); 335 | }); 336 | it("should write a full octet in one pass", function() { 337 | var vec = Uint32Array.of(0,0,0); 338 | var data = []; 339 | const rc = () => ~~(Math.random()*255); 340 | const ra = () => ~~(Math.random()*15); 341 | for(let i = 0; i < 8; i++) { 342 | let vx = Object.create(voxel); 343 | vx.r = rc(); 344 | vx.g = rc(); 345 | vx.b = rc(); 346 | vx.a = ra(); 347 | data.push(vx); 348 | } 349 | voc.setOctet(vec, data); 350 | voc.getOctet(vec).should.eql(data); 351 | }); 352 | xit("should compute ray intersections", function() { 353 | let vx = Object.create(voc.voxel); 354 | vx.r = 255; vx.g = 128; vx.b = 92; vx.a = 4; 355 | let data = [vx,vx,vx,vx,vx,vx,vx,vx]; 356 | let cb = function(t, p) { 357 | console.log(t); 358 | console.log(p); 359 | return 1; 360 | } 361 | voc.setOctet([0,0,0], data, 0); 362 | voc.intersect([16,16,-32], [16,16,512], cb); 363 | voc.intersect([16,16,-16], [0.0,0.0,1.0], cb); 364 | voc.intersect([16,16,-16], [0.0622573, 0.0622573, 0.996116], cb); 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /test/voctopus.kernel.asm.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | require("should"); 3 | const {VoctopusKernel, VK_FO, VK_OS} = require("../src/voctopus.kernel.asm.js"); 4 | describe("the voctopus 32-bit kernel", function() { 5 | var vk, buffer, view; 6 | beforeEach(function() { 7 | buffer = new ArrayBuffer(0x10000); 8 | view = new Uint32Array(buffer); 9 | vk = new VoctopusKernel(buffer, 5); 10 | }); 11 | it("should have set up its initial values", function() { 12 | view[0].should.eql(5); 13 | view[1].should.eql(VK_FO); 14 | view[2].should.eql(VK_FO+VK_OS); 15 | }); 16 | it("should implement depth functions", function() { 17 | var {getCurrentDepth, getMaxDepth, setCurrentDepth, incrementCurrentDepth} = vk; 18 | getMaxDepth().should.eql(5); 19 | for(let i = 0; i < 5; i++) { 20 | setCurrentDepth(i); 21 | getCurrentDepth().should.eql(i); 22 | } 23 | setCurrentDepth(0); 24 | for(let i = i; i < 6; i++) { 25 | incrementCurrentDepth(); 26 | getCurrentDepth().should.eql(i); 27 | } 28 | }); 29 | it("should implement offset functions", function() { 30 | var {getFirstOffset, setFirstOffset, getNextOffset, setNextOffset} = vk; 31 | getFirstOffset().should.eql(VK_FO); 32 | getNextOffset().should.eql(VK_FO+VK_OS); 33 | setFirstOffset(124); 34 | getFirstOffset().should.eql(124); 35 | setNextOffset(132); 36 | getNextOffset().should.eql(132); 37 | }); 38 | it("should allocate octets", function() { 39 | var {getNextOffset, allocateOctet} = vk, n = VK_FO+VK_OS; 40 | for(let i = 0; i < 256; i++) { 41 | allocateOctet().should.eql(n); 42 | n += VK_OS; 43 | getNextOffset().should.eql(n); 44 | } 45 | }); 46 | it("should be able to test whether a number is an encoded pointer", function() { 47 | var isP = vk.isP; 48 | for(let i = 0; i < 2048; i++) { 49 | isP(i).should.eql(i % 2); // only odd numbers should be treated as pointers 50 | } 51 | }); 52 | it("should encode pointers correctly", function() { 53 | var makeP = vk.makeP; 54 | for(let i = 0; i < 2048; i++) { 55 | makeP(i).should.eql(i << 1 | 1); 56 | } 57 | }); 58 | it("should decode pointers correctly", function() { 59 | var {makeP, pFrom} = vk; 60 | for(let i = 0; i < 2048; i++) { 61 | pFrom(makeP(i)).should.eql(i); 62 | } 63 | for(let i = 0; i < 2048; i+=2) { 64 | pFrom(i).should.eql(0); 65 | } 66 | }); 67 | it("should set pointers", function() { 68 | const {setP, makeP, isP, pFrom} = vk; 69 | for(let i = VK_FO; i < 256+VK_FO; ++i) { 70 | setP(i, i+1); 71 | view[i].should.eql(makeP(i+1)); 72 | } 73 | let n = makeP(VK_FO), i = 0; 74 | while(isP(n)) { 75 | n = view[pFrom(n)]; 76 | i++; 77 | } 78 | // should loop 256 times, then count one extra 79 | i.should.eql(257); 80 | }); 81 | it("should read decoded pointers", function() { 82 | const {setP, getP} = vk; 83 | for(let i = VK_FO; i < 256+VK_FO; ++i) { 84 | setP(i, i+1); 85 | } 86 | let n = VK_FO, i = 0; 87 | while(n) { 88 | n = getP(n); 89 | i++; 90 | } 91 | // should loop 256 times, then count one extra 92 | i.should.eql(257); 93 | }); 94 | it("should encode red channels", function() { 95 | var valFromRGBA = vk.valFromRGBA; 96 | for(let i = 0; i < 256; i++) { 97 | valFromRGBA(i, 0, 0, 0).should.eql(i << 24); 98 | } 99 | }); 100 | it("should encode green channels", function() { 101 | var valFromRGBA = vk.valFromRGBA; 102 | for(let i = 0; i < 256; i++) { 103 | valFromRGBA(0, i, 0, 0).should.eql(i << 16); 104 | } 105 | }); 106 | it("should encode blue channels", function() { 107 | var valFromRGBA = vk.valFromRGBA; 108 | for(let i = 0; i < 256; i++) { 109 | valFromRGBA(0, 0, i, 0).should.eql(i << 8); 110 | } 111 | }); 112 | it("should encode alpha channels", function() { 113 | var valFromRGBA = vk.valFromRGBA; 114 | for(let i = 1; i < 16; i++) { 115 | valFromRGBA(0, 0, 0, i).should.eql(i << 4); 116 | } 117 | }); 118 | it("should decode rgba data", function() { 119 | var rgba; 120 | var {valFromRGBA, rFrom, gFrom, bFrom, aFrom} = vk; 121 | for(let i = 1; i < 16; i++) { 122 | rgba = valFromRGBA(i << 3, i << 2, i << 1, i); 123 | rFrom(rgba).should.eql(i << 3); 124 | gFrom(rgba).should.eql(i << 2); 125 | bFrom(rgba).should.eql(i << 1); 126 | aFrom(rgba).should.eql(i); 127 | } 128 | for(let i = 0; i < 256; i++) { 129 | rgba = valFromRGBA(i, i, i, 15); 130 | rFrom(rgba).should.eql(i); 131 | gFrom(rgba).should.eql(i); 132 | bFrom(rgba).should.eql(i); 133 | aFrom(rgba).should.eql(15); 134 | } 135 | }); 136 | it("should find octant identities", function() { 137 | const {prepareLookup, setCurrentDepth, octantIdentity} = vk; 138 | var d = 5; 139 | var i; 140 | function check(x, y, z, td, cd, expected) { 141 | prepareLookup(x, y, z, td); 142 | setCurrentDepth(cd); 143 | octantIdentity().should.equal(expected); 144 | } 145 | // These should have the same identity at any depth 146 | for(i = 0; i < d; ++i) { 147 | check( 0, 0, 0, 0, d, 0); 148 | check(31, 0, 0, 0, d, 1); 149 | check( 0,31, 0, 0, d, 2); 150 | check(31,31, 0, 0, d, 3); 151 | check( 0, 0,31, 0, d, 4); 152 | check(31, 0,31, 0, d, 5); 153 | check( 0,31,31, 0, d, 6); 154 | check(31,31,31, 0, d, 7); 155 | } 156 | for(i = 1; i < d; ++i) { 157 | check( 0, 0, 0, 0, d, 0); 158 | check(15, 0, 0, 0, d, 1); 159 | check( 0,15, 0, 0, d, 2); 160 | check(15,15, 0, 0, d, 3); 161 | check( 0, 0,15, 0, d, 4); 162 | check(15, 0,15, 0, d, 5); 163 | check( 0,15,15, 0, d, 6); 164 | check(15,15,15, 0, d, 7); 165 | } 166 | for(i = 2; i < d; ++i) { 167 | check( 0, 0, 0, 0, d, 0); 168 | check( 7, 0, 0, 0, d, 1); 169 | check( 0, 7, 0, 0, d, 2); 170 | check( 7, 7, 0, 0, d, 3); 171 | check( 0, 0, 7, 0, d, 4); 172 | check( 7, 0, 7, 0, d, 5); 173 | check( 0, 7, 7, 0, d, 6); 174 | check( 7, 7, 7, 0, d, 7); 175 | } 176 | for(i = 3; i < d; ++i) { 177 | check( 0, 0, 0, 0, d, 0); 178 | check( 3, 0, 0, 0, d, 1); 179 | check( 0, 3, 0, 0, d, 2); 180 | check( 3, 3, 0, 0, d, 3); 181 | check( 0, 0, 3, 0, d, 4); 182 | check( 3, 0, 3, 0, d, 5); 183 | check( 0, 3, 3, 0, d, 6); 184 | check( 3, 3, 3, 0, d, 7); 185 | } 186 | check( 0, 0, 0, 0, 5, 0); 187 | check( 1, 0, 0, 0, 5, 1); 188 | check( 0, 1, 0, 0, 5, 2); 189 | check( 1, 1, 0, 0, 5, 3); 190 | check( 0, 0, 1, 0, 5, 4); 191 | check( 1, 0, 1, 0, 5, 5); 192 | check( 0, 1, 1, 0, 5, 6); 193 | check( 1, 1, 1, 0, 5, 7); 194 | }); 195 | it("should find an octant's pointer given its vector, depth and the pointer to its octet", function() { 196 | const {prepareLookup, octantPointer, getP, setP, allocateOctet, setCurrentDepth, setCurrentPointer} = vk; 197 | let c = VK_FO, p = 0; 198 | for(let i = 0; i < 5; i++) { 199 | p = allocateOctet(); 200 | setP(c+7, p); 201 | c = p; 202 | } 203 | prepareLookup(31, 31, 31, 0); 204 | setCurrentDepth(1); 205 | p = VK_FO; 206 | for(let i = 1; i < 6; i++) { 207 | setCurrentPointer(p); 208 | setCurrentDepth(i); 209 | p = getP(octantPointer()); 210 | p.should.eql(VK_FO+i*8); 211 | } 212 | }); 213 | it("should seek the octree given a vector and target depth", function() { 214 | const {allocateOctet, valFromRGBA, prepareLookup, setP, setOctant, 215 | seek, rFrom, gFrom, bFrom, aFrom, getCurrentDepth} = vk; 216 | let c = VK_FO, p = 0, res = 0; 217 | 218 | // with deeper pointers unset, seek should return after one iteration 219 | prepareLookup(31, 31, 31, 5); 220 | res = seek(); 221 | getCurrentDepth().should.eql(1); 222 | res.should.eql(0); 223 | 224 | // setup pointers 225 | for(let i = 0; i < 4; i++) { 226 | p = allocateOctet(); 227 | setP(c+7, p); 228 | c = p; 229 | } 230 | setOctant(c+7, valFromRGBA(244,213,112,15)); 231 | 232 | // full depth walk to target 233 | for(let i = 1; i < 4; i++) { 234 | prepareLookup(31,31,31,i); 235 | res = seek(); 236 | res.should.eql(VK_FO+VK_OS*i); 237 | getCurrentDepth().should.eql(i); 238 | } 239 | 240 | // read octant at lowest depth 241 | prepareLookup(31, 31, 31, 5); 242 | res = seek(); 243 | getCurrentDepth().should.eql(5); 244 | rFrom(res).should.eql(244); 245 | gFrom(res).should.eql(213); 246 | bFrom(res).should.eql(112); 247 | aFrom(res).should.eql(15); 248 | }); 249 | it("should initialize an octet at a given depth", function() { 250 | const {getCurrentDepth, seek, prepareLookup, initOctet} = vk; 251 | let res = 0; 252 | // do it one step at a time 253 | for(let i = 0; i < 5; i++) { 254 | prepareLookup(1, 7, 3, i); 255 | initOctet().should.eql(VK_FO+VK_OS*i); 256 | getCurrentDepth().should.eql(i); 257 | } 258 | let start = VK_FO+VK_OS*4; 259 | // do it again with another coordinate 260 | prepareLookup(15,31,17,4); 261 | initOctet().should.eql(VK_FO+VK_OS*8); 262 | for(let i = 1; i < 5; i++) { 263 | prepareLookup(15,31,17,i); 264 | res = seek(); 265 | res.should.eql(start+VK_OS*i); 266 | getCurrentDepth().should.eql(i); 267 | } 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /test/voctopus.util.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | require("should"); 3 | const {sump8, fullOctreeSize, maxAddressableOctreeDepth, 4 | maxOctreeDensityFactor, coordinateSpace, npot, 5 | octantIdentity, loop3D, rayAABB 6 | } = require("../src/voctopus.util.js"); 7 | 8 | describe("The extended DataView", function() { 9 | it("should implement Uint24 accessors", function() { 10 | this.timeout(10000); 11 | var i, n, buf, dv; 12 | buf = new ArrayBuffer(3); 13 | dv = new DataView(buf); 14 | n = Math.pow(2, 24); 15 | // this should give us decent coverage without trying every single number 16 | for(i = 1; i < n; i += 1111) { 17 | dv.setUint24(0, i); 18 | dv.getUint24(0).should.equal(i); 19 | } 20 | }); 21 | }); 22 | 23 | describe("The extended ArrayBuffer", function() { 24 | it("should implement transfer", function() { 25 | ArrayBuffer.prototype.transfer.should.be.type("function"); 26 | }); 27 | it("should transfer data correctly", function() { 28 | var buf, buf2, dv, dv2, i; 29 | buf = new ArrayBuffer(127); // 64+32+16+8+7 - should test each part of the copy 30 | dv = new DataView(buf); 31 | for(i = 0; i < 127; i++) { 32 | dv.setUint8(i, i); 33 | } 34 | buf2 = new ArrayBuffer(127); 35 | dv2 = new DataView(buf2); 36 | buf2.transfer(buf); 37 | for(i = 0; i < 127; i++) { 38 | dv2.getUint8(i).should.eql(i); 39 | } 40 | }); 41 | }); 42 | 43 | describe("util functions", function() { 44 | it("should find the next highest power of two with npot", function() { 45 | // test a number very slightly higher than each i+1, should be the next power of 2 46 | for(let i = 1, n = Math.pow(2, 31); i < n; i *=2) { 47 | npot((i+1)*1.0001).should.eql(i*2); 48 | } 49 | }); 50 | it("should find the sum of powers of 8 up to 8^n with sump8", function() { 51 | var i, n, sum; 52 | // use a naive implementation to check the math 53 | // stop at 8^24 because precision limits make the result useless at that point 54 | for(i = 0; i < 24; ++i) { 55 | sum = 0; 56 | for(n = 0; n <= i; ++n) { 57 | sum += Math.pow(8, n); 58 | } 59 | sump8(i).should.eql(sum); 60 | } 61 | }); 62 | it("should find the maximum (fully dense) size of an octree with fullOctreeSize", function() { 63 | var i, n, sum; 64 | for(i = 0; i < 22; ++i) { 65 | for(n = 1; n <= 64; ++n) { 66 | sum = sump8(i)*n; 67 | fullOctreeSize(n, i).should.eql(sump8(i)*n); 68 | } 69 | } 70 | }); 71 | it("should find the coordinate space of an octree with a given depth", function() { 72 | // this test is kind of spurious because this function is just a quick shortcut 73 | // for Math.pow, but it's here for coverage 74 | for(let i = 0; i < 16; i++) { 75 | coordinateSpace(i).should.eql(Math.pow(2, i)); 76 | } 77 | }); 78 | it("should find the maximum addressable depth of an octree with maxAddressableOctreeDepth", function() { 79 | // these are precalculated values based on other tests 80 | maxAddressableOctreeDepth(4).should.eql(17); 81 | maxAddressableOctreeDepth(29).should.eql(16); 82 | maxAddressableOctreeDepth(225).should.eql(15); 83 | }); 84 | it("should find the maximum density factor for an octree", function() { 85 | var limit = 512*1024*1024; // 512mb limit 86 | maxOctreeDensityFactor(4, 9, limit).should.eql(2); 87 | maxOctreeDensityFactor(4, 10, limit).should.eql(10); 88 | maxOctreeDensityFactor(4, 13, limit).should.eql(4682); 89 | maxOctreeDensityFactor(8, 9, limit).should.eql(3); 90 | maxOctreeDensityFactor(8, 10, limit).should.eql(19); 91 | maxOctreeDensityFactor(8, 13, limit).should.eql(9363); 92 | }); 93 | it("should calculate interesting information about an octree", function() { 94 | }); 95 | it("should loop through all the coordinates in a space with loop3D", function() { 96 | let x = -1, y = -1, z = -1; 97 | loop3D(16, { 98 | x:(pos) => { 99 | y = 0; z = 0; 100 | x++; 101 | pos.should.eql(Uint32Array.of(x,y,z)); 102 | y = -1; z = -1; 103 | }, 104 | y:(pos) => { 105 | z = 0; 106 | y++; 107 | pos.should.eql(Uint32Array.of(x,y,z)); 108 | z = -1; 109 | }, 110 | z:(pos) => { 111 | z++; 112 | pos.should.eql(Uint32Array.of(x,y,z)); 113 | } 114 | }); 115 | }); 116 | it("should yield expected octant offsets (range 0-7 * octantSize) for a position vector", function() { 117 | var d = 5; 118 | var i; 119 | // These should have the same identity at any depth 120 | for(i = 0; i < d; ++i) { 121 | octantIdentity([ 0, 0, 0], i, d).should.equal(0); 122 | octantIdentity([31, 0, 0], i, d).should.equal(1); 123 | octantIdentity([ 0,31, 0], i, d).should.equal(2); 124 | octantIdentity([31,31, 0], i, d).should.equal(3); 125 | octantIdentity([ 0, 0,31], i, d).should.equal(4); 126 | octantIdentity([31, 0,31], i, d).should.equal(5); 127 | octantIdentity([ 0,31,31], i, d).should.equal(6); 128 | octantIdentity([31,31,31], i, d).should.equal(7); 129 | } 130 | for(i = 1; i < d; ++i) { 131 | octantIdentity([ 0, 0, 0], i, d).should.equal(0); 132 | octantIdentity([15, 0, 0], i, d).should.equal(1); 133 | octantIdentity([ 0,15, 0], i, d).should.equal(2); 134 | octantIdentity([15,15, 0], i, d).should.equal(3); 135 | octantIdentity([ 0, 0,15], i, d).should.equal(4); 136 | octantIdentity([15, 0,15], i, d).should.equal(5); 137 | octantIdentity([ 0,15,15], i, d).should.equal(6); 138 | octantIdentity([15,15,15], i, d).should.equal(7); 139 | } 140 | for(i = 2; i < d; ++i) { 141 | octantIdentity([ 0, 0, 0], i, d).should.equal(0); 142 | octantIdentity([ 7, 0, 0], i, d).should.equal(1); 143 | octantIdentity([ 0, 7, 0], i, d).should.equal(2); 144 | octantIdentity([ 7, 7, 0], i, d).should.equal(3); 145 | octantIdentity([ 0, 0, 7], i, d).should.equal(4); 146 | octantIdentity([ 7, 0, 7], i, d).should.equal(5); 147 | octantIdentity([ 0, 7, 7], i, d).should.equal(6); 148 | octantIdentity([ 7, 7, 7], i, d).should.equal(7); 149 | } 150 | for(i = 3; i < d; ++i) { 151 | octantIdentity([ 0, 0, 0], i, d).should.equal(0); 152 | octantIdentity([ 3, 0, 0], i, d).should.equal(1); 153 | octantIdentity([ 0, 3, 0], i, d).should.equal(2); 154 | octantIdentity([ 3, 3, 0], i, d).should.equal(3); 155 | octantIdentity([ 0, 0, 3], i, d).should.equal(4); 156 | octantIdentity([ 3, 0, 3], i, d).should.equal(5); 157 | octantIdentity([ 0, 3, 3], i, d).should.equal(6); 158 | octantIdentity([ 3, 3, 3], i, d).should.equal(7); 159 | } 160 | octantIdentity([ 0, 0, 0], 4, d).should.equal(0); 161 | octantIdentity([ 1, 0, 0], 4, d).should.equal(1); 162 | octantIdentity([ 0, 1, 0], 4, d).should.equal(2); 163 | octantIdentity([ 1, 1, 0], 4, d).should.equal(3); 164 | octantIdentity([ 0, 0, 1], 4, d).should.equal(4); 165 | octantIdentity([ 1, 0, 1], 4, d).should.equal(5); 166 | octantIdentity([ 0, 1, 1], 4, d).should.equal(6); 167 | octantIdentity([ 1, 1, 1], 4, d).should.equal(7); 168 | }); 169 | it("should compute axis-aligned bounding box intersections", function() { 170 | let bs = [0,0,0], 171 | be = [32,32,32], 172 | // ray vectors are inverted 173 | ro = [-16,-16,16], 174 | rd = [16, 16, -35]; 175 | rayAABB(bs, be, ro, rd).should.be.true(); 176 | rayAABB(bs, be, rd, ro).should.be.false(); 177 | // start outside the box and never reach it 178 | rayAABB([64,64,64],[128,128,128],ro,rd).should.be.false(); 179 | // start outside the box and travel diagonally through it 180 | rayAABB(bs, be, [16,16,16],[-64,-64,-64]).should.be.true(); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /voctopus.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Voctopus = require("./src/voctopus.core").Voctopus 3 | const voxel = require("./src/voctopus.core").voxel 4 | window.Voctopus = {voxel:voxel,create:(n) => new Voctopus(n)} 5 | --------------------------------------------------------------------------------