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.
481 lines
22 KiB
481 lines
22 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Swyrlyx - Exophysics</title>
|
|
<meta charset="utf-8">
|
|
</head>
|
|
<!--
|
|
Dear fellow amateur exophysicist,
|
|
|
|
I'm excited to see you embarking with me on our expedition towards new
|
|
frontiers. It takes courage to leave the comfort of the physics of your
|
|
home universe and explore the uncharted worlds of exophysics.
|
|
|
|
Along with this letter, you will find:
|
|
|
|
- One complimentary explorer hat: 👒 (limited edition)
|
|
- One Exophysics Field Manual: https://exophysics.net/manual.html
|
|
- One Exochemistry Cookbook: https://exophysics.net/cookbook.html
|
|
|
|
And this so-called "HTML file", which is a human-era magic spell that will
|
|
serve as your entrance portal. The file is self-contained, will run as-is
|
|
on any modern 2020's web browser with WebGL2 support, and contains 4 parts:
|
|
|
|
1. The HTML backbone
|
|
2. The "parameters" object, for tweaking basic settings
|
|
3. The WebGL2 vertex shader code that describes the laws of physics
|
|
4. The JavaScript code that takes care of the uninteresting plumbing
|
|
|
|
A good place to start is the vertex shader, where you can tweak physical
|
|
constants and laws, and get an intuition for what it takes to maximize the
|
|
beautiful emergent properties of a universe. Additional pointers:
|
|
|
|
- Website: https://exophysics.net/
|
|
- Code: https://github.com/exophysics/exophysics
|
|
- WebGL2 Tutorial: https://webgl2fundamentals.org/
|
|
- License: Public Domain
|
|
|
|
Divine intervention is possible through:
|
|
|
|
- Mouse click: Pull particles towards the mouse pointer
|
|
- Press ".": Step through the simulation, one frame at a time
|
|
- Space bar: Pause or resume simulation
|
|
|
|
Finally, I am legally obliged to inform you of the one law of exophysics:
|
|
YOU ARE REQUIRED TO TERMINATE A SIMULATION IMMEDIATELY UPON FINDING
|
|
EVIDENCE OF THE EMERGENCE OF SENTIENT CREATURES CAPABLE OF SUFFERING.
|
|
|
|
With that, 旅行愉快, buen viaje, счастливого пути, et un bon voyage!
|
|
-->
|
|
<body style="margin: 0; background: black;">
|
|
<canvas id="canvas" style="display: block; height: 100vh; width: 100vw;"></canvas>
|
|
<script>
|
|
var parameters = {
|
|
particleLimit: 1024, // 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;
|
|
// Don't change the following line. The JavaScript code below will fill in this value
|
|
const int particleLimit = 0;
|
|
|
|
// tweakable parameters
|
|
const float floorGravity = 0.0;
|
|
const float particleGravity = 0.00000000;
|
|
const float flavorAttraction = -0.0000035;
|
|
const float numFlavors = 5.0;
|
|
const float bounceDampeningFactor = 0.99;
|
|
const float maxV = 0.019;
|
|
const float pointerRadius = 0.30;
|
|
const float pointerSlowFactor = 0.0;
|
|
const float pointerPullFactor = 10.0;
|
|
const float energyGain = 0.001; // it's warmer at the top, and the particles will grow
|
|
|
|
// data transfer variables
|
|
uniform vec4 allStates[particleLimit];
|
|
uniform vec4 userInput; // (x, y, clicking?, paused?), the latter two can be 1.0 or 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: (x-velocity, y-velocity, undefined, undefined)
|
|
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;
|
|
|
|
gl_Position.xy = state.xy;
|
|
gl_Position.zw = vec2(0.0, 1.0);
|
|
gl_PointSize = state.z * 3.0 + 1.0;
|
|
|
|
float flavor = floor(state.w * numFlavors);
|
|
|
|
color.a = 1.0;
|
|
if (flavor == 0.0) { color.rgb = vec3(1.0, 0.3, 0.0); }
|
|
else if (flavor == 1.0) { color.rgb = vec3(0.0, 0.7, 1.0); }
|
|
else if (flavor == 2.0) { color.rgb = vec3(1.0, 1.0, 0.0); }
|
|
else if (flavor == 3.0) { color.rgb = vec3(1.0, 0.0, 1.0); }
|
|
else if (flavor == 4.0) { color.rgb = vec3(0.0, 1.0, 1.0); }
|
|
else { color.rgb = vec3(1.0, 1.0, 1.0); }
|
|
|
|
// Has the user paused the simulation?
|
|
if (userInput.w == 1.0) { return; }
|
|
|
|
// Initialization, decay, and respawning of particles:
|
|
if (state.z < 0.001) {
|
|
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));
|
|
}
|
|
|
|
// Movement
|
|
state.xy += oldV.xy;
|
|
|
|
// Gain/loss of energy
|
|
state.z += state.y * energyGain;
|
|
|
|
// Attraction of particles towards each other
|
|
if (particleGravity != 0.0) {
|
|
vec4 otherState;
|
|
vec2 dist;
|
|
for (int i = 0; i < particleLimit; i++) {
|
|
otherState = allStates[i];
|
|
dist = otherState.xy - state.xy;
|
|
if (length(dist) > 0.000001) {
|
|
newV.xy += dist / pow(length(dist), 4.0) * particleGravity;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (flavorAttraction != 0.0) {
|
|
float repulsion = pow(numFlavors - 1.0, 0.3);
|
|
vec4 otherState;
|
|
float otherFlavor;
|
|
vec2 dist;
|
|
for (int i = 0; i < particleLimit; i++) {
|
|
if (i == currentIndex) { continue; }
|
|
otherState = allStates[i];
|
|
otherFlavor = floor(otherState.w * numFlavors);
|
|
dist = otherState.xy - state.xy;
|
|
newV.xy += dist.xy
|
|
/ pow(length(dist.xy), 2.5)
|
|
* flavorAttraction
|
|
* (flavor == otherFlavor ? repulsion : -1.0);
|
|
}
|
|
}
|
|
|
|
// Movement towards the mouse pointer
|
|
if (userInput.b == 1.0) {
|
|
float dist = distance(state.xy, userInput.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 - userInput.xy) * dist * pointerPullFactor;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Movement towards the bottom
|
|
if (floorGravity != 0.0) {
|
|
newV.y -= floorGravity;
|
|
}
|
|
|
|
// 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 > 1.0) { state.z = 1.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;
|
|
param.paused = false;
|
|
|
|
initUI(gl, param);
|
|
const shaderInfo = initShaders(gl, param.particleLimit);
|
|
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);
|
|
}
|
|
});
|
|
window.addEventListener('keypress', function(e) {
|
|
if (e.key === " ")
|
|
param.paused ^= true;
|
|
else if (e.key === ".") {
|
|
param.paused = false;
|
|
param.pauseOnNextFrame = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initShaders(gl, particleLimit) {
|
|
const vertexSource = document.getElementById("vertex-shader").text
|
|
.replace(/(particleLimit = )\d+;/, '$1' + particleLimit + ';');
|
|
const fragmentSource = document.getElementById("fragment-shader").text
|
|
const feedbackVars = ['state', 'newV'];
|
|
const shaderProgram = createProgram(gl, vertexSource, fragmentSource, feedbackVars);
|
|
|
|
const shaderInfo = {
|
|
program: shaderProgram,
|
|
attribs: {
|
|
oldState: gl.getAttribLocation(shaderProgram, 'oldState'),
|
|
oldV: gl.getAttribLocation(shaderProgram, 'oldV'),
|
|
currentIndex: gl.getAttribLocation(shaderProgram, 'currentIndex'),
|
|
random: gl.getAttribLocation(shaderProgram, 'random'),
|
|
},
|
|
uniforms: {
|
|
allStates: gl.getUniformLocation(shaderProgram, 'allStates'),
|
|
userInput: gl.getUniformLocation(shaderProgram, 'userInput'),
|
|
},
|
|
};
|
|
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.attribs.oldState,
|
|
size, type, normalize, stride, offset);
|
|
gl.enableVertexAttribArray(shaderInfo.attribs.oldState);
|
|
|
|
// initiate with random velocity
|
|
const oldVBuffer = gl.createBuffer();
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, oldVBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(param.particleLimit * size), gl.DYNAMIC_DRAW);
|
|
gl.vertexAttribPointer(shaderInfo.attribs.oldV, 4, gl.FLOAT, false, 0, 0);
|
|
gl.enableVertexAttribArray(shaderInfo.attribs.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);
|
|
|
|
gl.vertexAttribIPointer(shaderInfo.attribs.currentIndex, 1, gl.INT, false, 0, 0);
|
|
gl.enableVertexAttribArray(shaderInfo.attribs.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.attribs.random, 1, gl.FLOAT, false, 0, 0);
|
|
gl.enableVertexAttribArray(shaderInfo.attribs.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 "userInput" uniform
|
|
var button_pressed = (param.pointerDown ? 1.0 : 0.0);
|
|
var paused = (param.paused ? 1.0 : 0.0);
|
|
gl.uniform4fv(shaderInfo.uniforms.userInput, new Float32Array(
|
|
[param.pointerX, param.pointerY, button_pressed, paused]));
|
|
|
|
if (param.pauseOnNextFrame) {
|
|
param.pauseOnNextFrame = false;
|
|
param.paused = true;
|
|
}
|
|
|
|
// 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 "allStates" uniform
|
|
gl.uniform4fv(shaderInfo.uniforms.allStates, 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>
|