geo-ambient-occlusion
Version:
GPU-accelerated per-vertex ambient occlusion generator built on top of regl.
311 lines (273 loc) • 8.52 kB
JavaScript
'use strict';
const REGL = require('regl');
const mat4 = require('gl-matrix').mat4;
const center = require('geo-center');
const boundingBox = require('vertices-bounding-box');
const transform = require('geo-3d-transform-mat4');
const converter = require('geo-convert-position-format');
const vertexNormals = require('normals').vertexNormals;
const reindex = require('mesh-reindex');
const defaults = require('defaults');
module.exports = function(positions, opts) {
opts = defaults(opts, {
cells: undefined,
regl: false,
bias: 0.01,
resolution: 512
});
// Center the mesh on the origin (and make a copy in the process).
positions = center(positions);
// Scale the mesh to a 1x1x1 cube.
const bb = boundingBox(positions);
const scale = [
1 / (bb[1][0] - bb[0][0]),
1 / (bb[1][1] - bb[0][1]),
1 / (bb[1][2] - bb[0][2]),
];
const scaleMat4 = mat4.scale([], mat4.create(), scale);
positions = transform(positions, scaleMat4);
// Create an array with the vertex data.
const vertexData = converter.convert(positions, converter.TYPED_ARRAY);
// Create normals.
let mesh = {
cells: opts.cells,
positions: positions,
};
if (mesh.cells === undefined) {
mesh = reindex(vertexData);
}
let normals;
if (opts.normals === undefined) {
normals = vertexNormals(mesh.cells, mesh.positions, 0);
} else {
normals = opts.normals;
}
const normalData = converter.convert(normals, converter.TYPED_ARRAY);
// Make sure the position array is divisible by three.
if (vertexData.length % 3 !== 0) {
throw new Error('geo-ambient-occlusion: Position array not divisible by three.');
}
// Copy it into the smallest POT-length array we can.
const vertexCount = vertexData.length/3;
let vertexTextureRes = 1;
while (vertexTextureRes * vertexTextureRes < vertexCount) {
vertexTextureRes *= 2;
}
const vertexDataArray = new Float32Array(vertexTextureRes * vertexTextureRes * 3);
vertexDataArray.set(vertexData);
// Copy the normal data into a same-size array.
const normalDataArray = new Float32Array(vertexTextureRes * vertexTextureRes * 3);
normalDataArray.set(normalData);
// Figure out what extensions we need.
const extensions = ['OES_texture_float'];
if (vertexCount > 65535 && opts.cells !== undefined) {
extensions.push('OES_element_index_uint');
}
// If we are given a regl context, make sure it has the extensions we need.
if (opts.regl) {
for (let ext of extensions) {
if (!opts.regl.hasExtension(ext)) {
throw new Error('geo-ambient-occlusion: Provided regl context needs the ' + ext + ' extension for this mesh.');
}
}
}
// If we don't have a regl context, create one.
let regl, ownregl;
if (opts.regl) {
regl = opts.regl;
ownregl = false;
} else {
regl = REGL({
canvas: document.createElement('canvas'),
extensions: extensions,
});
ownregl = true;
}
// Define a framebuffer for our position data.
const fboPosition = fbo(opts.resolution);
// Define the command that gathers the position data.
const cmdPositionObj = {
vert: `
precision highp float;
attribute vec3 position;
uniform mat4 model, projection;
varying vec3 vPos;
void main() {
gl_Position = projection * model * vec4(position, 1);
vPos = vec3(model * vec4(position, 1));
}
`,
frag: `
precision highp float;
varying vec3 vPos;
void main() {
gl_FragColor = vec4(vPos, 1);
}
`,
attributes: {
position: positions,
},
uniforms: {
model: regl.prop('model'),
projection: regl.prop('projection'),
},
viewport: {x: 0, y: 0, width: opts.resolution, height: opts.resolution},
framebuffer: fboPosition,
};
if (opts.cells) {
cmdPositionObj.elements = opts.cells;
} else {
cmdPositionObj.count = vertexCount;
}
const cmdPosition = regl(cmdPositionObj);
// Define a pair of buffers we'll ping-pong to accumulate occlusion data.
const fboOcclusion = [fbo(vertexTextureRes), fbo(vertexTextureRes)];
// Create a texture that stores our vertex locations.
const tVertex = regl.texture({
width: vertexTextureRes,
height: vertexTextureRes,
data: vertexDataArray,
format: 'rgb',
type: 'float'
});
// Create a texture that stores our normal directions.
const tNormal = regl.texture({
width: vertexTextureRes,
height: vertexTextureRes,
data: normalDataArray,
format: 'rgb',
type: 'float',
});
// Define the command for occlusion accumulation.
const cmdOcclusion = regl({
vert: `
precision highp float;
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0, 1);
}
`,
frag: `
precision highp float;
uniform sampler2D tPosition, tSource, tVertex, tNormal;
uniform float count, bias;
uniform vec2 resolution;
uniform mat4 model;
const float INVSQRT3 = 0.5773502691896258; // 1/sqrt(3)
void main() {
vec2 texel = gl_FragCoord.xy/resolution;
vec3 vert = texture2D(tVertex, texel).rgb;
vert = vec3(model * vec4(vert, 1));
vec3 norm = texture2D(tNormal, texel).rgb;
norm = vec3(model * vec4(norm, 1));
float z = texture2D(tPosition, INVSQRT3 * vert.xy + 0.5).z;
float o = 0.0;
if ((vert.z - z) < -bias) {
o = 1.0;
}
vec4 src = texture2D(tSource, texel);
if (dot(norm, vec3(0,0,1)) > 0.0) {
gl_FragColor = src + vec4(o, 1, 0, 0);
} else {
gl_FragColor = src;
}
}
`,
attributes: {
position: [-1,-1, 1,-1, 1,1, -1,-1, 1,1, -1,1],
},
uniforms: {
tPosition: fboPosition,
tSource: regl.prop('source'),
tVertex: tVertex,
tNormal: tNormal,
count: regl.prop('count'),
bias: regl.prop('bias'),
resolution: [vertexTextureRes, vertexTextureRes],
model: regl.prop('model'),
},
viewport: {x: 0, y: 0, width: vertexTextureRes, height: vertexTextureRes},
framebuffer: regl.prop('destination'),
count: 6,
});
// Keep track of our ping-ponging.
let occlusionIndex = 0;
// Keep track of how many samples we've collected.
let occlusionCount = 0;
// set up our projection matrix.
const d = Math.sqrt(3)/2;
const projection = mat4.ortho([], -d, d, -d, d, -d, d);
// Take a single occlusion sample.
function sample() {
occlusionIndex = 1 - occlusionIndex;
const source = fboOcclusion[occlusionIndex];
const destination = fboOcclusion[1 - occlusionIndex];
occlusionCount++;
const model = mat4.create();
mat4.rotateX(model, model, Math.random() * 100);
mat4.rotateY(model, model, Math.random() * 100);
mat4.rotateZ(model, model, Math.random() * 100);
regl.clear({
color: [0,0,0,1],
depth: 1,
framebuffer: fboPosition,
});
cmdPosition({
model: model,
projection: projection,
});
regl.clear({
color: [0,0,0,1],
depth: 1,
framebuffer: destination,
});
cmdOcclusion({
source: source,
destination: destination,
count: occlusionCount,
bias: opts.bias,
model: model,
});
}
// Return the per-vertex ambient occlusion in a Float32Array of length vertexCount.
function report() {
// Gather the resulting pixels.
let pixels;
fboOcclusion[1 - occlusionIndex].use(() => {
pixels = regl.read();
});
// Format them and return the final product.
const result = new Float32Array(vertexCount);
for (let i = 0; i < vertexCount; i++) {
const index = i * 4;
const total = pixels[index + 1];
result[i] = total === 0.0 ? 0.0 : pixels[index + 0]/total;
}
return result;
}
// Dispose of all resources. Do not dispose of regl if we did not create it. Behavior after calling dispose is
// undefined.
function dispose() {
tVertex.destroy();
fboPosition.destroy();
fboOcclusion[0].destroy();
fboOcclusion[1].destroy();
if (ownregl) {
regl.destroy();
}
}
// Utility function for creating a common fbo.
function fbo(resolution) {
return regl.framebuffer({
width: resolution,
height: resolution,
colorFormat: 'rgba',
colorType: 'float',
});
}
return {
sample: sample,
report: report,
dispose: dispose
};
};