You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
514 lines
23 KiB
HTML
514 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Mordial - 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: 2048,
|
|
extraUniforms: 1, // Besides "allStates", we use 1 more uniform: "userInput"
|
|
logFPS: true,
|
|
}
|
|
</script>
|
|
<script type="x-shader/x-vertex" id="vertex-shader">
|
|
#version 300 es
|
|
|
|
const float PI = 3.1415926535897932384626433832795;
|
|
const float TAU = 2.0 * PI;
|
|
|
|
// Physical Constants
|
|
const float speed = 0.010;
|
|
const float visionRadius = 0.10;
|
|
const float constantRotation = PI;
|
|
const float attractionRotation = PI / 360.0 * 36.0;
|
|
const float pointerRadius = 0.30;
|
|
const float pointerPullFactor = 0.01;
|
|
const float frictionFactor = 0.90;
|
|
const float distanceCutoff = 0.000001;
|
|
const bool cyclicalGeometry = true; // Wrap around x- and y-coordinates on the edges?
|
|
|
|
// Derived particle properties, based on flavor
|
|
const vec3 colors[] = vec3[](
|
|
vec3(0.0, 0.7, 0.0),
|
|
vec3(1.0, 0.0, 0.7),
|
|
vec3(0.0, 1.0, 1.0),
|
|
vec3(1.0, 0.8, 0.0),
|
|
vec3(0.0, 0.0, 1.0),
|
|
vec3(1.0, 1.0, 1.0),
|
|
vec3(0.5, 0.5, 0.5),
|
|
vec3(1.0, 0.0, 0.0),
|
|
vec3(0.5, 0.0, 1.0)
|
|
);
|
|
|
|
// Don't change the following line. The JavaScript code below will fill in this value
|
|
const int particleLimit = 0;
|
|
|
|
// Input/output 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 oldVelocity;
|
|
in int currentIndex;
|
|
in float random;
|
|
out vec4 state; // attributes that other particles know about: (x, y, energy, flavor)
|
|
out vec4 velocity; // internal attributes: (x-velocity, y-velocity, undefined, undefined)
|
|
out lowp vec4 color;
|
|
|
|
// This function generates pseudorandom noise, see https://stackoverflow.com/a/28095165
|
|
float randomSeed = 0.0;
|
|
float rand() {
|
|
return fract(sin(dot(vec2(randomSeed++ + float(currentIndex)),
|
|
vec2(12.9898, 78.233))) * 43758.5453);
|
|
}
|
|
|
|
float modulo2PI(float angle) {
|
|
if (angle < 0.0)
|
|
angle += TAU * ceil(-angle / TAU);
|
|
if (angle > TAU)
|
|
angle -= TAU * floor(angle / TAU);
|
|
return angle;
|
|
}
|
|
|
|
//float atan2(float y, float x) {
|
|
// bool s = abs(x) > abs(y);
|
|
// return mix(PI/2.0 - atan(x,y), atan(y,x), s);
|
|
//}
|
|
|
|
vec2 getDistance(vec2 me, vec2 other) {
|
|
vec2 dif = other.xy - me.xy;
|
|
if (cyclicalGeometry) {
|
|
if (dif.x > 1.0)
|
|
dif.x -= 2.0;
|
|
else if (dif.x < -1.0)
|
|
dif.x += 2.0;
|
|
if (dif.y > 1.0)
|
|
dif.y -= 2.0;
|
|
else if (dif.y < -1.0)
|
|
dif.y += 2.0;
|
|
}
|
|
return dif;
|
|
}
|
|
|
|
void main() {
|
|
/* state.x = X-coordinate
|
|
* state.y = Y-coordinate
|
|
* state.z = rotation (between 0 and 2*PI)
|
|
* state.w = a cache for the color index
|
|
* velocity.xyzw = ONLY used for moving particles towards the mouse pointer
|
|
*/
|
|
|
|
state = oldState;
|
|
velocity = oldVelocity;
|
|
randomSeed = random; // randomize the seed to make each run unique, if desired
|
|
|
|
gl_Position = vec4(state.xy, 0.0, 1.0);
|
|
gl_PointSize = 7.0;
|
|
|
|
// Set the color. This line is duplicated, to work even when paused.
|
|
color.rgb = colors[int(state.w)];
|
|
|
|
// Logarithmic regression: https://keisan.casio.com/exec/system/14059930226691
|
|
// with f(2048)=1.0, f(1024)=0.9, f(512)=0.7, f(256)=0.475
|
|
float scale = -0.9175 + 0.2561 * log(float(particleLimit));
|
|
|
|
// Initialization, decay, and respawning of particles:
|
|
if (state.z == 0.0) {
|
|
state.xy = vec2(rand(), rand()) * 2.0 - 1.0;
|
|
state.z = rand() * PI;
|
|
}
|
|
|
|
// Has the user paused the simulation?
|
|
if (userInput.w == 1.0) { return; }
|
|
|
|
// Apply velocity (only used for moving particles towards mouse pointer)
|
|
state.xy += velocity.xy;
|
|
|
|
// Turn direction, based on neighbours
|
|
int neighbors_left = 0;
|
|
int neighbors_right = 0;
|
|
for (int i = 0; i < particleLimit; i++) {
|
|
if (i == currentIndex) continue;
|
|
vec4 otherState = allStates[i];
|
|
vec2 dist = getDistance(state.xy, otherState.xy);
|
|
if (length(dist) < visionRadius / scale && length(dist) > distanceCutoff) {
|
|
float angle = atan(dist.y, dist.x);
|
|
angle -= state.z;
|
|
angle = modulo2PI(angle);
|
|
if (angle > PI) {
|
|
neighbors_right++;
|
|
}
|
|
else {
|
|
neighbors_left++;
|
|
}
|
|
}
|
|
}
|
|
int total_neighbors = neighbors_left + neighbors_right;
|
|
state.z += constantRotation + attractionRotation * float(total_neighbors) * float(sign(neighbors_left - neighbors_right));
|
|
|
|
// Colorize the particle
|
|
if (total_neighbors < 15)
|
|
state.w = 0.0;
|
|
else if (total_neighbors < 30)
|
|
state.w = 1.0;
|
|
else if (total_neighbors < 50)
|
|
state.w = 2.0;
|
|
else if (total_neighbors < 85)
|
|
state.w = 3.0;
|
|
else if (total_neighbors < 110)
|
|
state.w = 4.0;
|
|
else if (total_neighbors < 140)
|
|
state.w = 5.0;
|
|
else if (total_neighbors < 300)
|
|
state.w = 6.0;
|
|
else
|
|
state.w = 7.0;
|
|
color.rgb = colors[int(state.w)];
|
|
|
|
// Movement
|
|
state.xy += speed / scale * vec2(cos(state.z), sin(state.z));
|
|
|
|
// Keep the rotation between 0 and 2*PI
|
|
state.z = modulo2PI(state.z);
|
|
|
|
// Keep particles inside the universe
|
|
if (cyclicalGeometry) {
|
|
for (int dimension = 0; dimension < 2; dimension++) {
|
|
if (state[dimension] < -1.0)
|
|
state[dimension] += 2.0;
|
|
else if (state[dimension] > 1.0)
|
|
state[dimension] -= 2.0;
|
|
}
|
|
}
|
|
else {
|
|
// This doesn't work very well, but we use cyclicalGeometry anyway
|
|
for (int dimension = 0; dimension < 2; dimension++) {
|
|
if (state[dimension] < -1.0) {
|
|
state[dimension] = -1.0;
|
|
}
|
|
else if (state[dimension] > 1.0) {
|
|
state[dimension] = 1.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Movement towards the mouse pointer
|
|
if (userInput.b == 1.0) {
|
|
vec2 dist = getDistance(state.xy, userInput.xy) / pointerRadius;
|
|
if (length(dist) <= 1.0) {
|
|
if (pointerPullFactor != 0.0) {
|
|
velocity.xy += pointerPullFactor * dist * length(dist);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Friction
|
|
velocity.xy *= frictionFactor;
|
|
}
|
|
</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)
|
|
return alert('Unable to initialize WebGL. Your browser/machine may not support it.');
|
|
|
|
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) {
|
|
function updatePointer(e) {
|
|
const rect = gl.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('pointermove', updatePointer);
|
|
gl.canvas.addEventListener('pointerdown', function(e) {
|
|
updatePointer(e);
|
|
if (e.button === 0) {
|
|
param.pointerDown = true;
|
|
gl.canvas.setPointerCapture(e.pointerId);
|
|
}
|
|
});
|
|
gl.canvas.addEventListener('pointerup', function(e) {
|
|
updatePointer(e);
|
|
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 = `#version 300 es
|
|
precision lowp float; in vec4 color; out vec4 outColor;
|
|
void main() { outColor = color; }` // Just output what was put in
|
|
const feedbackVars = ['state', 'velocity'];
|
|
const shaderProgram = createProgram(gl, vertexSource, fragmentSource, feedbackVars);
|
|
|
|
const shaderInfo = {
|
|
program: shaderProgram,
|
|
attribs: {
|
|
oldState: gl.getAttribLocation(shaderProgram, 'oldState'),
|
|
oldVelocity: gl.getAttribLocation(shaderProgram, 'oldVelocity'),
|
|
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);
|
|
|
|
// Positions from previous frame
|
|
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);
|
|
|
|
// Velocities from previous frame
|
|
const oldVelocityBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, oldVelocityBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(param.particleLimit * size), gl.DYNAMIC_DRAW);
|
|
gl.vertexAttribPointer(shaderInfo.attribs.oldVelocity, 4, gl.FLOAT, false, 0, 0);
|
|
gl.enableVertexAttribArray(shaderInfo.attribs.oldVelocity);
|
|
|
|
// 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 velocityBuffer = gl.createBuffer();
|
|
|
|
return {
|
|
vao: vao,
|
|
oldState: oldStateBuffer,
|
|
oldVelocity: oldVelocityBuffer,
|
|
state: stateBuffer,
|
|
velocity: velocityBuffer,
|
|
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 (gl.canvas.width !== gl.canvas.clientWidth || gl.canvas.height !== gl.canvas.clientHeight) {
|
|
gl.canvas.width = gl.canvas.clientWidth;
|
|
gl.canvas.height = gl.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 velocityArray = 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.velocity);
|
|
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, velocityArray, gl.STATIC_READ);
|
|
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, buffers.velocity);
|
|
|
|
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 "velocity" values to the current "oldVelocity"
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.velocity);
|
|
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, output);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.oldVelocity);
|
|
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>
|