├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── example ├── index.html └── main.js ├── index.js ├── package.json └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | screenshot.png 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isosurface-generator 2 | 3 | A JS generator function that returns a mesh describing an isosuface given a density and level. Since it's 4 | a generator function, you can perform this expensive calculation in a way that allows you to keep your UI 5 | responsive. 6 | 7 | [DEMO](https://wwwtyro.github.io/isosurface-generator) 8 | 9 |

10 | 11 |

12 | 13 | ## Install 14 | 15 | ```sh 16 | npm install isosurface-generator 17 | ``` 18 | 19 | ## Example 20 | 21 | ```js 22 | const isosurfaceGenerator = require('isosurface-generator'); 23 | const ndarray = require('ndarray'); 24 | 25 | const size = 8; 26 | 27 | const density = ndarray(new Float32Array(size*size*size), [size, size, size]); 28 | 29 | for (let i = 0; i < 1000; i++) { 30 | density.set( 31 | Math.floor(Math.random() * size), 32 | Math.floor(Math.random() * size), 33 | Math.floor(Math.random() * size), 34 | Math.random() 35 | ); 36 | } 37 | 38 | let mesh; 39 | 40 | for (let data of isosurfaceGenerator(density, 0.5)) { 41 | mesh = { 42 | positions: data.positions, 43 | cells: data.cells, 44 | }; 45 | console.log('Fraction complete:', data.fraction); 46 | // await display update 47 | } 48 | ``` 49 | 50 | ## API 51 | 52 | ### require('isosurface-generator')(density, level) 53 | 54 | #### Parameters 55 | 56 | `density` is an [ndarray](https://github.com/scijs/ndarray) (or an object that implements ndarray's `.get` method and `.shape` attribute) 57 | 58 | `level` is the density value for which we're generating an isosurface 59 | 60 | #### Return value 61 | 62 | A generator function that will provide a mesh describing the isosurface mesh and the fraction complete: 63 | 64 | ```js 65 | const generator = isosurfaceGenerator(density, 0.5); 66 | 67 | generator.next(); 68 | 69 | // Returns { 70 | // value: { 71 | // positions: [[1,2,3], [4,5,6], ...], 72 | // cells: [[1,2,3], [4,5,6], ...], 73 | // fraction: 0.009 74 | // }, 75 | // done: false 76 | // } 77 | ``` 78 | 79 | ## Resources 80 | 81 | - https://0fps.net/2012/07/12/smooth-voxel-terrain-part-2 82 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | const Trackball = require('trackball-controller'); 5 | const mat4 = require('gl-matrix').mat4; 6 | const vec3 = require('gl-matrix').vec3; 7 | const ndarray = require('ndarray'); 8 | const center = require('geo-center'); 9 | const vertexNormals = require('normals').vertexNormals; 10 | const isosurfaceGenerator = require('../index.js'); 11 | 12 | 13 | // Entrypoint. 14 | main(); 15 | 16 | 17 | async function main() { 18 | 19 | const size = 128; // The size of the density grid cube. 20 | const cutoff = 0.5; // The level of the isosurface we'll generate. 21 | 22 | // Create the density grid. 23 | let density = ndarray(new Float32Array(size*size*size), [size,size,size]); 24 | 25 | // Fill it with a few random points of density. Make the density significant so that when we 26 | // smooth it out later, there's plenty to go around. 27 | for (let i = 0; i < size*size*size*0.009; i++) { 28 | const x = Math.floor(Math.random() * size); 29 | const y = Math.floor(Math.random() * size); 30 | const z = Math.floor(Math.random() * size); 31 | const dx = size/2 - x; 32 | const dy = size/2 - y; 33 | const dz = size/2 - z; 34 | if (dx*dx + dy*dy + dz*dz > (size/2)*(size/2)*0.9) continue; 35 | density.set(x, y, z, 64); 36 | } 37 | 38 | // Indicate that we're about to smooth the density. 39 | document.getElementById('fraction-label').innerHTML = 'Smoothing density...' 40 | await display(); 41 | 42 | // Average out the density so that it is smoothed and merged. 43 | for (let i = 0; i < 64; i++) { 44 | const densityTemp = ndarray(new Float32Array(size*size*size), [size,size,size]); 45 | for (let x = 1; x < size - 1; x++) { 46 | for (let y = 1; y < size - 1; y++) { 47 | for (let z = 1; z < size - 1; z++) { 48 | let sum = 0; 49 | sum += density.get(x + 0, y + 0, z + 0); 50 | sum += density.get(x + 1, y + 0, z + 0); 51 | sum += density.get(x - 1, y + 0, z + 0); 52 | sum += density.get(x + 0, y + 1, z + 0); 53 | sum += density.get(x + 0, y - 1, z + 0); 54 | sum += density.get(x + 0, y + 0, z + 1); 55 | sum += density.get(x + 0, y + 0, z - 1); 56 | densityTemp.set(x, y, z, sum/7); 57 | } 58 | } 59 | } 60 | density = densityTemp; 61 | document.getElementById('fraction').style.width = 100 * (i/64) + '%'; 62 | await display(); 63 | } 64 | 65 | // We'll store the result of the isosurface generation in this object. 66 | let mesh; 67 | 68 | // Grab the current time. 69 | let t0 = performance.now(); 70 | 71 | // Indicate that we're going to generate the isosurface now. 72 | document.getElementById('fraction-label').innerHTML = 'Generating isosurface...' 73 | await display(); 74 | 75 | // Iterate over the isosurface generator and store the generated mesh. 76 | for (let data of isosurfaceGenerator(density, cutoff)) { 77 | // Save the data in our mesh. 78 | mesh = {positions: data.positions, cells: data.cells}; 79 | // If more than 100ms has passed, update the progress indicator and wait for the display to update. 80 | if (performance.now() - t0 > 100) { 81 | document.getElementById('fraction').style.width = 100 * data.fraction + '%'; 82 | await display(); 83 | t0 = performance.now(); 84 | } 85 | } 86 | 87 | // Indicate that we're going to center the mesh. 88 | document.getElementById('fraction-label').innerHTML = 'Centering Mesh...' 89 | await display(); 90 | 91 | // Center the resulting mesh on the origin. 92 | mesh.positions = center(mesh.positions); 93 | 94 | // Indicate that we're going to calculate the mesh normals. 95 | document.getElementById('fraction-label').innerHTML = 'Calculating mesh normals...' 96 | await display(); 97 | 98 | // Calculate the normals. 99 | const normals = vertexNormals(mesh.cells, mesh.positions); 100 | 101 | // All done, so hide the progress indicator. 102 | document.getElementById('fraction').style.display = 'none'; 103 | 104 | // Give some instructions. 105 | document.getElementById('fraction-label').innerHTML = 'Click and drag with your mouse to rotate the isosurface.' 106 | 107 | // Grab our canvas. 108 | const canvas = document.getElementById('render-canvas'); 109 | 110 | // Create our regl object. 111 | const regl = require('regl')({ 112 | canvas: canvas, 113 | extensions: ['OES_element_index_uint'], 114 | }); 115 | 116 | // Create the render command. 117 | const render = regl({ 118 | vert: ` 119 | precision highp float; 120 | attribute vec3 position; 121 | attribute vec3 normal; 122 | uniform mat4 model, view, projection; 123 | varying vec3 vNormal; 124 | void main() { 125 | gl_Position = projection * view * model * vec4(position, 1); 126 | vNormal = (model * vec4(normal, 1)).xyz; 127 | } 128 | `, 129 | frag: ` 130 | precision highp float; 131 | varying vec3 vNormal; 132 | void main() { 133 | vec3 n = normalize(vNormal); 134 | float c = dot(n, normalize(vec3(1,1,1))) + 0.5; 135 | gl_FragColor = vec4(n * c, 1.0); 136 | } 137 | `, 138 | attributes: { 139 | position: mesh.positions, 140 | normal: normals, 141 | }, 142 | uniforms: { 143 | model: regl.prop('model'), 144 | view: regl.prop('view'), 145 | projection: regl.prop('projection'), 146 | }, 147 | viewport: regl.prop('viewport'), 148 | cull: { 149 | enable: true, 150 | face: 'back' 151 | }, 152 | elements: mesh.cells, 153 | }); 154 | 155 | // Create a trackball controller for our scene. 156 | var trackball = new Trackball(canvas, { 157 | drag: 0.01 158 | }); 159 | 160 | // Give the trackball controller a little initial spin. 161 | trackball.spin(13,11); 162 | 163 | // Render the scene nonstop. 164 | function loop() { 165 | canvas.width = canvas.clientWidth; 166 | canvas.height = canvas.clientHeight; 167 | 168 | const model = trackball.rotation; 169 | const view = mat4.lookAt([], [0, 0, size * 2], [0, 0, 0], [0,1,0]); 170 | const projection = mat4.perspective([], Math.PI/4, canvas.width/canvas.height, 0.1, 1000); 171 | 172 | render({ 173 | model: model, 174 | view: view, 175 | projection: projection, 176 | viewport: {x: 0, y: 0, width: canvas.width, height: canvas.height}, 177 | }); 178 | 179 | requestAnimationFrame(loop); 180 | 181 | } 182 | 183 | loop(); 184 | } 185 | 186 | 187 | // Utility function that allows waiting for the display to be updated. 188 | function display() { 189 | return new Promise(resolve => { 190 | requestAnimationFrame(resolve); 191 | }); 192 | } 193 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const unitCube = { 4 | points: [ 5 | [0, 0, 0], 6 | [1, 0, 0], 7 | [0, 1, 0], 8 | [1, 1, 0], 9 | [0, 0, 1], 10 | [1, 0, 1], 11 | [0, 1, 1], 12 | [1, 1, 1], 13 | ], 14 | edges: [ 15 | [0, 1], 16 | [0, 2], 17 | [0, 4], 18 | [1, 3], 19 | [1, 5], 20 | [2, 3], 21 | [2, 6], 22 | [3, 7], 23 | [4, 5], 24 | [4, 6], 25 | [5, 7], 26 | [6, 7], 27 | ], 28 | }; 29 | 30 | function* isosurfaceGenerator(density, level) { 31 | 32 | window.density = density; 33 | 34 | const width = density.shape[0]; 35 | const height = density.shape[1]; 36 | const depth = density.shape[2]; 37 | 38 | const featurePoints = []; 39 | const featurePointIndex = {}; 40 | 41 | function getFeaturePointIndex(x, y, z) { 42 | if ([x,y,z] in featurePointIndex) return featurePointIndex[[x,y,z]]; 43 | const values = []; 44 | unitCube.points.forEach(function(v) { 45 | values.push(density.get(x + v[0], y + v[1], z + v[2])) 46 | }); 47 | let p = [0,0,0]; 48 | let sum = 0; 49 | unitCube.edges.forEach(function(e) { 50 | // if the surface doesn't pass through this edge, skip it 51 | if (values[e[0]] < level && values[e[1]] < level) return; 52 | if (values[e[0]] >= level && values[e[1]] >= level) return; 53 | // Calculate the rate of change of the density along this edge. 54 | const dv = values[e[1]] - values[e[0]]; 55 | // Figure out how far along this edge the surface lies (linear approximation). 56 | const dr = (level - values[e[0]]) / dv; 57 | // Figure out the direction of this edge. 58 | const r = [ 59 | unitCube.points[e[1]][0] - unitCube.points[e[0]][0], 60 | unitCube.points[e[1]][1] - unitCube.points[e[0]][1], 61 | unitCube.points[e[1]][2] - unitCube.points[e[0]][2], 62 | ]; 63 | // Figure out the point that the surface intersects this edge. 64 | const interp = [ 65 | unitCube.points[e[0]][0] + r[0] * dr, 66 | unitCube.points[e[0]][1] + r[1] * dr, 67 | unitCube.points[e[0]][2] + r[2] * dr, 68 | ]; 69 | // Add this intersection to the sum of intersections. 70 | p = [p[0] + interp[0] + x, p[1] + interp[1] + y, p[2] + interp[2] + z]; 71 | // Increment the edge intersection count for later averaging. 72 | sum++; 73 | }); 74 | featurePoints.push([p[0]/sum, p[1]/sum, p[2]/sum]); 75 | featurePointIndex[[x,y,z]] = featurePoints.length - 1; 76 | return featurePointIndex[[x,y,z]]; 77 | } 78 | 79 | const total = (width - 1) * (height - 1) * (depth - 1); 80 | let count = 0; 81 | 82 | const cells = []; 83 | 84 | for (let x = 0; x < width - 1; x++) { 85 | for (let y = 0; y < height - 1; y++) { 86 | for (let z = 0; z < depth - 1; z++) { 87 | const p0 = density.get(x + 0, y + 0, z + 0) >= level ? 1 : 0; 88 | const px = density.get(x + 1, y + 0, z + 0) >= level ? 1 : 0; 89 | const py = density.get(x + 0, y + 1, z + 0) >= level ? 1 : 0; 90 | const pz = density.get(x + 0, y + 0, z + 1) >= level ? 1 : 0; 91 | if (p0 + px === 1 && y > 0 && z > 0) { 92 | const a = getFeaturePointIndex(x + 0, y - 1, z - 1); 93 | const b = getFeaturePointIndex(x + 0, y - 1, z + 0); 94 | const c = getFeaturePointIndex(x + 0, y + 0, z + 0); 95 | const d = getFeaturePointIndex(x + 0, y + 0, z - 1); 96 | if (px > p0) { 97 | cells.push([a,b,c]); 98 | cells.push([a,c,d]); 99 | } 100 | else { 101 | cells.push([a,c,b]); 102 | cells.push([a,d,c]); 103 | } 104 | } 105 | if (p0 + py === 1 && x > 0 && z > 0) { 106 | const a = getFeaturePointIndex(x - 1, y + 0, z - 1); 107 | const b = getFeaturePointIndex(x + 0, y + 0, z - 1); 108 | const c = getFeaturePointIndex(x + 0, y + 0, z + 0); 109 | const d = getFeaturePointIndex(x - 1, y + 0, z + 0); 110 | if (py > p0) { 111 | cells.push([a,b,c]); 112 | cells.push([a,c,d]); 113 | } 114 | else { 115 | cells.push([a,c,b]); 116 | cells.push([a,d,c]); 117 | } 118 | } 119 | if (p0 + pz === 1 && x > 0 && y > 0) { 120 | const a = getFeaturePointIndex(x - 1, y - 1, z + 0); 121 | const b = getFeaturePointIndex(x + 0, y - 1, z + 0); 122 | const c = getFeaturePointIndex(x + 0, y + 0, z + 0); 123 | const d = getFeaturePointIndex(x - 1, y + 0, z + 0); 124 | if (pz < p0) { 125 | cells.push([a,b,c]); 126 | cells.push([a,c,d]); 127 | } 128 | else { 129 | cells.push([a,c,b]); 130 | cells.push([a,d,c]); 131 | } 132 | } 133 | count++; 134 | yield { 135 | positions: featurePoints, 136 | cells: cells, 137 | fraction: count/total, 138 | }; 139 | } 140 | } 141 | } 142 | 143 | 144 | } 145 | 146 | module.exports = isosurfaceGenerator; 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isosurface-generator", 3 | "version": "2.0.1", 4 | "description": "isosurface generator", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cd example && budo -H 0.0.0.0 main.js:bundle.js --live" 8 | }, 9 | "keywords": [ 10 | "isosurface" 11 | ], 12 | "author": { 13 | "name": "Rye Terrell", 14 | "email": "wwwtyro@gmail.com", 15 | "url": "http://wwwtyro.github.io" 16 | }, 17 | "license": "Unlicense", 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "budo": "^10.0.4", 21 | "geo-center": "^1.0.2", 22 | "gl-matrix": "^2.4.0", 23 | "ndarray": "^1.0.18", 24 | "normals": "^1.1.0", 25 | "regl": "^1.3.0", 26 | "trackball-controller": "^1.1.1" 27 | }, 28 | "repository": { 29 | "type" : "git", 30 | "url" : "https://github.com/wwwtyro/isosurface-generator" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwtyro/isosurface-generator/4316b61448cbbc65f4a4d3a4da29f8ada6da2c38/screenshot.png --------------------------------------------------------------------------------