Exploratory webgl2 particle simulator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
15 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<title>Vahalla - Exophysics</title>
<meta charset="utf-8">
</head>
<body style="margin: 0; background: black;">
<canvas id="canvas" style="display: block; height: 100vh; width: 100vw;"></canvas>
<script>
var parameters = {
particleLimit: 256, // Keep this in sync with particleLimit in vertex shader!
extraUniforms: 1, // Update this when you add more uniforms to shader
logFPS: false,
}
</script>
<script type="x-shader/x-vertex" id="vertex-shader">
#version 300 es
// parameters
const int extraUniforms = 1;
const int particleLimit = min(gl_MaxVertexUniformVectors - extraUniforms, 256);
// data transfer variables
uniform vec2 allPositions[particleLimit];
uniform vec4 mouse;
in vec4 oldPos;
in vec4 oldV;
in int currentIndex;
out vec2 newPos;
out vec2 newV;
out lowp vec4 color;
void main() {
vec2 particle_pos;
vec2 center_of_mass;
center_of_mass = vec2(0.0, 0.0);
for (int i = 0; i < particleLimit; i++) {
particle_pos = allPositions[i];
center_of_mass += particle_pos;
}
center_of_mass /= float(particleLimit) * 1.1;
vec2 dist = allPositions[currentIndex] - center_of_mass;
newPos.x = oldPos.x + oldV.x;
newPos.y = oldPos.y + oldV.y;
if (newPos.x < -1.0) {
newPos.x = -1.0;
}
if (newPos.x > 1.0) {
newPos.x = 1.0;
}
if (newPos.y < -1.0) {
newPos.y = -1.0;
}
if (newPos.y > 1.0) {
newPos.y = 1.0;
}
newV.x = oldV.x - dist.x / 1000.0;
newV.y = oldV.y - dist.y / 1000.0;
color.b = min(1.0, max(0.0, abs(newV.x) * 100.0));
color.r = min(1.0, max(0.0, abs(newV.y) * 100.0));
color.g = 1.0 - (color.r + color.b) / 2.0;
color.a = 1.0;
if (mouse.z > 0.0) {
float dist = distance(newPos.xy, mouse.xy);
if (dist < 0.3) {
newV.x *= 1.0 - (0.3 - dist);
newV.y *= 1.0 - (0.3 - dist);
}
}
gl_Position.xy = newPos.xy;
gl_Position.zw = vec2(0.0, 1.0);
gl_PointSize = 4.0 - 2.0 * min(1.0, max(0.0, (newV.x + newV.y) * 50.0));
}
</script>
<script type="x-shader/x-fragment" id="fragment-shader">
#version 300 es
precision highp float;
in vec4 color;
out vec4 outColor;
// The color is computed by vertex shader to avoid copying uniforms twice.
void main() { outColor = color; }
</script>
<script type="application/javascript" id="boilerplate">
"use strict";
main(parameters);
function main(param) {
const canvas = document.querySelector('#canvas');
const gl = canvas.getContext('webgl2');
if (!gl) {
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
return;
}
param.particleLimit = Math.min(param.particleLimit,
gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS) - param.extraUniforms);
param.fpsCounter = 0;
param.fpsTime = 0;
param.mouseX = 0;
param.mouseY = 0;
param.mouseDown = false;
initUI(gl, param);
const shaderInfo = initShaders(gl);
const buffers = initBuffers(gl, shaderInfo, param);
drawScene(gl, shaderInfo, buffers, param);
}
function initUI(gl, param) {
gl.canvas.addEventListener('pointermove', function(e) {
const rect = canvas.getBoundingClientRect();
param.mouseX = (e.clientX - rect.left) / gl.canvas.clientWidth * 2 - 1;
param.mouseY = ((e.clientY - rect.top) / gl.canvas.clientHeight * 2 - 1) * -1;
});
gl.canvas.addEventListener('pointerdown', function(e) {
const rect = canvas.getBoundingClientRect();
param.mouseX = (e.clientX - rect.left) / gl.canvas.clientWidth * 2 - 1;
param.mouseY = ((e.clientY - rect.top) / gl.canvas.clientHeight * 2 - 1) * -1;
if (e.button == 0) {
param.mouseDown = true;
gl.canvas.setPointerCapture(e.pointerId);
}
});
gl.canvas.addEventListener('pointerup', function(e) {
const rect = canvas.getBoundingClientRect();
param.mouseX = (e.clientX - rect.left) / gl.canvas.clientWidth * 2 - 1;
param.mouseY = ((e.clientY - rect.top) / gl.canvas.clientHeight * 2 - 1) * -1;
if (e.button == 0) {
param.mouseDown = false;
gl.canvas.releasePointerCapture(e.pointerId);
}
});
}
function initShaders(gl) {
const vertexSource = document.getElementById("vertex-shader").text
const fragmentSource = document.getElementById("fragment-shader").text
const feedbackVars = ['newPos', 'newV'];
const shaderProgram = createProgram(gl, vertexSource, fragmentSource, feedbackVars);
const shaderInfo = {
program: shaderProgram,
attribLocations: {
oldPos: gl.getAttribLocation(shaderProgram, 'oldPos'),
oldV: gl.getAttribLocation(shaderProgram, 'oldV'),
currentIndex: gl.getAttribLocation(shaderProgram, 'currentIndex'),
},
uniformLocations: {
allPositions: gl.getUniformLocation(shaderProgram, 'allPositions'),
mouse: gl.getUniformLocation(shaderProgram, 'mouse'),
},
};
gl.useProgram(shaderProgram);
return shaderInfo;
}
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vertexSource, fragmentSource, feedbackVars) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource.trim());
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource.trim());
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.transformFeedbackVaryings(shaderProgram, feedbackVars, gl.SEPARATE_ATTRIBS);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
function initBuffers(gl, shaderInfo, param) {
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// initiate with random positions
const oldPos = [...Array(param.particleLimit * 2)].map(_=>Math.random() * 2 - 1);
const oldPosBuffer = gl.createBuffer();
var size = 2;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, oldPosBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(oldPos), gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(shaderInfo.attribLocations.oldPos,
size, type, normalize, stride, offset);
gl.enableVertexAttribArray(shaderInfo.attribLocations.oldPos);
// initiate with random velocity
const oldV = [...Array(param.particleLimit * 2)].map(_=>(Math.random() * 2 - 1) * 0.005);
const oldVBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, oldVBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(oldV), gl.DYNAMIC_DRAW);
var size = 2;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(shaderInfo.attribLocations.oldV,
size, type, normalize, stride, offset);
gl.enableVertexAttribArray(shaderInfo.attribLocations.oldV);
// currentIndex is simply [0, 1, 2, ...]
const currentIndex = [...Array(param.particleLimit)].map((val,key) => key)
const currentIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, currentIndexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Int32Array(currentIndex), gl.STATIC_DRAW);
var size = 1;
var type = gl.INT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribIPointer(shaderInfo.attribLocations.currentIndex,
size, type, normalize, stride, offset);
gl.enableVertexAttribArray(shaderInfo.attribLocations.currentIndex);
// buffers for reading data from the shader through transform feedback buffers
var newPosBuffer = gl.createBuffer();
var newVBuffer = gl.createBuffer();
return {
vao: vao,
oldPos: oldPosBuffer,
oldV: oldVBuffer,
newPos: newPosBuffer,
newV: newVBuffer,
currentIndex: currentIndexBuffer,
};
}
function drawScene(gl, shaderInfo, buffers, param) {
// this function will call itself in a loop forever.
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
}
// update the "mouse" uniform
gl.uniform4fv(shaderInfo.uniformLocations.mouse, new Float32Array(
[param.mouseX, param.mouseY, (param.mouseDown ? 1.0 : 0.0), 0.0]));
// Initialize reading of shader output through transform feedback buffers
var newPosArray = new Float32Array(param.particleLimit * 2);
var newVArray = new Float32Array(param.particleLimit * 2);
const tf = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buffers.newPos);
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, newPosArray, gl.STATIC_READ);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffers.newPos);
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buffers.newV);
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, newVArray, gl.STATIC_READ);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, buffers.newV);
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
gl.beginTransformFeedback(gl.POINTS);
// Draw scene
var offset = 0;
var vertexCount = param.particleLimit;
gl.drawArrays(gl.POINTS, offset, vertexCount);
// Read shader output
gl.endTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
// asynchronous callback that waits for the shader to finish running
const fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
function readShaderOutput(timestamp) {
const status = gl.clientWaitSync(fence, 0, 0);
if (status === gl.CONDITION_SATISFIED || status === gl.ALREADY_SIGNALED) {
gl.deleteSync(fence);
const output = new Float32Array(param.particleLimit * 2);
// copy the previous "newPos" values to the current "oldPos"
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.newPos);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, output);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.oldPos);
gl.bufferData(gl.ARRAY_BUFFER, output, gl.DYNAMIC_DRAW);
// update the "allPositions" uniform
gl.uniform2fv(shaderInfo.uniformLocations.allPositions, output);
// copy the previous "newV" values to the current "oldV"
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.newV);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, output);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.oldV);
gl.bufferData(gl.ARRAY_BUFFER, output, gl.DYNAMIC_DRAW);
if (param.logFPS) {
if (timestamp - 1000 > param.fpsTime) {
param.fpsTime = timestamp;
console.log('frames per second: ' + param.fpsCounter);
param.fpsCounter = 0;
}
param.fpsCounter++;
}
drawScene(gl, shaderInfo, buffers, param);
} else {
window.requestAnimationFrame(readShaderOutput);
}
}
window.requestAnimationFrame(readShaderOutput);
}
</script>
</body>
</html>