Skip to content

Instantly share code, notes, and snippets.

@adrianseeley
Last active March 22, 2023 06:10
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adrianseeley/9fd4e0a28e8f559646c4 to your computer and use it in GitHub Desktop.
Save adrianseeley/9fd4e0a28e8f559646c4 to your computer and use it in GitHub Desktop.
QP GPU (queue-pee gee-pee-you, or just Q-P for short) Quantum Particles via Graphics Processing Unit
window.onerror = function (msg, url, lineno) {
alert(url + '(' + lineno + '): ' + msg);
}
function createShader (str, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, str);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
throw gl.getShaderInfoLog(shader);
return shader;
}
function createProgram (vstr, fstr) {
var OES_texture_float = gl.getExtension('OES_texture_float');
if (!OES_texture_float)
throw new Error("No support for OES_texture_float");
var program = gl.createProgram();
var vshader = createShader(vstr, gl.VERTEX_SHADER);
var fshader = createShader(fstr, gl.FRAGMENT_SHADER);
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
throw gl.getProgramInfoLog(program);
return program;
}
function screenQuad () {
var vertexPosBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
var vertices = [-1, -1, 1, -1, -1, 1, 1, 1];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
vertexPosBuffer.itemSize = 2;
vertexPosBuffer.numItems = 4;
/*
2___3
|\ |
| \ |
|__\|
0 1
*/
return vertexPosBuffer;
}
function linkProgram (program) {
var vshader = createShader(program.vshaderSource, gl.VERTEX_SHADER);
var fshader = createShader(program.fshaderSource, gl.FRAGMENT_SHADER);
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw gl.getProgramInfoLog(program);
}
}
function loadFile (file, callback, noCache, isJson) {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == 1) {
if (isJson)
request.overrideMimeType('application/json');
request.send();
} else if (request.readyState == 4) {
if (request.status == 200)
callback(request.responseText);
else if (request.status == 404)
throw 'File "' + file + '" does not exist.';
else
throw 'XHR error ' + request.status + '.';
}
};
var url = file;
if (noCache)
url += '?' + (new Date()).getTime();
request.open('GET', url, true);
}
function loadProgram (vs, fs, callback) {
var program = gl.createProgram();
function vshaderLoaded(str) {
program.vshaderSource = str;
if (program.fshaderSource) {
linkProgram(program);
callback(program);
}
}
function fshaderLoaded (str) {
program.fshaderSource = str;
if (program.vshaderSource) {
linkProgram(program);
callback(program);
}
}
loadFile(vs, vshaderLoaded, true);
loadFile(fs, fshaderLoaded, true);
return program;
}
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelRequestAnimationFrame = window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function (callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
}())
function prepScreenQuad () {
var vertexPosBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
var vertices = [-1, -1, 1, -1, -1, 1, 1, 1];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
vertexPosBuffer.itemSize = 2;
vertexPosBuffer.numItems = 4;
/*
2 ___ 3
|\ |
| \ |
0|__\|1
*/
return vertexPosBuffer;
}
<!DOCTYPE html>
<html style="text-align: center;">
<canvas id="c" width="128" height="128" style="display: none;"></canvas>
<canvas id="d" width="512" height="512"></canvas>
<script src="glutil.js"></script>
<script id="vshader" type="text/plain">
attribute vec2 vtxpos;
varying vec2 texpos;
void main() {
texpos = (vtxpos / 2.) + vec2(0.5, 0.5);
gl_Position = vec4(vtxpos, 0, 1);
}
</script>
<script id="fshader" type="text/plain">
precision mediump float;
varying vec2 texpos;
uniform sampler2D PARTICLETEX;
uniform sampler2D ATTRACTIONTEX;
uniform int PASS;
uniform float COEFDRAG;
uniform float COEFACELSCALE;
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);
}
float vupdate (float vn) {
// calculate this particles number
float this_particle_number = ((texpos.y * PARTICLES_TALL) * PARTICLES_TALL) + (texpos.x * PARTICLES_WIDE);
// iterate through all particles
for (float x = 0.0; x < PARTICLES_WIDE; x++) {
for (float y = 0.0; y < PARTICLES_TALL; y++) {
// create texpos for other particle
vec2 that_texpos = vec2(x / PARTICLES_WIDE, y / PARTICLES_TALL);
// calculate the vector to the other particle (that - this)
vec2 vector_to_particle = vec2(texture2D(PARTICLETEX, that_texpos).b, texture2D(PARTICLETEX, that_texpos).a) - vec2(texture2D(PARTICLETEX, texpos).b, texture2D(PARTICLETEX, texpos).a);
// calculate that particle number
float that_particle_number = (y * PARTICLES_TALL) + x;
// lookup attraction pixel
vec4 attraction_pixel = texture2D(ATTRACTIONTEX, vec2(floor(this_particle_number / 4.0) / (PARTICLES_TOTAL / 4.0), that_particle_number / PARTICLES_TOTAL));
// calculate which component of attraction pixel
float attraction_color = floor(mod(this_particle_number, 4.0));
// determine attraction value (this is a switch case for rgb or a)
float attraction_value;
if (attraction_color == 0.0) attraction_value = attraction_pixel[0];
else if (attraction_color == 1.0) attraction_value = attraction_pixel[1];
else if (attraction_color == 2.0) attraction_value = attraction_pixel[2];
else if (attraction_color == 3.0) attraction_value = attraction_pixel[3];
// determine gravity value
float gravity_value = 1.;//length(vector_to_particle);
// grab the right component and update vn
if (PASS == 0) { // vx
vn += normalize(vector_to_particle).x * attraction_value * gravity_value * COEFACELSCALE;
} else if (PASS == 1) { // pass == 1, vy
vn += normalize(vector_to_particle).y * attraction_value * gravity_value * COEFACELSCALE;
}
}
}
// drag
vn *= COEFDRAG;
// return updated vn
return vn;
}
void main() {
if (PASS == 0) { // vx
// update vx
gl_FragColor = encode_float(vupdate(texture2D(PARTICLETEX, texpos).r));
} else if (PASS == 1) { // vy
// update vx
gl_FragColor = encode_float(vupdate(texture2D(PARTICLETEX, texpos).g));
} else if (PASS == 2) { // px
// px += vx;
gl_FragColor = encode_float(clamp(texture2D(PARTICLETEX, texpos).b + texture2D(PARTICLETEX, texpos).r, -1000., 1000.));
} else if (PASS == 3) { // py
// py += vy;
gl_FragColor = encode_float(clamp(texture2D(PARTICLETEX, texpos).a + texture2D(PARTICLETEX, texpos).g, -1000., 1000.));
}
}
</script>
<script>
/*
Textures are power of two, you want to be as close
as possible without going over to avoid waste.
1 x 1 = 1
2 x 2 = 4
4 x 4 = 16
8 x 8 = 64
16 x 16 = 256
32 x 32 = 1024
64 x 64 = 4096
128 x 128 = 16384
256 x 256 = 65536
512 x 512 = 262144
1024 x 1024 = 1048576
2056 x 2056 = 4227136
*/
var CONFIG_INITIALIZATION_RADIUS = 0.01;
var CONFIG_VISUAL_DRAW_RADIUS_PX = 3;
var CONFIG_VISUAL_DRAW_BORDER_PX = 20;
var CONFIG_VISUAL_CLEAR_COLOR = 'rgba(255, 255, 255, 0.1)';
var CONFIG_NUMBER_OF_PARTICLES = 4096;
var CONFIG_PARITCLE_TEXTURE_SIZE = 64;
var CONFIG_ATTRACTOR_LAMBDA = function (a, b) {
if (a == null || b == null) return 0;
return b[0] - a[0];
};
var CONFIG_SET_UNIFORMS_LAMBDA = function () {
// set drag
gl.uniform1f(program.coefdrag, 1);
// set acel scale
gl.uniform1f(program.coefacelscale, 1);
};
var CONFIG_VECTOR_CLASSES = [];
for (var test = 0; test < CONFIG_NUMBER_OF_PARTICLES; test++)
CONFIG_VECTOR_CLASSES.push([[test], [test], 'rgb(' + Math.floor((test / CONFIG_NUMBER_OF_PARTICLES) * 255) + ', 0, 0)']);
for (var shuf = 0; shuf < 10000; shuf++) {
var a = Math.floor(Math.random() * CONFIG_VECTOR_CLASSES.length);
var b = Math.floor(Math.random() * CONFIG_VECTOR_CLASSES.length);
var hold = CONFIG_VECTOR_CLASSES[a];
CONFIG_VECTOR_CLASSES[a] = CONFIG_VECTOR_CLASSES[b];
CONFIG_VECTOR_CLASSES[b] = hold;
}
var c = document.getElementById('c');
c.width = CONFIG_PARITCLE_TEXTURE_SIZE;
c.height = CONFIG_PARITCLE_TEXTURE_SIZE;
var d = document.getElementById('d');
var ctx = d.getContext('2d');
var vs = document.getElementById('vshader').textContent;
var fs = document.getElementById('fshader').textContent
.split('PARTICLES_WIDE').join(c.width + '.')
.split('PARTICLES_TALL').join(c.height + '.')
.split('PARTICLES_TOTAL').join(CONFIG_NUMBER_OF_PARTICLES + '.');
var gl = c.getContext('experimental-webgl');
var program = createProgram(vs, fs);
prepScreenQuad();
gl.useProgram(program);
gl.enableVertexAttribArray(program.vertexPosArray);
gl.vertexAttribPointer(program.vtxpos, 2, gl.FLOAT, false, 0, 0);
program.vtxpos = gl.getAttribLocation(program, 'vtxpos');
program.sampler = [gl.getUniformLocation(program, 'PARTICLETEX'), gl.getUniformLocation(program, 'ATTRACTIONTEX')];
program.pass = gl.getUniformLocation(program, 'PASS');
program.coefdrag = gl.getUniformLocation(program, 'COEFDRAG');
program.coefacelscale = gl.getUniformLocation(program, 'COEFACELSCALE');
program.tex = [gl.createTexture(), gl.createTexture()];
program.texsize = [[CONFIG_PARITCLE_TEXTURE_SIZE, CONFIG_PARITCLE_TEXTURE_SIZE], [CONFIG_NUMBER_OF_PARTICLES / 4, CONFIG_NUMBER_OF_PARTICLES]];
program.texbuff = [new Float32Array(4 * program.texsize[0][0] * program.texsize[0][1]), new Float32Array(4 * program.texsize[1][0] * program.texsize[1][1])];
program.RDBUFF = new Uint8Array(4 * c.width * c.height);
program.VXBUFF = new Float32Array(program.texsize[0][0] * program.texsize[0][1]);
program.VYBUFF = new Float32Array(program.texsize[0][0] * program.texsize[0][1]);
program.PXBUFF = new Float32Array(program.texsize[0][0] * program.texsize[0][1]);
program.PYBUFF = new Float32Array(program.texsize[0][0] * program.texsize[0][1]);
program.BUFFS = [program.VXBUFF, program.VYBUFF, program.PXBUFF, program.PYBUFF];
// establish PARTICLETEX
for (var i = 0; i < program.texbuff[0].length; i += 4) {
program.texbuff[0][i + 0] = CONFIG_INITIALIZATION_RADIUS * Math.cos((i / 4) / CONFIG_NUMBER_OF_PARTICLES * Math.PI * 2); // px
program.texbuff[0][i + 1] = CONFIG_INITIALIZATION_RADIUS * Math.sin((i / 4) / CONFIG_NUMBER_OF_PARTICLES * Math.PI * 2); // py
program.texbuff[0][i + 2] = 0; // vx
program.texbuff[0][i + 3] = 0; // vy
}
// establish ATTRACTIONTEX
var i3 = 0;
for (var i = 0; i < CONFIG_PARITCLE_TEXTURE_SIZE * CONFIG_PARITCLE_TEXTURE_SIZE; i++)
for (var i2 = 0; i2 < CONFIG_PARITCLE_TEXTURE_SIZE * CONFIG_PARITCLE_TEXTURE_SIZE; i2++)
// calculate attraction between particles vector classes (or if we have empty particle slots, pass null)
program.texbuff[1][i3++] = CONFIG_ATTRACTOR_LAMBDA(CONFIG_VECTOR_CLASSES[i] || null, CONFIG_VECTOR_CLASSES[i2] || null);
function draw () {
// find caps
var xmax = program.texbuff[0][0];
var xmin = program.texbuff[0][0];
var ymax = program.texbuff[0][1];
var ymin = program.texbuff[0][1];
// skip the first as it is our starting point, iterate by pixel (particle)
for (var i = 4; i < program.texbuff[0].length && i / 4 < CONFIG_NUMBER_OF_PARTICLES; i += 4) {
if (program.texbuff[0][i + 0] > xmax) xmax = program.texbuff[0][i + 0]; // px > xmax ? xmax = px
if (program.texbuff[0][i + 0] < xmin) xmin = program.texbuff[0][i + 0]; // px < xmin ? xmin = px
if (program.texbuff[0][i + 1] > ymax) ymax = program.texbuff[0][i + 1]; // py > ymax ? ymax = py
if (program.texbuff[0][i + 1] < ymin) ymin = program.texbuff[0][i + 1]; // py < ymin ? ymin = py
}
// define ranges
var xrng = xmax - xmin;
var yrng = ymax - ymin;
// clear
ctx.fillStyle = CONFIG_VISUAL_CLEAR_COLOR;
ctx.fillRect(0, 0, d.width, d.height);
// draw particles
for (var i = 0; i < program.texbuff[0].length && i / 4 < CONFIG_NUMBER_OF_PARTICLES; i += 4) {
ctx.beginPath();
ctx.fillStyle = CONFIG_VECTOR_CLASSES[i / 4][2];
ctx.arc((((program.texbuff[0][i + 0] - xmin) / xrng) * (d.width - (CONFIG_VISUAL_DRAW_BORDER_PX * 2))) + CONFIG_VISUAL_DRAW_BORDER_PX, (((program.texbuff[0][i + 1] - ymin) / yrng) * (d.height - (CONFIG_VISUAL_DRAW_BORDER_PX * 2))) + CONFIG_VISUAL_DRAW_BORDER_PX, CONFIG_VISUAL_DRAW_RADIUS_PX, 0, 2 * Math.PI, false);
ctx.fill();
}
};
function blit () {
// set uniforms
CONFIG_SET_UNIFORMS_LAMBDA();
// load textures
for (var t = 0; t < program.tex.length; t++) {
// set the active texture to the appropriate texture reference
gl.activeTexture(gl.TEXTURE0 + t);
// bind the appropriate texture
gl.bindTexture(gl.TEXTURE_2D, program.tex[t]);
// copy actual texture data to GPU
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, program.texsize[t][0], program.texsize[t][1], 0, gl.RGBA, gl.FLOAT, program.texbuff[t]);
// set flags to render pixel perfect
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// tell sampler to use appropriate texture unit
gl.uniform1i(program.sampler[t], t);
};
// make draw passes
for (var pass = 0; pass < 4; pass++) {
// set pass
gl.uniform1i(program.pass, pass);
// make draw call
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// read output to ubyte buffer
gl.readPixels(0, 0, c.width, c.height, gl.RGBA, gl.UNSIGNED_BYTE, program.RDBUFF);
// copy to appropriate pass buffer
program.BUFFS[pass].set(new Float32Array(program.RDBUFF.buffer));
}
// transpose new values onto state texture
for (var i = 0; i < program.texbuff[0].length; i += 4) {
program.texbuff[0][i + 0] = program.PXBUFF[i / 4]; // px
program.texbuff[0][i + 1] = program.PYBUFF[i / 4]; // py
program.texbuff[0][i + 2] = program.VXBUFF[i / 4]; // vx
program.texbuff[0][i + 3] = program.VYBUFF[i / 4]; // vy
}
};
function iter () {
requestAnimationFrame(iter);
var t = new Date().getTime();
blit();
draw();
console.log(new Date().getTime() - t);
//setTimeout(iter, 1000);
};
draw();
iter();
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment