├── .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
--------------------------------------------------------------------------------