src/isosurface/dual-contouring/DualContouring.js
import { edges } from "sparse-octree";
import { Material } from "../../volume/Material";
import { Isosurface } from "../Isosurface";
import * as tables from "./tables";
/**
* The maximum number of vertices. Vertex indices use 16 bits.
*
* @type {Number}
* @private
*/
const MAX_VERTEX_COUNT = Math.pow(2, 16) - 1;
/**
* An edge contouring sub-procedure.
*
* @private
* @param {Array} octants - Four leaf octants.
* @param {Number} dir - A direction index.
* @param {Array} indexBuffer - An output list for vertex indices.
*/
function contourProcessEdge(octants, dir, indexBuffer) {
const indices = [-1, -1, -1, -1];
const signChange = [false, false, false, false];
let minSize = Infinity;
let minIndex = 0;
let flip = false;
let c1, c2, m1, m2;
let octant, edge;
let i;
for(i = 0; i < 4; ++i) {
octant = octants[i];
edge = tables.procEdgeMask[dir][i];
c1 = edges[edge][0];
c2 = edges[edge][1];
m1 = (octant.voxel.materials >> c1) & 1;
m2 = (octant.voxel.materials >> c2) & 1;
if(octant.size < minSize) {
minSize = octant.size;
minIndex = i;
flip = (m1 !== Material.AIR);
}
indices[i] = octant.voxel.index;
signChange[i] = (m1 !== m2);
}
if(signChange[minIndex]) {
if(!flip) {
indexBuffer.push(indices[0]);
indexBuffer.push(indices[1]);
indexBuffer.push(indices[3]);
indexBuffer.push(indices[0]);
indexBuffer.push(indices[3]);
indexBuffer.push(indices[2]);
} else {
indexBuffer.push(indices[0]);
indexBuffer.push(indices[3]);
indexBuffer.push(indices[1]);
indexBuffer.push(indices[0]);
indexBuffer.push(indices[2]);
indexBuffer.push(indices[3]);
}
}
}
/**
* An edge contouring procedure.
*
* @private
* @param {Array} octants - Four edge octants.
* @param {Number} dir - A direction index.
* @param {Array} indexBuffer - An output list for vertex indices.
*/
function contourEdgeProc(octants, dir, indexBuffer) {
const c = [0, 0, 0, 0];
let edgeOctants;
let octant;
let i, j;
if(octants[0].voxel !== null && octants[1].voxel !== null &&
octants[2].voxel !== null && octants[3].voxel !== null) {
contourProcessEdge(octants, dir, indexBuffer);
} else {
for(i = 0; i < 2; ++i) {
c[0] = tables.edgeProcEdgeMask[dir][i][0];
c[1] = tables.edgeProcEdgeMask[dir][i][1];
c[2] = tables.edgeProcEdgeMask[dir][i][2];
c[3] = tables.edgeProcEdgeMask[dir][i][3];
edgeOctants = [];
for(j = 0; j < 4; ++j) {
octant = octants[j];
if(octant.voxel !== null) {
edgeOctants[j] = octant;
} else if(octant.children !== null) {
edgeOctants[j] = octant.children[c[j]];
} else {
break;
}
}
if(j === 4) {
contourEdgeProc(edgeOctants, tables.edgeProcEdgeMask[dir][i][4], indexBuffer);
}
}
}
}
/**
* A face contouring procedure.
*
* @private
* @param {Array} octants - Two face octants.
* @param {Number} dir - A direction index.
* @param {Array} indexBuffer - An output list for vertex indices.
*/
function contourFaceProc(octants, dir, indexBuffer) {
const c = [0, 0, 0, 0];
const orders = [
[0, 0, 1, 1],
[0, 1, 0, 1]
];
let faceOctants, edgeOctants;
let order, octant;
let i, j;
if(octants[0].children !== null || octants[1].children !== null) {
for(i = 0; i < 4; ++i) {
c[0] = tables.faceProcFaceMask[dir][i][0];
c[1] = tables.faceProcFaceMask[dir][i][1];
faceOctants = [
(octants[0].children === null) ? octants[0] : octants[0].children[c[0]],
(octants[1].children === null) ? octants[1] : octants[1].children[c[1]]
];
contourFaceProc(faceOctants, tables.faceProcFaceMask[dir][i][2], indexBuffer);
}
for(i = 0; i < 4; ++i) {
c[0] = tables.faceProcEdgeMask[dir][i][1];
c[1] = tables.faceProcEdgeMask[dir][i][2];
c[2] = tables.faceProcEdgeMask[dir][i][3];
c[3] = tables.faceProcEdgeMask[dir][i][4];
order = orders[tables.faceProcEdgeMask[dir][i][0]];
edgeOctants = [];
for(j = 0; j < 4; ++j) {
octant = octants[order[j]];
if(octant.voxel !== null) {
edgeOctants[j] = octant;
} else if(octant.children !== null) {
edgeOctants[j] = octant.children[c[j]];
} else {
break;
}
}
if(j === 4) {
contourEdgeProc(edgeOctants, tables.faceProcEdgeMask[dir][i][5], indexBuffer);
}
}
}
}
/**
* The main contouring procedure.
*
* @private
* @param {Octant} octant - An octant.
* @param {Array} indexBuffer - An output list for vertex indices.
*/
function contourCellProc(octant, indexBuffer) {
const children = octant.children;
const c = [0, 0, 0, 0];
let faceOctants, edgeOctants;
let i;
if(children !== null) {
for(i = 0; i < 8; ++i) {
contourCellProc(children[i], indexBuffer);
}
for(i = 0; i < 12; ++i) {
c[0] = tables.cellProcFaceMask[i][0];
c[1] = tables.cellProcFaceMask[i][1];
faceOctants = [
children[c[0]],
children[c[1]]
];
contourFaceProc(faceOctants, tables.cellProcFaceMask[i][2], indexBuffer);
}
for(i = 0; i < 6; ++i) {
c[0] = tables.cellProcEdgeMask[i][0];
c[1] = tables.cellProcEdgeMask[i][1];
c[2] = tables.cellProcEdgeMask[i][2];
c[3] = tables.cellProcEdgeMask[i][3];
edgeOctants = [
children[c[0]],
children[c[1]],
children[c[2]],
children[c[3]]
];
contourEdgeProc(edgeOctants, tables.cellProcEdgeMask[i][4], indexBuffer);
}
}
}
/**
* Collects positions and normals from the voxel information of the given octant
* and its children. The generated vertex indices are stored in the respective
* voxels during the octree traversal.
*
* @private
* @param {Octant} octant - An octant.
* @param {Array} positions - An array to be filled with vertices.
* @param {Array} normals - An array to be filled with normals.
* @param {Number} index - The next vertex index.
*/
function generateVertexIndices(octant, positions, normals, index) {
let i, voxel;
if(octant.children !== null) {
for(i = 0; i < 8; ++i) {
index = generateVertexIndices(octant.children[i], positions, normals, index);
}
} else if(octant.voxel !== null) {
voxel = octant.voxel;
voxel.index = index;
positions[index * 3] = voxel.position.x;
positions[index * 3 + 1] = voxel.position.y;
positions[index * 3 + 2] = voxel.position.z;
normals[index * 3] = voxel.normal.x;
normals[index * 3 + 1] = voxel.normal.y;
normals[index * 3 + 2] = voxel.normal.z;
++index;
}
return index;
}
/**
* Dual Contouring is an isosurface extraction technique that was originally
* presented by Tao Ju in 2002:
* http://www.cs.wustl.edu/~taoju/research/dualContour.pdf
*/
export class DualContouring {
/**
* Contours the given volume data.
*
* @param {SparseVoxelOctree} svo - A voxel octree.
* @return {Isosurface} The generated isosurface or null if no data was generated.
*/
static run(svo) {
const indexBuffer = [];
// Each voxel contains one vertex.
const vertexCount = svo.voxelCount;
let result = null;
let positions = null;
let normals = null;
let uvs = null;
let materials = null;
if(vertexCount > MAX_VERTEX_COUNT) {
console.warn(
"Could not create geometry for cell at position", svo.min,
"(vertex count of", vertexCount, "exceeds limit of ", MAX_VERTEX_COUNT, ")"
);
} else if(vertexCount > 0) {
positions = new Float32Array(vertexCount * 3);
normals = new Float32Array(vertexCount * 3);
uvs = new Float32Array(vertexCount * 2);
materials = new Uint8Array(vertexCount);
generateVertexIndices(svo.root, positions, normals, 0);
contourCellProc(svo.root, indexBuffer);
result = new Isosurface(
new Uint16Array(indexBuffer),
positions,
normals,
uvs,
materials
);
}
return result;
}
}