webgl-3d-animation
Version:
Interactive 3D animation using WebGL showing a 2D predator prey ecology on a grid which is real-time mapped onto the surface of a 3D torus. node.js server side gives access to WAV format files which are rendered using Web Audio API
478 lines (430 loc) • 15.9 kB
JavaScript
/*jshint multistr: true */
/* Author:
Piotr Janik
*/
$(document).ready(function () {
var
gl = GL.create({ alpha: true }),
TEX_WIDTH = 128,
TEX_HEIGHT = 128,
RANGE = 1000,
plane,
textureA,
textureB,
outputTexture,
outputStorage,
outputConverted,
data,
dataTmp,
step,
renderTime,
cpuTime,
gpuTime,
readTime1,
readTime2,
statsInterval = 5,
renderShader = new GL.Shader('\
varying vec2 coord;\
void main() {\
coord = gl_TexCoord.xy;\
gl_Position = gl_Vertex;\
}\
', '\
uniform sampler2D texture;\
uniform float range;\
varying vec2 coord;\
void main() {\
gl_FragColor = vec4(texture2D(texture, coord).r / range, 0.0, 1.0 - texture2D(texture, coord).r / range, 1.0);\
}\
'),
updateShader = new GL.Shader('\
varying vec2 coord;\
void main() {\
coord = gl_Vertex.xy * 0.5 + 0.5;\
gl_Position = vec4(gl_Vertex.xyz, 1.0);\
}\
', '\
uniform sampler2D texture;\
uniform vec2 delta;\
varying vec2 coord;\
void main() {\
vec4 data = texture2D(texture, coord);\
/* calculate average neighbor value */\
vec2 dx = vec2(delta.x, 0.0);\
vec2 dy = vec2(0.0, delta.y);\
float average = (\
texture2D(texture, coord - dx).r +\
texture2D(texture, coord - dy).r +\
texture2D(texture, coord + dx).r +\
texture2D(texture, coord + dy).r\
) * 0.25;\
data += (average - data.r) * 0.5;\
gl_FragColor = data;\
}\
'),
// ========================================================================
// The first method of encoding floats based on:
// https://github.com/cscheid/facet/blob/master/src/shade/bits/encode_float.js
//
// After rendering to RGBA, UNSIGNED_BYTE texture just call gl.readPixels with
// an Uint8Array array and cast it to Float32Array.
// e.g.:
// var output = new Uint8Array(size);
// (render to RGBA texture)
// gl.readPixels(..., output);
// var result = new Float32Array(output.buffer);
//
// 'result' array should be filled with float values.
//
encodeShader1 = new GL.Shader('\
varying vec2 coord;\
void main() {\
coord = gl_Vertex.xy * 0.5 + 0.5;\
gl_Position = vec4(gl_Vertex.xyz, 1.0);\
}\
', '\
uniform sampler2D texture;\
varying vec2 coord;\
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 data = texture2D(texture, coord);\
gl_FragColor = encode_float(data.r);\
}\
'),
readTextureMethod1 = function (tex) {
outputTexture.drawTo(function () {
tex.bind();
encodeShader1.draw(plane);
// format: gl.RGBA, type: gl.UNSIGNED_BYTE - only this set is accepted by WebGL readPixels.
gl.readPixels(0, 0, outputTexture.width, outputTexture.height, outputTexture.format, outputTexture.type, outputStorage);
outputConverted = new Float32Array(outputStorage.buffer);
});
gl.finish();
},
// ========================================================================
// ========================================================================
// The second method of encoding floats based on:
// http://d.hatena.ne.jp/ultraist/20110608/1307539319
// http://www.udp.jp/misc/nvjs/gpgpu.html
//
// After rendering to RGBA, UNSIGNED_BYTE texture just call gl.readPixels with
// Uint8Array array and call decodeFloatArray.
// e.g.:
// var output = new Uint8Array(size);
// var result = new Float32Array(size / 4);
// (render to RGBA texture)
// gl.readPixels(..., output);
// decodeFloatArray(output, result);
//
// 'result' array should be filled with float values.
//
encodeShader2 = new GL.Shader('\
varying vec2 coord;\
void main() {\
coord = gl_Vertex.xy * 0.5 + 0.5;\
gl_Position = vec4(gl_Vertex.xyz, 1.0);\
}\
', '\
uniform sampler2D texture;\
varying vec2 coord;\
vec4 encode_float(float v) {\
vec4 c = vec4(0.0, 0.0, 0.0, 0.0);\
if (v < 0.0) {\
c[0] += 64.0;\
v = -v;\
}\
float f = 0.0;\
float e = ceil(log2(v));\
float m = v * exp2(-e);\
if (e < 0.0) {\
e = -e;\
c[0] += 128.0;\
}\
c[0] += e;\
m *= 255.0;\
f = floor(m);\
c[1] = f;\
m -= f;\
m *= 255.0;\
f = floor(m);\
c[2] = f;\
m -= f;\
m *= 255.0;\
c[3] = floor(m);\
return c * 3.921569E-03;\
}\
void main() {\
vec4 data = texture2D(texture, coord);\
gl_FragColor = encode_float(data.r);\
}\
'),
exp2Table = [
2.168404E-19, 4.336809E-19, 8.673617E-19, 1.734723E-18,
3.469447E-18, 6.938894E-18, 1.387779E-17, 2.775558E-17,
5.551115E-17, 1.110223E-16, 2.220446E-16, 4.440892E-16,
8.881784E-16, 1.776357E-15, 3.552714E-15, 7.105427E-15,
1.421085E-14, 2.842171E-14, 5.684342E-14, 1.136868E-13,
2.273737E-13, 4.547474E-13, 9.094947E-13, 1.818989E-12,
3.637979E-12, 7.275958E-12, 1.455192E-11, 2.910383E-11,
5.820766E-11, 1.164153E-10, 2.328306E-10, 4.656613E-10,
9.313226E-10, 1.862645E-09, 3.725290E-09, 7.450581E-09,
1.490116E-08, 2.980232E-08, 5.960464E-08, 1.192093E-07,
2.384186E-07, 4.768372E-07, 9.536743E-07, 1.907349E-06,
3.814697E-06, 7.629395E-06, 1.525879E-05, 3.051758E-05,
6.103516E-05, 1.220703E-04, 2.441406E-04, 4.882812E-04,
9.765625E-04, 1.953125E-03, 3.906250E-03, 7.812500E-03,
1.562500E-02, 3.125000E-02, 6.250000E-02, 1.250000E-01,
2.500000E-01, 5.000000E-01, 1.000000E+00, 2.000000E+00,
4.000000E+00, 8.000000E+00, 1.600000E+01, 3.200000E+01,
6.400000E+01, 1.280000E+02, 2.560000E+02, 5.120000E+02,
1.024000E+03, 2.048000E+03, 4.096000E+03, 8.192000E+03,
1.638400E+04, 3.276800E+04, 6.553600E+04, 1.310720E+05,
2.621440E+05, 5.242880E+05, 1.048576E+06, 2.097152E+06,
4.194304E+06, 8.388608E+06, 1.677722E+07, 3.355443E+07,
6.710886E+07, 1.342177E+08, 2.684355E+08, 5.368709E+08,
1.073742E+09, 2.147484E+09, 4.294967E+09, 8.589935E+09,
1.717987E+10, 3.435974E+10, 6.871948E+10, 1.374390E+11,
2.748779E+11, 5.497558E+11, 1.099512E+12, 2.199023E+12,
4.398047E+12, 8.796093E+12, 1.759219E+13, 3.518437E+13,
7.036874E+13, 1.407375E+14, 2.814750E+14, 5.629500E+14,
1.125900E+15, 2.251800E+15, 4.503600E+15, 9.007199E+15,
1.801440E+16, 3.602880E+16, 7.205759E+16, 1.441152E+17,
2.882304E+17, 5.764608E+17, 1.152922E+18, 2.305843E+18
],
decodeFloatArray = function (input, output) {
var
m, e, i_sign,
i, i4, len;
for (i = 0, len = output.length; i < len; i += 1) {
i4 = i * 4;
m = input[i4 + 1] * 3.921569E-03
+ input[i4 + 2] * 1.537870E-05
+ input[i4 + 3] * 6.030863E-08;
e = input[i4 + 0];
i_sign = 0;
if (e & 0x80) {
i_sign = 1;
e &= ~0x80;
}
if (e & 0x40) {
m = -m;
e &= ~0x40;
}
if (i_sign) {
e = -e;
}
output[i] = m * exp2Table[e + 62];
}
},
readTextureMethod2 = function (tex) {
outputTexture.drawTo(function () {
tex.bind();
encodeShader2.draw(plane);
// format: gl.RGBA, type: gl.UNSIGNED_BYTE - only this set is accepted by WebGL readPixels.
gl.readPixels(0, 0, outputTexture.width, outputTexture.height, outputTexture.format, outputTexture.type, outputStorage);
decodeFloatArray(outputStorage, outputConverted);
});
gl.finish();
},
init = function () {
var dataGPU, i, len;
// Init WebGL context.
if (!gl.getExtension('OES_texture_float')) {
throw new Error('This sddemo requires the OES_texture_float extension');
}
gl.canvas.width = 512;
gl.canvas.height = 512;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.disable(gl.DEPTH_TEST);
// Plane from -1 do 1 (x and y) with texture coordinates.
plane = GL.Mesh.plane({ coords: true });
// Two textures used for simple simulation.
// Do not use only single component textures (ALPHA/LUMINANCE), as they cause problems on some GPUs
// (when used as render targets).
textureA = new GL.Texture(TEX_WIDTH, TEX_HEIGHT, { type: gl.FLOAT, format: gl.RGBA, filter: gl.NEAREST });
textureB = new GL.Texture(TEX_WIDTH, TEX_HEIGHT, { type: gl.FLOAT, format: gl.RGBA, filter: gl.NEAREST });
// Texture used for reading data from GPU.
outputTexture = new GL.Texture(TEX_WIDTH, TEX_HEIGHT, { type: gl.UNSIGNED_BYTE, format: gl.RGBA, filter: gl.NEAREST });
outputStorage = new Uint8Array(TEX_WIDTH * TEX_HEIGHT * 4);
outputConverted = new Float32Array(TEX_WIDTH * TEX_HEIGHT);
// !!! Workaround for the bug: http://code.google.com/p/chromium/issues/detail?id=125481 !!!
// lightgl.js sets this parameter to 1 during each GL.Texture call, so overwrite it when
// all textures are created.
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
// Init data. Draw simple rectangle in the center.
data = new Float32Array(TEX_WIDTH * TEX_HEIGHT);
dataGPU = new Float32Array(TEX_WIDTH * TEX_HEIGHT * 4);
for (i = 0, len = TEX_WIDTH * TEX_HEIGHT; i < len; i += 1) {
data[i] = Math.random() * RANGE;
dataGPU[4 * i] = data[i];
dataGPU[4 * i + 1] = 0;
dataGPU[4 * i + 2] = 0;
dataGPU[4 * i + 3] = 0;
}
// No need to initialize temp data now.
dataTmp = new Float32Array(TEX_WIDTH * TEX_HEIGHT);
writeTexture(textureA, dataGPU);
// Stats.
step = 0;
renderTime = 0;
cpuTime = 0;
gpuTime = 0;
readTime1 = 0;
readTime2 = 0;
},
writeTexture = function (tex, input) {
// Make sure that texture is bound.
gl.bindTexture(gl.TEXTURE_2D, tex.id);
gl.texImage2D(gl.TEXTURE_2D, 0, tex.format, tex.width, tex.height, 0, tex.format, tex.type, input);
gl.finish();
},
render = function () {
gl.clear(gl.COLOR_BUFFER_BIT);
// Set viewport as GPGPU operations can modify it.
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
textureA.bind(0);
renderShader.uniforms({
texture: 0,
range: RANGE
}).draw(plane);
textureA.unbind(0);
gl.finish();
},
simulationStepCPU = function () {
var i, j, iwidth, idx, avg, n, w, s, e, size = TEX_WIDTH * TEX_HEIGHT;
for (i = 0; i < TEX_HEIGHT; i += 1) {
iwidth = i * TEX_WIDTH;
for (j = 0; j < TEX_WIDTH; j += 1) {
idx = iwidth + j;
// Clamp to edge.
n = idx - TEX_WIDTH; if (n < 0) n = idx;
w = idx - 1; if (w < 0) w = idx;
s = idx + TEX_WIDTH; if (s > size - 1) s = idx;
e = idx + 1; if (e > size - 1) e = idx;
avg = (data[n] + data[w] + data[s] + data[e]) * 0.25;
dataTmp[idx] = data[idx] + (avg - data[idx]) * 0.5;
}
}
data = dataTmp;
},
simulationStepGPU = function () {
textureB.drawTo(function () {
textureA.bind();
updateShader.uniforms({
delta: [1 / TEX_WIDTH, 1 / TEX_HEIGHT]
}).draw(plane);
});
textureB.swapWith(textureA);
gl.finish();
},
compareGPUAndCPUResults = function () {
var error = 0.0, i, len;
for (i = 0, len = TEX_WIDTH * TEX_HEIGHT; i < len; i += 1) {
error += Math.abs((data[i] - outputConverted[i]) / data[i]) * 100; // %
}
return error / len;
},
getTime = function () {
return (new Date()).getTime();
},
initialTest = function () {
var error, time;
// Method 1.
time = getTime();
readTextureMethod1(textureA);
time = getTime() - time;
error = compareGPUAndCPUResults();
$('#init-error-m1').text(error);
$('#init-read-time-m1').text(time);
// Method 2.
time = getTime();
readTextureMethod2(textureA);
time = getTime() - time;
error = compareGPUAndCPUResults();
$('#init-error-m2').text(error);
$('#init-read-time-m2').text(time);
},
updateStats = function () {
// $('#error').text(decimalFormat(compareGPUAndCPUResults(), 2));
$('#error').text(compareGPUAndCPUResults().toFixed(2));
$('#render-time').text((renderTime).toFixed(1));
$('#cpu-time').text((cpuTime).toFixed(1));
$('#gpu-time').text((gpuTime).toFixed(1));
$('#read-time-m1').text((readTime1).toFixed(1));
$('#read-time-m2').text((readTime2).toFixed(1));
},
onDrawCallback = function () {
var time, diff;
$('#step').text(step);
// Measure time in the first (=== 0) step differently.
time = getTime();
simulationStepGPU();
diff = getTime() - time;
gpuTime = step ? gpuTime * 0.95 + diff * 0.05 : diff;
time = getTime();
simulationStepCPU();
diff = getTime() - time;
cpuTime = step ? cpuTime * 0.95 + diff * 0.05 : diff;
time = getTime();
readTextureMethod1(textureA);
diff = getTime() - time;
readTime1 = step ? readTime1 * 0.95 + diff * 0.05 : diff;
time = getTime();
readTextureMethod2(textureA);
diff = getTime() - time;
readTime2 = step ? readTime2 * 0.95 + diff * 0.05 : diff;
time = getTime();
render();
diff = getTime() - time;
renderTime = step ? renderTime * 0.95 + diff * 0.05 : diff;
if (step % statsInterval === 0)
updateStats();
step += 1;
};
$('#grid-res').change(function () {
var val;
val = $("#grid-res").val();
TEX_WIDTH = Number(val);
TEX_HEIGHT = Number(val);
init();
initialTest();
});
$('#grid-res').change();
$('#convas-container').append(gl.canvas);
gl.ondraw = onDrawCallback;
gl.animate();
});