/**
* @fileOverview JavaScript/GLSL parallel reduction for Three.js
* @author Skeel Lee <skeel@skeelogy.com>
* @version 1.0.3
*
* @example
* //create a parallel reducer
* var textureRes = 1024;
* var reductionStopRes = 1;
* var pr = new SKPR.ParallelReducer(threejsWebGLRenderer, textureRes, reductionStopRes);
*
* //reduce a given texture / render target
* var reductionOp = 'sum';
* var textureChannel = 'r';
* pr.reduce(threejsRenderTargetToReduce, reductionOp, textureChannel);
*
* //if you want to read the resulting float data from the GPU to the CPU (expensive operation):
* var resultFloat32Array = pr.getPixelFloatData(textureChannel);
* var sum = 0;
* var i, len;
* for (i = 0, len = resultFloat32Array.length; i < len; i++) {
* sum += resultFloat32Array[i];
* }
*/
//FIXME: pixel access still has some problems, causing interpolated values to appear. Does not matter to 'sum' mode for some reason, but other modes like 'max' will not work.
//TODO: do a vertical flip of UVs before going into shaders, so that there's no need to constantly flip the v coordinates
/**
* @namespace
*/
var SKPR = SKPR || { version: '1.0.3' };
console.log('Using SKPR ' + SKPR.version);
/**
* Parallel reduction class
* @constructor
* @param {THREE.WebGLRenderer} renderer Renderer
* @param {number} res Power-of-2 resolution of textures to reduce
* @param {number} stopRes Power-of-2 resolution to stop the reduction process (min of 1)
*/
SKPR.ParallelReducer = function (renderer, res, stopRes) {
//store renderer
if (typeof renderer === 'undefined') {
throw new Error('renderer not specified');
}
this.__renderer = renderer;
this.__checkExtensions();
//store res
if (typeof res === 'undefined') {
throw new Error('res not specified');
}
if (res & (res - 1)) {
throw new Error('res is not a power of 2');
}
this.__res = res;
//store stop res
stopRes = stopRes || 1;
if (res & (res - 1)) {
throw new Error('res is not a power of 2');
}
this.__stopRes = stopRes;
//check that stop res is smaller than res
if (this.__res <= this.__stopRes) {
throw new Error('stopRes must be smaller than res');
}
//init
this.__init();
};
SKPR.ParallelReducer.prototype.__checkExtensions = function () {
var context = this.__renderer.context;
//determine floating point texture support
//https://www.khronos.org/webgl/public-mailing-list/archives/1306/msg00002.html
//get floating point texture support
if (!context.getExtension('OES_texture_float')) {
var msg = 'No support for floating point textures. Extension not available: OES_texture_float';
alert(msg);
throw new Error(msg);
}
//NOTE: we do not need linear filtering in this file
// //get floating point linear filtering support
// this.supportsTextureFloatLinear = context.getExtension('OES_texture_float_linear') !== null;
// if (!this.supportsTextureFloatLinear) {
// console.log('OES_texture_float available but not OES_texture_float_linear');
// }
};
SKPR.ParallelReducer.prototype.__init = function () {
this.__setupRttScene();
this.__setupRttRenderTargets();
this.__setupRttShaders();
this.__pixelByteData = new Uint8Array(this.__stopRes * this.__stopRes * 4);
};
SKPR.ParallelReducer.prototype.__setupRttScene = function () {
var size = 1.0; //arbitrary
var halfSize = size / 2.0;
this.__rttScene = new THREE.Scene();
var far = 10000;
var near = -far;
this.__rttCamera = new THREE.OrthographicCamera(-halfSize, halfSize, halfSize, -halfSize, near, far);
//create quads of different sizes to invoke the shaders
var w;
var newMaxUv = 1.0;
var scale = 1.0;
var dummyTexture = new THREE.Texture();
this.__rttQuadMeshes = [];
for (w = this.__res; w >= 1; w /= 2) {
//generate the plane geom
var rttQuadGeom = new THREE.PlaneGeometry(size, size);
rttQuadGeom.faceVertexUvs[0][0][0].set(0.0, 1.0);
rttQuadGeom.faceVertexUvs[0][0][1].set(0.0, 1.0 - newMaxUv);
rttQuadGeom.faceVertexUvs[0][0][2].set(newMaxUv, 1.0 - newMaxUv);
rttQuadGeom.faceVertexUvs[0][0][3].set(newMaxUv, 1.0);
rttQuadGeom.applyMatrix(new THREE.Matrix4().makeTranslation(0.5 * size, -0.5 * size, 0.0));
rttQuadGeom.applyMatrix(new THREE.Matrix4().makeScale(scale, scale, scale));
rttQuadGeom.applyMatrix(new THREE.Matrix4().makeTranslation(-0.5 * size, 0.5 * size, 0.0));
//add mesh
//have to load with a dummy map, or else we will get this WebGL error when we swap to another material with a texture:
//"glDrawElements: attempt to access out of range vertices in attribute"
//http://stackoverflow.com/questions/16531759/three-js-map-material-causes-webgl-warning
var rttQuadMesh = new THREE.Mesh(rttQuadGeom, new THREE.MeshBasicMaterial({map: dummyTexture}));
rttQuadMesh.visible = false;
this.__rttScene.add(rttQuadMesh);
this.__rttQuadMeshes.push(rttQuadMesh);
newMaxUv /= 2.0;
scale /= 2.0;
}
};
SKPR.ParallelReducer.prototype.__setupRttRenderTargets = function () {
this.__nearestFloatRgbaParams = {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
format: THREE.RGBAFormat,
stencilBuffer: false,
depthBuffer: false,
type: THREE.FloatType
};
this.__rttRenderTarget1 = new THREE.WebGLRenderTarget(this.__res, this.__res, this.__nearestFloatRgbaParams);
this.__rttRenderTarget1.generateMipmaps = false;
this.__rttRenderTarget2 = this.__rttRenderTarget1.clone();
};
SKPR.ParallelReducer.prototype.__setupRttShaders = function () {
this.__rttMaterials = {};
this.__rttMaterials['sum'] = new THREE.ShaderMaterial({
uniforms: {
uTexture: { type: 't', value: null },
uTexelSize: { type: 'f', value: 0 },
uHalfTexelSize: { type: 'f', value: 0 },
uChannelMask: { type: 'v4', value: new THREE.Vector4() }
},
vertexShader: this.__shaders.vert['passUv'],
fragmentShader: this.__shaders.frag['parallelSum']
});
this.__rttEncodeFloatMaterial = new THREE.ShaderMaterial({
uniforms: {
uTexture: { type: 't', value: null },
uChannelMask: { type: 'v4', value: new THREE.Vector4() }
},
vertexShader: this.__shaders.vert['passUv'],
fragmentShader: this.__shaders.frag['encodeFloat']
});
this.__channelVectors = {
'r': new THREE.Vector4(1, 0, 0, 0),
'g': new THREE.Vector4(0, 1, 0, 0),
'b': new THREE.Vector4(0, 0, 1, 0),
'a': new THREE.Vector4(0, 0, 0, 1)
};
};
SKPR.ParallelReducer.prototype.__shaders = {
vert: {
passUv: [
//Pass-through vertex shader for passing interpolated UVs to fragment shader
"varying vec2 vUv;",
"void main() {",
"vUv = vec2(uv.x, uv.y);",
"gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
"}"
].join('\n')
},
frag: {
encodeFloat: [
//Fragment shader that encodes float value in input R channel to 4 unsigned bytes in output RGBA channels
//Most of this code is from original GLSL codes from Piotr Janik, only slight modifications are done to fit the needs of this script
//http://concord-consortium.github.io/lab/experiments/webgl-gpgpu/script.js
//Using method 1 of the code.
"uniform sampler2D uTexture;",
"uniform vec4 uChannelMask;",
"varying vec2 vUv;",
"float shift_right(float v, float amt) {",
"v = floor(v) + 0.5;",
"return floor(v / exp2(amt));",
"}",
"float shift_left(float v, float amt) {",
"return floor(v * exp2(amt) + 0.5);",
"}",
"float mask_last(float v, float bits) {",
"return mod(v, shift_left(1.0, bits));",
"}",
"float extract_bits(float num, float from, float to) {",
"from = floor(from + 0.5);",
"to = floor(to + 0.5);",
"return mask_last(shift_right(num, from), to - from);",
"}",
"vec4 encode_float(float val) {",
"if (val == 0.0) {",
"return vec4(0, 0, 0, 0);",
"}",
"float sign = val > 0.0 ? 0.0 : 1.0;",
"val = abs(val);",
"float exponent = floor(log2(val));",
"float biased_exponent = exponent + 127.0;",
"float fraction = ((val / exp2(exponent)) - 1.0) * 8388608.0;",
"float t = biased_exponent / 2.0;",
"float last_bit_of_biased_exponent = fract(t) * 2.0;",
"float remaining_bits_of_biased_exponent = floor(t);",
"float byte4 = extract_bits(fraction, 0.0, 8.0) / 255.0;",
"float byte3 = extract_bits(fraction, 8.0, 16.0) / 255.0;",
"float byte2 = (last_bit_of_biased_exponent * 128.0 + extract_bits(fraction, 16.0, 23.0)) / 255.0;",
"float byte1 = (sign * 128.0 + remaining_bits_of_biased_exponent) / 255.0;",
"return vec4(byte4, byte3, byte2, byte1);",
"}",
"void main() {",
"vec4 t = texture2D(uTexture, vUv);",
"gl_FragColor = encode_float(dot(t, uChannelMask));",
"}"
].join('\n'),
parallelSum: [
//Fragment shader for performing parallel sum reduction
"uniform sampler2D uTexture;",
"uniform float uTexelSize;",
"uniform float uHalfTexelSize;",
"uniform vec4 uChannelMask;",
"varying vec2 vUv;",
"void main() {",
"//read original texture",
"vec4 t = texture2D(uTexture, vUv);",
"//expand the UVs and then read data from neighbours",
"//do dot product with uChannelMask vector to mask out only the channel value needed",
"float oneMinusHalfTexelSize = 1.0 - uHalfTexelSize;",
"vec2 expandedUv = vec2(",
"(vUv.x - uHalfTexelSize) * 2.0 + uHalfTexelSize,",
"(vUv.y - oneMinusHalfTexelSize) * 2.0 + oneMinusHalfTexelSize",
");",
"float v1 = dot(texture2D(uTexture, expandedUv), uChannelMask);",
"float v2 = dot(texture2D(uTexture, expandedUv + vec2(uTexelSize, 0.0)), uChannelMask);",
"float v3 = dot(texture2D(uTexture, expandedUv + vec2(uTexelSize, -uTexelSize)), uChannelMask);",
"float v4 = dot(texture2D(uTexture, expandedUv + vec2(0.0, -uTexelSize)), uChannelMask);",
"//sum of values",
"float final = v1 + v2 + v3 + v4;",
"gl_FragColor = (vec4(1.0) - uChannelMask) * t + uChannelMask * final;",
"}"
].join('\n')
}
};
SKPR.ParallelReducer.prototype.__swapRenderTargets = function () {
var temp = this.__rttRenderTarget1;
this.__rttRenderTarget1 = this.__rttRenderTarget2;
this.__rttRenderTarget2 = temp;
};
/**
* Initiate the reduction process
* @param {THREE.Texture | THREE.WebGLRenderTarget} texture Texture which contains data for reduction
* @param {string} type Reduction type: 'sum' (only choice available now)
* @param {string} channelId Channel to reduce: 'r', 'g', 'b' or 'a'
*/
SKPR.ParallelReducer.prototype.reduce = function (texture, type, channelId) {
var currMaterial = this.__rttMaterials[type];
var firstIteration = true;
var texelSize = 1.0 / this.__res;
var level = 1;
this.__currRes = this.__res;
while (this.__currRes > this.__stopRes) {
//reduce width by half
this.__currRes /= 2;
// console.log('currRes: ' + this.__currRes);
//render to do parallel reduction
this.__swapRenderTargets();
this.__rttQuadMeshes[level].visible = true;
this.__rttQuadMeshes[level].material = currMaterial;
currMaterial.uniforms['uTexture'].value = firstIteration ? texture : this.__rttRenderTarget2;
currMaterial.uniforms['uTexelSize'].value = texelSize;
currMaterial.uniforms['uHalfTexelSize'].value = texelSize / 2.0;
currMaterial.uniforms['uChannelMask'].value.copy(this.__channelVectors[channelId]);
this.__renderer.render(this.__rttScene, this.__rttCamera, this.__rttRenderTarget1, false);
this.__rttQuadMeshes[level].visible = false;
level += 1;
firstIteration = false;
}
};
/**
* Gets the reduced float data from the previous reduction.<br/><strong>NOTE: This is an expensive operation.</strong>
* @param {string} channelId Channel to get float data from
* @return {number} Floating point result of the reduction
*/
SKPR.ParallelReducer.prototype.getPixelFloatData = function (channelId) {
//I need to read in pixel data from WebGLRenderTarget but there seems to be no direct way.
//Seems like I have to do some native WebGL stuff with readPixels().
//need to first render the float data into an unsigned byte RGBA texture
this.__swapRenderTargets();
this.__rttQuadMeshes[0].visible = true;
this.__rttQuadMeshes[0].material = this.__rttEncodeFloatMaterial;
this.__rttEncodeFloatMaterial.uniforms['uTexture'].value = this.__rttRenderTarget2;
this.__rttEncodeFloatMaterial.uniforms['uChannelMask'].value.copy(this.__channelVectors[channelId]);
this.__renderer.render(this.__rttScene, this.__rttCamera, this.__rttRenderTarget1, false);
this.__rttQuadMeshes[0].visible = false;
var gl = this.__renderer.getContext();
//bind texture to gl context
gl.bindFramebuffer(gl.FRAMEBUFFER, this.__rttRenderTarget1.__webglFramebuffer);
//read pixels
gl.readPixels(0, this.__res - this.__stopRes, this.__stopRes, this.__stopRes, gl.RGBA, gl.UNSIGNED_BYTE, this.__pixelByteData);
//unbind
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
//cast to float
var floatData = new Float32Array(this.__pixelByteData.buffer);
return floatData;
};