Last active
March 12, 2025 23:05
-
-
Save martandrMC/b99c29800367415a18641efa77201598 to your computer and use it in GitHub Desktop.
WebGL Raymarched Metaballs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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