├── .gitignore ├── LICENSE ├── README.md ├── bench ├── contour3d.js └── isosurface3d.js ├── example ├── 2d.svg ├── 3d.stl ├── 3d.svg ├── example-1d.js ├── example-2d.js ├── example-3d-stl.js └── example-3d.js ├── package.json ├── surfacenets.js └── test └── test.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 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Mikola Lysenko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | surface-nets 2 | ============ 3 | Extract a simplicial level set from an [ndarray](https://github.com/mikolalysenko/ndarray) in any dimension using naive surface nets. This module works in both node.js and with [browserify](http://browserify.org/)! 4 | 5 | 6 | If you are doing experiments with greedy meshing or working with rectangular data, then you might want [contour2d](https://github.com/mikolalysenko/contour2d) instead. 7 | 8 | # Example 9 | 10 | Here is a 2D example: 11 | 12 | ```javascript 13 | //Load modules 14 | var surfaceNets = require("surface-nets") 15 | var ndarray = require("ndarray") 16 | var fill = require("ndarray-fill") 17 | 18 | //Initialize array to a circle 19 | var array = ndarray(new Float32Array(32*32), [32,32]) 20 | fill(array, function(i,j) { 21 | return Math.pow(i-16,2) + Math.pow(j-16,2) 22 | }) 23 | 24 | //Extract 2D contour (this is all there is to it!) 25 | var complex = surfaceNets(array, 15*15) 26 | 27 | //Write SVG image to stdout 28 | var svgFile = [''] 29 | complex.cells.forEach(function(cell) { 30 | var p0 = complex.positions[cell[0]] 31 | var p1 = complex.positions[cell[1]] 32 | svgFile.push('') 33 | }) 34 | complex.positions.forEach(function(p) { 35 | svgFile.push('') 36 | }) 37 | svgFile.push('') 38 | console.log(svgFile.join("")) 39 | ``` 40 | 41 | And here is the output SVG: 42 | 43 | 44 | 45 | This module also works in 3D. Here is an example: 46 | 47 | ```javascript 48 | //Load modules 49 | var surfaceNets = require("surface-nets") 50 | var ndarray = require("ndarray") 51 | var fill = require("ndarray-fill") 52 | var mat4 = require("gl-matrix").mat4 53 | 54 | //Initialize array 55 | var array = ndarray(new Float32Array(32*32*32), [32,32,32]) 56 | fill(array, function(i,j,k) { 57 | return Math.pow(i-16,2) + Math.pow(j-16,2) + Math.pow(k-16,2) 58 | }) 59 | 60 | //Generate surface! (again, just one line) 61 | var complex = surfaceNets(array, 100) 62 | 63 | //Render the implicit surface to stdout 64 | console.log('') 65 | console.log(require("svg-3d-simplicial-complex")( 66 | complex.cells, 67 | complex.positions, { 68 | view: mat4.lookAt( 69 | mat4.create(), 70 | [32, 32, 32], 71 | [16, 16, 16], 72 | [0,1,0]), 73 | projection: mat4.perspective(mat4.create(), 74 | Math.PI/4.0, 75 | 1.0, 76 | 0.1, 77 | 1000.0), 78 | viewport: [[0,0], [512,512]] 79 | })) 80 | console.log("") 81 | ``` 82 | 83 | And here is the result: 84 | 85 | 86 | 87 | And while it is a bit trivial, you can also generate surfaces in 1D: 88 | 89 | ```javascript 90 | var surfaceNets = require("surface-nets") 91 | var ndarray = require("ndarray") 92 | 93 | console.log(surfaceNets(ndarray([1, -1, 0, 5, -10]))) 94 | ``` 95 | 96 | Output: 97 | 98 | ```javascript 99 | { positions: [ [ 0.5 ], [ 2 ], [ 3.3333333333333335 ] ], 100 | cells: [ [ 0 ], [ 1 ], [ 2 ] ] } 101 | ``` 102 | 103 | The code *should* work in 4D and higher dimensions, but this is not well tested and it is harder to visualize. (Also, why would you want to bother!?!) 104 | 105 | # Install 106 | 107 | ``` 108 | npm install surface-nets 109 | ``` 110 | 111 | # API 112 | 113 | #### `require("surface-nets")(array[,level])` 114 | Extracts the level set at `level` from `array` as a simplicial complex. 115 | 116 | * `array` is an [ndarray](https://github.com/mikolalysenko/ndarray) 117 | * `level` is an optional number which determines the level at which the levelset is evaluated (default `0`) 118 | 119 | **Returns** An object with a pair of properties representing a simplicial complex: 120 | 121 | * `positions` is an array encoding the positions of the vertices. The coordinates of the positions are with respect to the indices in `array`. 122 | * `cells` is an array encoding the cells of the simplicial complex as tuples of indices into the `position` array. 123 | 124 | # Credits 125 | (c) 2014 Mikola Lysenko. MIT License 126 | -------------------------------------------------------------------------------- /bench/contour3d.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | //Load modules 4 | var surfaceNets = require("../surfacenets.js") 5 | var ndarray = require("ndarray") 6 | var fill = require("ndarray-fill") 7 | 8 | //Initialize array 9 | var array = ndarray(new Float32Array(32*32*32), [32,32,32]) 10 | fill(array, function(i,j,k) { 11 | return Math.pow(i-16,2) + Math.pow(j-16,2) + Math.pow(k-16,2) 12 | }) 13 | 14 | //Generate surface! 15 | var complex = surfaceNets(array, 100) 16 | for(var i=0; i<10; ++i) { 17 | complex = surfaceNets(array, 100) 18 | } 19 | 20 | console.time("surface-nets 3d") 21 | for(var i=0; i<1000; ++i) { 22 | complex = surfaceNets(array, 100) 23 | } 24 | console.timeEnd("surface-nets 3d") -------------------------------------------------------------------------------- /bench/isosurface3d.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var isosurface = require("isosurface") 4 | 5 | function sphere(i,j,k) { 6 | return Math.pow(i-16,2) + Math.pow(j-16,2) + Math.pow(k-16,2) - 100 7 | } 8 | 9 | var complex 10 | 11 | console.time("isosurface.surfaceNets") 12 | for(var i=0; i<1000; ++i) { 13 | complex = isosurface.surfaceNets([32,32,32], sphere) 14 | } 15 | console.timeEnd("isosurface.surfaceNets") -------------------------------------------------------------------------------- /example/2d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/example-1d.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var surfaceNets = require("../surfacenets.js") 4 | var ndarray = require("ndarray") 5 | 6 | console.log(surfaceNets(ndarray([1, -1, 0, 5, -10]))) -------------------------------------------------------------------------------- /example/example-2d.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var surfaceNets = require("../surfacenets.js") 4 | var ndarray = require("ndarray") 5 | var fill = require("ndarray-fill") 6 | 7 | var array = ndarray(new Float32Array(33*33), [33,33]) 8 | fill(array, function(i,j) { 9 | return Math.pow(i-16,2) + Math.pow(j-16,2) 10 | }) 11 | 12 | var complex = surfaceNets(array, 15*15) 13 | 14 | complex.positions.forEach(function(pt) { 15 | pt[0] += 1 16 | pt[1] += 1 17 | }) 18 | 19 | var svgFile = [''] 20 | complex.cells.forEach(function(cell) { 21 | var p0 = complex.positions[cell[0]] 22 | var p1 = complex.positions[cell[1]] 23 | svgFile.push('') 24 | }) 25 | complex.positions.forEach(function(p) { 26 | svgFile.push('') 27 | }) 28 | svgFile.push('') 29 | 30 | var fs = require("fs") 31 | fs.writeFileSync("example/2d.svg", svgFile.join("")) -------------------------------------------------------------------------------- /example/example-3d-stl.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | //Load modules 4 | var surfaceNets = require("../surfacenets.js") 5 | var ndarray = require("ndarray") 6 | var fill = require("ndarray-fill") 7 | var mat4 = require("gl-matrix").mat4 8 | var stl = require("stl") 9 | 10 | //Initialize array 11 | var array = ndarray(new Float32Array(32*32*32), [32,32,32]) 12 | fill(array, function(i,j,k) { 13 | return Math.pow(i-16,2) + Math.pow(j-16,2) + Math.pow(k-16,2) 14 | }) 15 | 16 | //Generate surface! 17 | var complex = surfaceNets(array, 100) 18 | 19 | //Render the implicit surface to stdout 20 | var data = { 21 | description: "surface net 3d object", 22 | facets: complex.cells.map(function(c) { 23 | return { 24 | verts: c.map(function(idx) { 25 | return complex.positions[idx] 26 | }) 27 | } 28 | }) 29 | } 30 | 31 | console.log(data) 32 | 33 | require("fs").writeFileSync("example/3d.stl", stl.fromObject(data)) -------------------------------------------------------------------------------- /example/example-3d.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | //Load modules 4 | var surfaceNets = require("../surfacenets.js") 5 | var ndarray = require("ndarray") 6 | var fill = require("ndarray-fill") 7 | var mat4 = require("gl-matrix").mat4 8 | 9 | //Initialize array 10 | var array = ndarray(new Float32Array(32*32*32), [32,32,32]) 11 | fill(array, function(i,j,k) { 12 | return Math.pow(i-16,2) + Math.pow(j-16,2) + Math.pow(k-16,2) 13 | }) 14 | 15 | //Generate surface! 16 | var complex = surfaceNets(array, 100) 17 | 18 | //Render the implicit surface to stdout 19 | var svg = [] 20 | svg.push('') 21 | svg.push(require("svg-3d-simplicial-complex")( 22 | complex.cells, 23 | complex.positions, { 24 | view: mat4.lookAt( 25 | mat4.create(), 26 | [32, 32, 32], 27 | [16, 16, 16], 28 | [0,1,0]), 29 | projection: mat4.perspective(mat4.create(), 30 | Math.PI/4.0, 31 | 1.0, 32 | 0.1, 33 | 1000.0), 34 | viewport: [[0,0], [512,512]] 35 | })) 36 | svg.push("") 37 | 38 | require("fs").writeFileSync("example/3d.svg", svg.join("")) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surface-nets", 3 | "version": "1.0.2", 4 | "description": "General purpose level set extraction", 5 | "main": "surfacenets.js", 6 | "directories": { 7 | "example": "example", 8 | "test": "test" 9 | }, 10 | "dependencies": { 11 | "ndarray-extract-contour": "^1.0.0", 12 | "triangulate-hypercube": "^1.0.0", 13 | "zero-crossings": "^1.0.0" 14 | }, 15 | "devDependencies": { 16 | "gl-matrix": "~2.1.0", 17 | "ndarray": "~1.0.11", 18 | "ndarray-fill": "~0.1.0", 19 | "svg-3d-simplicial-complex": "~0.1.1", 20 | "tape": "~2.12.3", 21 | "isosurface": "^1.0.0", 22 | "robust-determinant": "^1.1.0", 23 | "gamma": "^0.1.0", 24 | "stl": "^1.1.1" 25 | }, 26 | "scripts": { 27 | "test": "tape test/*.js" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git://github.com/mikolalysenko/surface-nets.git" 32 | }, 33 | "keywords": [ 34 | "surface", 35 | "net", 36 | "marching", 37 | "cubes", 38 | "polygonize", 39 | "isosurface", 40 | "ndarray", 41 | "extract", 42 | "boundary", 43 | "volume", 44 | "marching", 45 | "squares", 46 | "surface nets", 47 | "dual contouring", 48 | "dual", 49 | "contour", 50 | "isocontour" 51 | ], 52 | "author": "Mikola Lysenko", 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/mikolalysenko/surface-nets/issues" 56 | }, 57 | "homepage": "https://github.com/mikolalysenko/surface-nets" 58 | } 59 | -------------------------------------------------------------------------------- /surfacenets.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | module.exports = surfaceNets 4 | 5 | var generateContourExtractor = require("ndarray-extract-contour") 6 | var triangulateCube = require("triangulate-hypercube") 7 | var zeroCrossings = require("zero-crossings") 8 | 9 | function buildSurfaceNets(order, dtype) { 10 | var dimension = order.length 11 | var code = ["'use strict';"] 12 | var funcName = "surfaceNets" + order.join("_") + "d" + dtype 13 | 14 | //Contour extraction function 15 | code.push( 16 | "var contour=genContour({", 17 | "order:[", order.join(), "],", 18 | "scalarArguments: 3,", 19 | "phase:function phaseFunc(p,a,b,c) { return (p > c)|0 },") 20 | if(dtype === "generic") { 21 | code.push("getters:[0],") 22 | } 23 | 24 | //Generate vertex function 25 | var cubeArgs = [] 26 | var extraArgs = [] 27 | for(var i=0; i>>7){") 56 | } 57 | for(var i=0; i<1<<(1< 128) { 59 | if((i%128)===0) { 60 | if(extraFuncs.length > 0) { 61 | currentFunc.push("}}") 62 | } 63 | var efName = "vExtra" + extraFuncs.length 64 | code.push("case ", (i>>>7), ":", efName, "(m&0x7f,", extraArgs.join(), ");break;") 65 | currentFunc = [ 66 | "function ", efName, "(m,", extraArgs.join(), "){switch(m){" 67 | ] 68 | extraFuncs.push(currentFunc) 69 | } 70 | } 71 | currentFunc.push("case ", (i&0x7f), ":") 72 | var crossings = new Array(dimension) 73 | var denoms = new Array(dimension) 74 | var crossingCount = new Array(dimension) 75 | var bias = new Array(dimension) 76 | var totalCrossings = 0 77 | for(var j=0; j j) { 87 | continue 88 | } 89 | if(!(i&(1< 0) { 127 | cStr = "+" + crossingCount[k] + "*c" 128 | } 129 | var weight = 0.5 * (crossings[k].length / totalCrossings) 130 | var shift = 0.5 + 0.5 * (bias[k] / totalCrossings) 131 | vertexStr.push("d" + k + "-" + shift + "-" + weight + "*(" + crossings[k].join("+") + cStr + ")/(" + denoms[k].join("+") + ")") 132 | 133 | } 134 | } 135 | currentFunc.push("a.push([", vertexStr.join(), "]);", 136 | "break;") 137 | } 138 | code.push("}},") 139 | if(extraFuncs.length > 0) { 140 | currentFunc.push("}}") 141 | } 142 | 143 | //Create face function 144 | var faceArgs = [] 145 | for(var i=0; i<(1<<(dimension-1)); ++i) { 146 | faceArgs.push("v" + i) 147 | } 148 | faceArgs.push("c0", "c1", "p0", "p1", "a", "b", "c") 149 | code.push("cell:function cellFunc(", faceArgs.join(), "){") 150 | 151 | var facets = triangulateCube(dimension-1) 152 | code.push("if(p0){b.push(", 153 | facets.map(function(f) { 154 | return "[" + f.map(function(v) { 155 | return "v" + v 156 | }) + "]" 157 | }).join(), ")}else{b.push(", 158 | facets.map(function(f) { 159 | var e = f.slice() 160 | e.reverse() 161 | return "[" + e.map(function(v) { 162 | return "v" + v 163 | }) + "]" 164 | }).join(), 165 | ")}}});function ", funcName, "(array,level){var verts=[],cells=[];contour(array,verts,cells,level);return {positions:verts,cells:cells};} return ", funcName, ";") 166 | 167 | for(var i=0; i 1) { 46 | //Compute volume of surface 47 | var vol = 0.0 48 | for(var k=0; k= -1e-6, "check orientation: " + r) 60 | vol += r[r.length-1] / gamma(d+1) 61 | } 62 | var bvol = Math.pow(Math.PI, Math.floor(d/2)) * Math.pow(6.0, d) 63 | if((d % 2) === 0) { 64 | bvol /= gamma((d/2) + 1) 65 | } else { 66 | var kk = Math.floor(d/2) 67 | bvol *= Math.pow(2.0, d) * gamma(1+kk) / gamma(1+d) 68 | } 69 | t.ok(Math.abs(vol / bvol - 1.0) < 0.05, "check volume - vol=" + vol + " vs bvol=" + bvol) 70 | } 71 | } 72 | t.end() 73 | }) --------------------------------------------------------------------------------