UNPKG

noa-engine

Version:

Experimental voxel game engine

988 lines (752 loc) 28.7 kB
import ndarray from 'ndarray' import { Mesh } from '@babylonjs/core/Meshes/mesh' import { VertexData } from '@babylonjs/core/Meshes/mesh.vertexData' import { TerrainMatManager } from './terrainMaterials' import { makeProfileHook } from './util' // enable for profiling.. var PROFILE_EVERY = 0 /* * * TERRAIN MESHER!! * * * top-level entry point: * takes a chunk, passes it to the greedy mesher, * gets back an intermediate struct of face data, * passes that to the mesh builder, * gets back an array of Mesh objects, * and finally puts those into the 3D engine * */ /** * @internal * @param {import('../index').Engine} noa */ export function TerrainMesher(noa) { // wrangles which block materials can be merged into the same mesh var terrainMatManager = new TerrainMatManager(noa) this.allTerrainMaterials = terrainMatManager.allMaterials // internally expose the default flat material used for untextured terrain this._defaultMaterial = terrainMatManager._defaultMat // two-pass implementations for this module var greedyMesher = new GreedyMesher(noa, terrainMatManager) var meshBuilder = new MeshBuilder(noa, terrainMatManager) /* * * API * */ // set or clean up any per-chunk properties needed for terrain meshing this.initChunk = function (chunk) { chunk._terrainMeshes.length = 0 } this.disposeChunk = function (chunk) { chunk._terrainMeshes.forEach(mesh => { noa.emit('removingTerrainMesh', mesh) mesh.dispose() }) chunk._terrainMeshes.length = 0 } /** * meshing entry point and high-level flow * @param {import('./chunk').Chunk} chunk */ this.meshChunk = function (chunk, ignoreMaterials = false) { profile_hook('start') // remove any previous terrain meshes this.disposeChunk(chunk) profile_hook('cleanup') // greedy mesher generates struct of face data var faceDataSet = greedyMesher.mesh(chunk, ignoreMaterials) profile_hook('geom') // builder generates mesh data (positions, normals, etc) var meshes = meshBuilder.buildMesh(chunk, faceDataSet, ignoreMaterials) profile_hook('build') profile_hook('end') // add meshes to scene and finish meshes.forEach((mesh) => { mesh.cullingStrategy = Mesh.CULLINGSTRATEGY_BOUNDINGSPHERE_ONLY noa.rendering.addMeshToScene(mesh, true, chunk.pos, this) noa.emit('addingTerrainMesh', mesh) mesh.freezeNormals() mesh.freezeWorldMatrix() chunk._terrainMeshes.push(mesh) if (!mesh.metadata) mesh.metadata = {} mesh.metadata[terrainMeshFlag] = true }) } var terrainMeshFlag = 'noa_chunk_terrain_mesh' } /* * * * * * Intermediate struct to hold data for a bunch of merged block faces * * The greedy mesher produces these (one per terrainID), * and the mesh builder turns each one into a Mesh instance. * * * * * */ function MeshedFaceData() { this.terrainID = 0 this.numFaces = 0 // following arrays are all one element per quad this.matIDs = [] this.dirs = [] this.is = [] this.js = [] this.ks = [] this.wids = [] this.hts = [] this.packedAO = [] } /** * * * * Greedy meshing algorithm * * Originally based on algo by Mikola Lysenko: * http://0fps.net/2012/07/07/meshing-minecraft-part-2/ * but probably no code remaining from there anymore. * Ad-hoc AO handling by me, made of cobwebs and dreams * * * Takes in a Chunk instance, and returns an object containing * GeometryData structs, keyed by terrain material ID, * which the terrain builder can then make into meshes. * * * @param {import('../index').Engine} noa * @param {import('./terrainMaterials').TerrainMatManager} terrainMatManager */ function GreedyMesher(noa, terrainMatManager) { // class-wide cached structs and getters var maskCache = new Int16Array(16) var aoMaskCache = new Int16Array(16) // terrain ID accessor can be overridded for hacky reasons var realGetTerrainID = terrainMatManager.getTerrainMatId.bind(terrainMatManager) var fakeGetTerrainID = (matID) => 1 var terrainIDgetter = realGetTerrainID /** * Entry point * * @param {import('./chunk').Chunk} chunk * @returns {Object.<string, MeshedFaceData>} keyed by terrain material ID */ this.mesh = function (chunk, ignoreMaterials) { var cs = chunk.size terrainIDgetter = (ignoreMaterials) ? fakeGetTerrainID : realGetTerrainID // no internal faces for empty or entirely solid chunks var edgesOnly = (chunk._isEmpty || chunk._isFull) /** @type {Object.<string, MeshedFaceData>} */ var faceDataSet = {} faceDataPool.reset() // Sweep over each axis, mapping axes to [d,u,v] for (var d = 0; d < 3; ++d) { var u = (d === 2) ? 0 : 2 var v = (d === 1) ? 0 : 1 // transposed ndarrays of nearby chunk voxels (self and neighbors) var nabVoxelsArr = chunk._neighbors.data.map(c => { if (c && c.voxels) return c.voxels.transpose(d, u, v) return null }) // ndarray of the previous, similarly transposed var nabVoxelsT = ndarray(nabVoxelsArr, [3, 3, 3]) .lo(1, 1, 1) .transpose(d, u, v) // embiggen the cached mask arrays if needed if (maskCache.length < cs * cs) { maskCache = new Int16Array(cs * cs) aoMaskCache = new Int16Array(cs * cs) } // sets up transposed accessor for querying solidity of (i,j,k): prepareSolidityLookup(nabVoxelsT, cs) // ACTUAL MASK AND GEOMETRY CREATION // mesh plane between this chunk and previous neighbor on i axis? var prev = nabVoxelsT.get(-1, 0, 0) var here = nabVoxelsT.get(0, 0, 0) if (prev) { // offset version of neighbor to make queries work at i=-1 var prevOff = prev.lo(cs, 0, 0) var nFaces = constructMeshMask(d, prevOff, -1, here, 0) if (nFaces > 0) { constructGeometryFromMasks(0, d, u, v, cs, cs, nFaces, faceDataSet) } } // if only doing edges, we're done with this axis if (edgesOnly) continue // mesh the rest of the planes internal to this chunk // note only looping up to (size-1), skipping final coord so as // not to duplicate faces at chunk borders for (var i = 0; i < cs - 1; i++) { // maybe skip y axis, if both layers are all the same voxel if (d === 1) { var v1 = chunk._wholeLayerVoxel[i] if (v1 >= 0 && v1 === chunk._wholeLayerVoxel[i + 1]) { continue } } // pass in layer array for skip checks, only if not already checked var layerVoxRef = (d === 1) ? null : chunk._wholeLayerVoxel var nf = constructMeshMask(d, here, i, here, i + 1, layerVoxRef) if (nf > 0) { constructGeometryFromMasks(i + 1, d, u, v, cs, cs, nf, faceDataSet) } } // we skip the i-positive neighbor so as not to duplicate edge faces } // done! return faceDataSet } /** * Rigging for a transposed (i,j,k) => boolean solidity lookup, * that knows how to query into neigboring chunks at edges. * This sets up the indirection used by `voxelIsSolid` below. */ function prepareSolidityLookup(nabVoxelsT, size) { if (solidityLookupInittedSize !== size) { solidityLookupInittedSize = size voxelIDtoSolidity = noa.registry._solidityLookup for (var x = -1; x < size + 1; x++) { var loc = (x < 0) ? 0 : (x < size) ? 1 : 2 coordToLoc[x + 1] = [0, 1, 2][loc] edgeCoordLookup[x + 1] = [size - 1, x, 0][loc] missingCoordLookup[x + 1] = [0, x, size - 1][loc] } } var centerChunk = nabVoxelsT.get(0, 0, 0) for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { for (var k = 0; k < 3; k++) { var ix = i * 9 + j * 3 + k var nab = nabVoxelsT.get(i - 1, j - 1, k - 1) var type = 0 if (!nab) type = 1 if (nab === centerChunk) type = 2 voxTypeLookup[ix] = type voxLookup[ix] = nab || centerChunk } } } } var solidityLookupInittedSize = -1 var voxelIDtoSolidity = [false, true] var voxLookup = Array(27).fill(null) var voxTypeLookup = Array(27).fill(0) var coordToLoc = [0, 1, 1, 1, 1, 1, 2] var edgeCoordLookup = [3, 0, 1, 2, 3, 0] var missingCoordLookup = [0, 0, 1, 2, 3, 3] function voxelIsSolid(i, j, k) { var li = coordToLoc[i + 1] var lj = coordToLoc[j + 1] var lk = coordToLoc[k + 1] var ix = li * 9 + lj * 3 + lk var voxArray = voxLookup[ix] var type = voxTypeLookup[ix] if (type === 2) { return voxelIDtoSolidity[voxArray.get(i, j, k)] } var lookup = [edgeCoordLookup, missingCoordLookup][type] var ci = lookup[i + 1] var cj = lookup[j + 1] var ck = lookup[k + 1] return voxelIDtoSolidity[voxArray.get(ci, cj, ck)] } /** * * Build a 2D array of mask values representing whether a * mesh face is needed at each position * * Each mask value is a terrain material ID, negative if * the face needs to point in the -i direction (towards voxel arr A) * * @returns {number} number of mesh faces found */ function constructMeshMask(d, arrA, iA, arrB, iB, wholeLayerVoxel = null) { var len = arrA.shape[1] var mask = maskCache var aoMask = aoMaskCache var doAO = noa.rendering.useAO var skipRevAo = (noa.rendering.revAoVal === noa.rendering.aoVals[0]) var opacityLookup = noa.registry._opacityLookup var getMaterial = noa.registry.getBlockFaceMaterial var materialDir = d * 2 // mask is iterated by a simple integer, both here and later when // merging meshes, so the j/k order must be the same in both places var n = 0 // set up for quick ndarray traversals var indexA = arrA.index(iA, 0, 0) var jstrideA = arrA.stride[1] var kstrideA = arrA.stride[2] var indexB = arrB.index(iB, 0, 0) var jstrideB = arrB.stride[1] var kstrideB = arrB.stride[2] var facesFound = 0 for (var k = 0; k < len; ++k) { var dA = indexA var dB = indexB indexA += kstrideA indexB += kstrideB // skip this second axis, if whole layer is same voxel? if (wholeLayerVoxel && wholeLayerVoxel[k] >= 0) { n += len continue } for (var j = 0; j < len; j++, n++, dA += jstrideA, dB += jstrideB) { // mask[n] represents the face needed between the two voxel layers // for now, assume we never have two faces in both directions // note that mesher zeroes out the mask as it goes, so there's // no need to zero it here when no face is needed // IDs at i-1,j,k and i,j,k var id0 = arrA.data[dA] var id1 = arrB.data[dB] // most common case: never a face between same voxel IDs, // so skip out early if (id0 === id1) continue // no face if both blocks are opaque var op0 = opacityLookup[id0] var op1 = opacityLookup[id1] if (op0 && op1) continue // also no face if both block faces have the same block material var m0 = getMaterial(id0, materialDir) var m1 = getMaterial(id1, materialDir + 1) if (m0 === m1) continue // choose which block face to draw: // * if either block is opaque draw that one // * if either material is missing draw the other one if (op0 || m1 === 0) { mask[n] = m0 if (doAO) aoMask[n] = packAOMask(voxelIsSolid, iB, iA, j, k, skipRevAo) facesFound++ } else if (op1 || m0 === 0) { mask[n] = -m1 if (doAO) aoMask[n] = packAOMask(voxelIsSolid, iA, iB, j, k, skipRevAo) facesFound++ } else { // leftover case is two different non-opaque blocks facing each other. // Someday we could try to draw both, but for now we draw neither. } } } return facesFound } // // Greedy meshing inner loop two // // construct geometry data from the masks function constructGeometryFromMasks(i, d, u, v, len1, len2, numFaces, faceDataSet) { var doAO = noa.rendering.useAO var mask = maskCache var aomask = aoMaskCache var n = 0 var materialDir = d * 2 var x = [0, 0, 0] x[d] = i var maskCompareFcn = (doAO) ? maskCompare : maskCompare_noAO for (var k = 0; k < len2; ++k) { var w = 1 var h = 1 for (var j = 0; j < len1; j += w, n += w) { var maskVal = mask[n] | 0 if (!maskVal) { w = 1 continue } var ao = aomask[n] | 0 // Compute width and height of area with same mask/aomask values for (w = 1; w < len1 - j; ++w) { if (!maskCompareFcn(n + w, mask, maskVal, aomask, ao)) break } OUTER: for (h = 1; h < len2 - k; ++h) { for (var m = 0; m < w; ++m) { var ix = n + m + h * len1 if (!maskCompareFcn(ix, mask, maskVal, aomask, ao)) break OUTER } } // for testing: doing the following will disable greediness //w=h=1 // materialID and terrain ID type for the face var matID = Math.abs(maskVal) var terrainID = terrainIDgetter(matID) // if terrainID not seen before, start a new MeshedFaceData // from the extremely naive object pool if (!(terrainID in faceDataSet)) { var fdFromPool = faceDataPool.get() fdFromPool.numFaces = 0 fdFromPool.terrainID = terrainID faceDataSet[terrainID] = fdFromPool } // pack one face worth of data into the return struct var faceData = faceDataSet[terrainID] var nf = faceData.numFaces faceData.numFaces++ faceData.matIDs[nf] = matID x[u] = j x[v] = k faceData.is[nf] = x[0] faceData.js[nf] = x[1] faceData.ks[nf] = x[2] faceData.wids[nf] = w faceData.hts[nf] = h faceData.packedAO[nf] = ao faceData.dirs[nf] = (maskVal > 0) ? materialDir : materialDir + 1 // Face now finished, zero out the used part of the mask for (var hx = 0; hx < h; ++hx) { for (var wx = 0; wx < w; ++wx) { mask[n + wx + hx * len1] = 0 } } // exit condition where no more faces are left to mesh numFaces -= w * h if (numFaces === 0) return } } } function maskCompare(index, mask, maskVal, aomask, aoVal) { if (maskVal !== mask[index]) return false if (aoVal !== aomask[index]) return false return true } function maskCompare_noAO(index, mask, maskVal, aomask, aoVal) { if (maskVal !== mask[index]) return false return true } } /** * Extremely naive object pool for MeshedFaceData objects */ var faceDataPool = (() => { var arr = [], ix = 0 var get = () => { if (ix >= arr.length) arr.push(new MeshedFaceData) ix++ return arr[ix - 1] } var reset = () => { ix = 0 } return { get, reset } })() /** * * * * * Mesh Builder - consumes all the raw data in geomData to build * Babylon.js mesh/submeshes, ready to be added to the scene * * * * * */ /** @param {import('../index').Engine} noa */ function MeshBuilder(noa, terrainMatManager) { /** * Consume the intermediate FaceData struct and produce * actual mesehes the 3D engine can render * @param {Object.<string, MeshedFaceData>} faceDataSet */ this.buildMesh = function (chunk, faceDataSet, ignoreMaterials) { var scene = noa.rendering.getScene() var doAO = noa.rendering.useAO var aoVals = noa.rendering.aoVals var revAoVal = noa.rendering.revAoVal var atlasIndexLookup = noa.registry._matAtlasIndexLookup var matColorLookup = noa.registry._materialColorLookup var white = [1, 1, 1] // geometry data is already keyed by terrain type, so build // one mesh per geomData object in the hash var meshes = [] for (var key in faceDataSet) { var faceData = faceDataSet[key] var terrainID = faceData.terrainID // will this mesh need texture atlas indexes? var usesAtlas = false if (!ignoreMaterials) { var firstIx = atlasIndexLookup[faceData.matIDs[0]] usesAtlas = (firstIx >= 0) } // build the necessary arrays var nf = faceData.numFaces var indices = new Uint16Array(nf * 6) var positions = new Float32Array(nf * 12) var normals = new Float32Array(nf * 12) var colors = new Float32Array(nf * 16) var uvs = new Float32Array(nf * 8) var atlasIndexes if (usesAtlas) atlasIndexes = new Float32Array(nf * 4) // scan all faces in the struct, creating data for each for (var f = 0; f < faceData.numFaces; f++) { // basic data from struct var matID = faceData.matIDs[f] var materialDir = faceData.dirs[f] // 0..5: x,-x, y,-y, z,-z var i = faceData.is[f] var j = faceData.js[f] var k = faceData.ks[f] var w = faceData.wids[f] var h = faceData.hts[f] var axis = (materialDir / 2) | 0 var dir = (materialDir % 2) ? -1 : 1 addPositionValues(positions, f, i, j, k, axis, w, h) addUVs(uvs, f, axis, w, h, dir) var norms = [0, 0, 0] norms[axis] = dir addNormalValues(normals, f, norms) var ao = faceData.packedAO[f] var [A, B, C, D] = unpackAOMask(ao) var triDir = decideTriDir(A, B, C, D) addIndexValues(indices, f, axis, dir, triDir) if (usesAtlas) { var atlasIndex = atlasIndexLookup[matID] addAtlasIndices(atlasIndexes, f, atlasIndex) } var matColor = matColorLookup[matID] || white if (doAO) { pushMeshColors(colors, f, matColor, aoVals, revAoVal, A, B, C, D) } else { pushMeshColors_noAO(colors, f, matColor) } } // the mesh and vertexData object var name = `chunk_${chunk.requestID}_${terrainID}` var mesh = new Mesh(name, scene) var vdat = new VertexData() // finish the mesh vdat.positions = positions vdat.indices = indices vdat.normals = normals vdat.colors = colors vdat.uvs = uvs vdat.applyToMesh(mesh) // meshes using a texture atlas need atlasIndices if (usesAtlas) { mesh.setVerticesData('texAtlasIndices', atlasIndexes, false, 1) } // disable some unnecessary bounding checks mesh.isPickable = false mesh.doNotSyncBoundingInfo = true mesh._refreshBoundingInfo = () => mesh // materials wrangled by external module if (!ignoreMaterials) { mesh.material = terrainMatManager.getMaterial(terrainID) } // done meshes.push(mesh) } return meshes } // HELPERS ---- these could probably be simplified and less magical function addPositionValues(posArr, faceNum, i, j, k, axis, w, h) { var offset = faceNum * 12 var loc = [i, j, k] var du = [0, 0, 0] var dv = [0, 0, 0] du[(axis === 2) ? 0 : 2] = w dv[(axis === 1) ? 0 : 1] = h for (var ix = 0; ix < 3; ix++) { posArr[offset + ix] = loc[ix] posArr[offset + 3 + ix] = loc[ix] + du[ix] posArr[offset + 6 + ix] = loc[ix] + du[ix] + dv[ix] posArr[offset + 9 + ix] = loc[ix] + dv[ix] } } function addUVs(uvArr, faceNum, d, w, h, dir) { var offset = faceNum * 8 var epsilon = 0 for (var i = 0; i < 8; i++) uvArr[offset + i] = epsilon if (d === 0) { uvArr[offset + 1] = uvArr[offset + 3] = h - epsilon uvArr[offset + 2] = uvArr[offset + 4] = dir * w } else if (d === 1) { uvArr[offset + 1] = uvArr[offset + 7] = w - epsilon uvArr[offset + 4] = uvArr[offset + 6] = dir * h } else { uvArr[offset + 1] = uvArr[offset + 3] = h - epsilon uvArr[offset + 2] = uvArr[offset + 4] = -dir * w } } function addNormalValues(normArr, faceNum, norms) { var offset = faceNum * 12 for (var i = 0; i < 12; i++) { normArr[offset + i] = norms[i % 3] } } function addIndexValues(indArr, faceNum, axis, dir, triDir) { var offset = faceNum * 6 var baseIndex = faceNum * 4 if (axis === 0) dir = -dir var ix = (dir < 0) ? 0 : 1 if (!triDir) ix += 2 var indexVals = indexLists[ix] for (var i = 0; i < 6; i++) { indArr[offset + i] = baseIndex + indexVals[i] } } var indexLists = [ [0, 1, 2, 0, 2, 3], // base [0, 2, 1, 0, 3, 2], // flipped [1, 2, 3, 1, 3, 0], // opposite triDir [1, 3, 2, 1, 0, 3], // opposite triDir ] function addAtlasIndices(indArr, faceNum, atlasIndex) { var offset = faceNum * 4 for (var i = 0; i < 4; i++) { indArr[offset + i] = atlasIndex } } function decideTriDir(A, B, C, D) { // this bit is pretty magical.. // (true means split along the a00-a11 axis) if (A === C) { return (D === B) ? (D === 2) : true } else { return (D === B) ? false : (A + C > D + B) } } function pushMeshColors_noAO(colors, faceNum, col) { var offset = faceNum * 16 for (var i = 0; i < 16; i += 4) { colors[offset + i] = col[0] colors[offset + i + 1] = col[1] colors[offset + i + 2] = col[2] colors[offset + i + 3] = 1 } } function pushMeshColors(colors, faceNum, col, aoVals, revAo, A, B, C, D) { var offset = faceNum * 16 pushAOColor(colors, offset, col, A, aoVals, revAo) pushAOColor(colors, offset + 4, col, D, aoVals, revAo) pushAOColor(colors, offset + 8, col, C, aoVals, revAo) pushAOColor(colors, offset + 12, col, B, aoVals, revAo) } // premultiply vertex colors by value depending on AO level // then push them into color array function pushAOColor(colors, ix, baseCol, ao, aoVals, revAoVal) { var mult = (ao === 0) ? revAoVal : aoVals[ao - 1] colors[ix] = baseCol[0] * mult colors[ix + 1] = baseCol[1] * mult colors[ix + 2] = baseCol[2] * mult colors[ix + 3] = 1 } } /* * * * * * SHARED HELPERS - used by both main classes * * * * * */ /** * * * * packAOMask: * * For a given face, find occlusion levels for each vertex, then * pack 4 such (2-bit) values into one Uint8 value * * Occlusion levels: * 1 is flat ground, 2 is partial occlusion, 3 is max (corners) * 0 is "reverse occlusion" - an unoccluded exposed edge * Packing order var(bit offset): * * B(2) - C(6) ^ K * - - +> J * A(0) - D(4) * */ function packAOMask(isSolid, ipos, ineg, j, k, skipReverse = false) { var A = 1 var B = 1 var D = 1 var C = 1 // inc occlusion of vertex next to obstructed side if (isSolid(ipos, j + 1, k)) { ++D; ++C } if (isSolid(ipos, j - 1, k)) { ++A; ++B } if (isSolid(ipos, j, k + 1)) { ++B; ++C } if (isSolid(ipos, j, k - 1)) { ++A; ++D } // facing into a solid (non-opaque) block? var facingSolid = isSolid(ipos, j, k) if (facingSolid) { // always 2, or 3 in corners C = (C === 3 || isSolid(ipos, j + 1, k + 1)) ? 3 : 2 B = (B === 3 || isSolid(ipos, j - 1, k + 1)) ? 3 : 2 D = (D === 3 || isSolid(ipos, j + 1, k - 1)) ? 3 : 2 A = (A === 3 || isSolid(ipos, j - 1, k - 1)) ? 3 : 2 return C << 6 | D << 4 | B << 2 | A } // simpler logic if skipping reverse AO? if (skipReverse) { // treat corner as occlusion 3 only if not occluded already if (C === 1 && (isSolid(ipos, j + 1, k + 1))) { C = 2 } if (B === 1 && (isSolid(ipos, j - 1, k + 1))) { B = 2 } if (D === 1 && (isSolid(ipos, j + 1, k - 1))) { D = 2 } if (A === 1 && (isSolid(ipos, j - 1, k - 1))) { A = 2 } return C << 6 | D << 4 | B << 2 | A } // check each corner, and if not present do reverse AO if (C === 1) { if (isSolid(ipos, j + 1, k + 1)) { C = 2 } else if (!(isSolid(ineg, j, k + 1)) || !(isSolid(ineg, j + 1, k)) || !(isSolid(ineg, j + 1, k + 1))) { C = 0 } } if (D === 1) { if (isSolid(ipos, j + 1, k - 1)) { D = 2 } else if (!(isSolid(ineg, j, k - 1)) || !(isSolid(ineg, j + 1, k)) || !(isSolid(ineg, j + 1, k - 1))) { D = 0 } } if (B === 1) { if (isSolid(ipos, j - 1, k + 1)) { B = 2 } else if (!(isSolid(ineg, j, k + 1)) || !(isSolid(ineg, j - 1, k)) || !(isSolid(ineg, j - 1, k + 1))) { B = 0 } } if (A === 1) { if (isSolid(ipos, j - 1, k - 1)) { A = 2 } else if (!(isSolid(ineg, j, k - 1)) || !(isSolid(ineg, j - 1, k)) || !(isSolid(ineg, j - 1, k - 1))) { A = 0 } } return C << 6 | D << 4 | B << 2 | A } /** * * Takes in a packed AO value representing a face, * and returns four 2-bit numbers for the AO levels * at the four corners. * */ function unpackAOMask(aomask) { var A = aomask & 3 var B = (aomask >> 2) & 3 var D = (aomask >> 4) & 3 var C = (aomask >> 6) & 3 return [A, B, C, D] } var profile_hook = (PROFILE_EVERY) ? makeProfileHook(PROFILE_EVERY, 'Meshing') : () => { }