Skip to content

Instantly share code, notes, and snippets.

@martandrMC
Last active March 12, 2025 23:05
Show Gist options
  • Select an option

  • Save martandrMC/b99c29800367415a18641efa77201598 to your computer and use it in GitHub Desktop.

Select an option

Save martandrMC/b99c29800367415a18641efa77201598 to your computer and use it in GitHub Desktop.
WebGL Raymarched Metaballs
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Metaballs</title>
<script type="x-shader/x-vertex" id="vert">
attribute vec2 vpos;
void main() {
gl_Position.xy = vpos;
gl_Position.zw = vec2(0.0, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="frag">
precision mediump float;
uniform float time;
uniform float window;
#define MAX_BALLS 16
uniform int ball_count;
uniform vec4 balls[MAX_BALLS];
// https://iquilezles.org/articles/palettes/
vec3 palette(float t) {
if(t == 0.0) return vec3(0.0);
vec3 a = vec3(0.500, 0.500, 0.500);
vec3 b = vec3(0.500, 0.667, 0.600);
vec3 c = vec3(0.667, 0.666, 0.600);
vec3 d = vec3(0.250, 0.000, 0.500);
return a + b * cos(6.28319 * (c * t + d));
}
// https://iquilezles.org/articles/distfunctions/
float sdf_sphere(vec3 p, float r) { return length(p) - r; }
float smooth_merge( float d1, float d2, float k ) {
float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
return mix(d2, d1, h) - k * h * (1.0 - h);
}
// https://www.desmos.com/calculator/gtriwrakho
vec2 box_bound(vec2 p) {
float revx = 2.0 * fract(floor(p.x) / 2.0);
float revy = 2.0 * fract(floor(p.y) / 2.0);
float newx = (1.0 - 2.0 * revx) * fract(p.x) + revx;
float newy = (1.0 - 2.0 * revy) * fract(p.y) + revy;
return vec2(newx, newy);
}
float map(vec3 p) {
float d = 1e9;
for(int i = 0; i < MAX_BALLS; i++) {
if(i >= ball_count) break;
vec4 ball = balls[i];
float posx = ball.w * time + ball.z;
float posy = ball.w * ball.y * time + ball.z;
vec2 pos = 10.0 * box_bound(vec2(posx, posy)) - 5.0;
float dp = sdf_sphere(p - vec3(pos, 0.0), ball.x);
d = smooth_merge(d, dp, 2.0);
}
return d;
}
void main() {
vec2 uv = gl_FragCoord.xy / window * 2.0 - 1.0;
vec3 origin = vec3(0.0, 0.0, -10.0);
vec3 direction = normalize(vec3(uv, 1.0));
float dist = 0.0;
for(int i = 0; i < 80; i++) {
float step = map(origin + direction * dist);
dist += step;
if(step < 0.001 || dist > 100.0) break;
}
vec3 color = palette(fract(smoothstep(6.0, 12.0, dist)));
gl_FragColor = vec4(color, 1.0);
}
</script>
<script type="text/javascript">
function buildShaderProgram(webgl, vsrc, fsrc) {
function compileShader(type, source) {
const shader = webgl.createShader(type);
webgl.shaderSource(shader, source);
webgl.compileShader(shader);
if(webgl.getShaderParameter(shader, webgl.COMPILE_STATUS)) return shader;
alert("Shader compilation error: " + webgl.getShaderInfoLog(shader));
webgl.deleteShader(shader);
}
const program = webgl.createProgram();
webgl.attachShader(program, compileShader(webgl.VERTEX_SHADER, vsrc));
webgl.attachShader(program, compileShader(webgl.FRAGMENT_SHADER, fsrc));
webgl.linkProgram(program);
if (webgl.getProgramParameter(program, webgl.LINK_STATUS)) return program;
alert("Shader linking error.");
webgl.deleteProgram(program);
}
function main() {
const vsrc = document.getElementById("vert").textContent;
const fsrc = document.getElementById("frag").textContent;
const canvas_size = Math.min(window.innerHeight, window.innerWidth);
canvas = document.getElementById("gl_canvas");
canvas.width = canvas.height = 0.95 * canvas_size;
const webgl = canvas.getContext("webgl");
webgl.viewport(0, 0, canvas_size, canvas_size);
webgl.clearColor(0.2, 0.3, 0.3, 1.0);
webgl.clear(webgl.COLOR_BUFFER_BIT);
const quad = new Float32Array([
1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
1.0, 1.0, -1.0, -1.0, 1.0, -1.0
]);
let balls = [];
let ball_count = Math.floor(Math.random() * 9) + 4;
for(var i = 0; i < ball_count; i++) {
balls[4 * i + 0] = Math.random() * 1.5 + 1; // Radius [1.0, 2.5]
balls[4 * i + 1] = Math.random() * 4 - 2; // Slope [-2., +2.]
balls[4 * i + 2] = Math.random() * 10 - 5; // Offset [-5., +5.]
balls[4 * i + 3] = Math.random() * 0.2 + 0.1; // Speed [0.1, 0.3]
}
const program = buildShaderProgram(webgl, vsrc, fsrc);
webgl.useProgram(program);
const vbo = webgl.createBuffer();
webgl.bindBuffer(webgl.ARRAY_BUFFER, vbo);
webgl.bufferData(webgl.ARRAY_BUFFER, quad, webgl.STATIC_DRAW);
const vpos_loc = webgl.getAttribLocation(program, "vpos");
webgl.enableVertexAttribArray(vpos_loc);
webgl.vertexAttribPointer(vpos_loc, 2, webgl.FLOAT, false, 2 * 4, 0 * 4);
const window_loc = webgl.getUniformLocation(program, "window");
webgl.uniform1f(window_loc, 0.95 * canvas_size);
const ball_count_loc = webgl.getUniformLocation(program, "ball_count");
webgl.uniform1i(ball_count_loc, ball_count);
const balls_loc = webgl.getUniformLocation(program, "balls");
webgl.uniform4fv(balls_loc, new Float32Array(balls));
function drawLoop() {
const sec_since_load = performance.now() / 1000.0;
const time_loc = webgl.getUniformLocation(program, "time");
webgl.uniform1f(time_loc, sec_since_load);
webgl.drawArrays(webgl.TRIANGLES, 0, 6);
requestAnimationFrame(drawLoop);
}
drawLoop();
}
</script>
</head>
<body onload="main()">
<canvas id="gl_canvas"></canvas>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment