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.

423 lines
19 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<title>Dorilia - 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 when you add more uniforms. Keep in sync too.
logFPS: true,
}
</script>
<script type="x-shader/x-vertex" id="vertex-shader">
#version 300 es
precision lowp float;
// constants
const int extraUniforms = 1;
const int particleLimit = min(gl_MaxVertexUniformVectors - extraUniforms, 256);
// tweakable parameters
const float floorGravity = 0.0;
const float particleGravity = 0.03;
const float flavorAttraction = -0.0001;
const float bounceDampeningFactor = 0.99;
const float maxV = 0.019;
const float pointerRadius = 0.30;
const float pointerSlowFactor = 0.9;
const float pointerPullFactor = 0.3;
const float energyGain = 0.001; /* it's warmer at the top, and the particles will grow */
// data transfer variables
uniform vec4 allPositions[particleLimit];
uniform vec4 pointer; /* (x, y, (pressed ? 1.0 : 0.0), 0.0) */
in vec4 oldState;
in vec4 oldV;
in int currentIndex;
in float random;
out vec4 state; // attributes that other particles know about: (x, y, energy, flavor)
out vec4 newV; // internal attributes: (dx, dy, undef, undef)
out lowp vec4 color;
float rand(vec2 co){
// https://stackoverflow.com/a/28095165
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
state = oldState;
newV = oldV;
if (state.z < 0.001) {
// Particle dies. Ashes to ashes.
newV.x = (rand(vec2(random)) * 2.0 - 1.0) * maxV;
newV.y = (rand(vec2(random + 1.0)) * 2.0 - 1.0) * maxV;
state.x = rand(vec2(random + 2.2)) * 2.0 - 1.0;
state.y = rand(vec2(random + 3.0)) * 2.0 - 1.0;
state.z = rand(vec2(random + 4.0)) * 0.5 + 0.5;
state.w = rand(vec2(random + 5.0));
}
int flavor = (state.w < 0.5) ? 1 : -1;
state.xy += oldV.xy;
state.z += state.y * energyGain;
if (particleGravity != 0.0) {
vec4 particle_pos;
vec4 center_of_mass;
center_of_mass = vec4(0.0, 0.0, 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);
vec4 dist = allPositions[currentIndex] - center_of_mass;
newV.x -= dist.x * particleGravity;
newV.y -= dist.y * particleGravity;
}
if (flavorAttraction != 0.0) {
vec4 otherState;
int otherFlavor;
vec2 dist;
for (int i = 0; i < particleLimit; i++) {
if (i == currentIndex) { continue; }
otherState = allPositions[i];
otherFlavor = (otherState.w < 0.5) ? 1 : -1;
dist = otherState.xy - state.xy;
newV.xy += dist.xy * flavorAttraction / pow(length(dist.xy), 4.0) * float(flavor * otherFlavor);
}
}
/* Limit Position */
if (state.x < -1.0) {
state.x = -1.0;
newV.x *= -bounceDampeningFactor;
}
else if (state.x > 1.0) {
state.x = 1.0;
newV.x *= -bounceDampeningFactor;
}
if (state.y < -1.0) {
state.y = -1.0;
newV.y *= -bounceDampeningFactor;
}
else if (state.y > 1.0) {
state.y = 1.0;
newV.y *= -bounceDampeningFactor;
}
/* Limit Velocity */
if (length(newV.xy) > maxV) {
newV.xy *= vec2(maxV / length(newV.xy));
}
/* Limit Energy */
if (state.z < 0.0) { state.z = 0.0; }
if (state.z > 1.0) { state.z = 1.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;
float alpha = 1.0 - state.z * 0.0; // color.a doesn't seem to work
color.r = (flavor == 1 ? 1.0 : 0.0);
color.g = (flavor == 1 ? 0.3 : 0.7) * alpha;
color.b = (flavor == 1 ? 0.0 : 1.0);
color.a = 1.0;
if (pointer.z > 0.0) {
float dist = distance(state.xy, pointer.xy);
if (dist <= pointerRadius) {
dist /= pointerRadius;
if (pointerSlowFactor != 0.0) {
newV.x *= 1.0 - pointerSlowFactor * (1.0 - dist);
newV.y *= 1.0 - pointerSlowFactor * (1.0 - dist);
}
if (pointerPullFactor != 0.0) {
newV.xy -= (state.xy - pointer.xy) * dist * pointerPullFactor;
}
}
}
if (floorGravity != 0.0) {
newV.y -= floorGravity;
}
gl_Position.xy = state.xy;
gl_Position.zw = vec2(0.0, 1.0);
//gl_PointSize = state.z * 4.0 + 2.0;
gl_PointSize = 3.0;
}
</script>
<script type="x-shader/x-fragment" id="fragment-shader">
#version 300 es
precision lowp 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.pointerX = 0;
param.pointerY = 0;
param.pointerDown = 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.pointerX = (e.clientX - rect.left) / gl.canvas.clientWidth * 2 - 1;
param.pointerY = ((e.clientY - rect.top) / gl.canvas.clientHeight * 2 - 1) * -1;
});
gl.canvas.addEventListener('pointerdown', function(e) {
const rect = canvas.getBoundingClientRect();
param.pointerX = (e.clientX - rect.left) / gl.canvas.clientWidth * 2 - 1;
param.pointerY = ((e.clientY - rect.top) / gl.canvas.clientHeight * 2 - 1) * -1;
if (e.button == 0) {
param.pointerDown = true;
gl.canvas.setPointerCapture(e.pointerId);
}
});
gl.canvas.addEventListener('pointerup', function(e) {
const rect = canvas.getBoundingClientRect();
param.pointerX = (e.clientX - rect.left) / gl.canvas.clientWidth * 2 - 1;
param.pointerY = ((e.clientY - rect.top) / gl.canvas.clientHeight * 2 - 1) * -1;
if (e.button == 0) {
param.pointerDown = 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 = ['state', 'newV'];
const shaderProgram = createProgram(gl, vertexSource, fragmentSource, feedbackVars);
const shaderInfo = {
program: shaderProgram,
attribLocations: {
oldState: gl.getAttribLocation(shaderProgram, 'oldState'),
oldV: gl.getAttribLocation(shaderProgram, 'oldV'),
currentIndex: gl.getAttribLocation(shaderProgram, 'currentIndex'),
random: gl.getAttribLocation(shaderProgram, 'random'),
},
uniformLocations: {
allPositions: gl.getUniformLocation(shaderProgram, 'allPositions'),
pointer: gl.getUniformLocation(shaderProgram, 'pointer'),
},
};
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 oldStateBuffer = gl.createBuffer();
var size = 4;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, oldStateBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(param.particleLimit * size), gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(shaderInfo.attribLocations.oldState,
size, type, normalize, stride, offset);
gl.enableVertexAttribArray(shaderInfo.attribLocations.oldState);
// initiate with random velocity
const oldVBuffer = gl.createBuffer();
var size = 4;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, oldVBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(param.particleLimit * size), gl.DYNAMIC_DRAW);
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);
// random variable, to be used for e.g. seeding a random function
const randomBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, randomBuffer);
gl.vertexAttribPointer(shaderInfo.attribLocations.random, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(shaderInfo.attribLocations.random);
// buffers for reading data from the shader through transform feedback buffers
var stateBuffer = gl.createBuffer();
var newVBuffer = gl.createBuffer();
return {
vao: vao,
oldState: oldStateBuffer,
oldV: oldVBuffer,
state: stateBuffer,
newV: newVBuffer,
currentIndex: currentIndexBuffer,
random: randomBuffer,
};
}
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 "pointer" uniform
gl.uniform4fv(shaderInfo.uniformLocations.pointer, new Float32Array(
[param.pointerX, param.pointerY, (param.pointerDown ? 1.0 : 0.0), 0.0]));
// Set the random variable
var random = [...Array(param.particleLimit)].map(_=>(Math.random()));
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.random);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(random), gl.DYNAMIC_DRAW);
// Initialize reading of shader output through transform feedback buffers
var stateArray = new Float32Array(param.particleLimit * 4);
var newVArray = new Float32Array(param.particleLimit * 4);
const tf = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buffers.state);
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, stateArray, gl.STATIC_READ);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffers.state);
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 * 4);
// copy the previous "state" values to the current "oldState"
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.state);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, output);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.oldState);
gl.bufferData(gl.ARRAY_BUFFER, output, gl.DYNAMIC_DRAW);
// update the "allPositions" uniform
gl.uniform4fv(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>