Skip to content

Instantly share code, notes, and snippets.

@thomaswilburn
Last active September 9, 2017 03:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomaswilburn/6b62632e79aee4c3e02a64be5c8defef to your computer and use it in GitHub Desktop.
Save thomaswilburn/6b62632e79aee4c3e02a64be5c8defef to your computer and use it in GitHub Desktop.
Voronoi diagram in WebGL
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Voronoi</title>
</head>
<body>
<canvas class="voronoi"></canvas>
<style>
canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<script>
var canvas = document.querySelector(".voronoi");
var gl = canvas.getContext("webgl");
/*
All the interesting stuff in this page is done
through the following fragment shader. A
fragment shader is run once for every pixel
contained in a polygon, or in our case, in the
two triangles we use to fill the canvas completely.
*/
var fragment = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragment, `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
// function that plots a circle using a distance field
// no longer used, but helpful when debugging.
float circle(vec2 pixel, vec2 position, float r) {
return step(distance(pixel, position), r);
}
// generates pseudo-random noise from an x/y value
// Not really random--calling with the same input
// returns the same output
float random(vec2 seed) {
return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123);
}
// given a "cell" onscreen, where is its associated voronoi point?
vec2 cellPoint(vec2 cell) {
float r = random(cell);
return vec2(
(sin(u_time * .0005 * r) + 1.0) / 2.0,
(cos(u_time * .002 * r) + 1.0) / 2.0
) * .5;
}
// given a cell, what color should we assign?
vec3 cellColor(vec2 cell) {
return vec3(
random(cell) * .3,
random(cell) * .8,
random(cell) * .6
);
}
void main() {
float cell_count = 10.0;
vec3 low = vec3(0.0, 0.4, 0.2);
vec3 high = vec3(0.0, 0.8, 0.6);
vec2 coord = gl_FragCoord.xy / u_resolution;
// here we split the screen into cell_count rows and columns
vec2 cell_coord = coord * cell_count;
vec2 cell = floor(cell_coord);
vec2 inner = fract(cell_coord);
vec2 point = cellPoint(cell);
// find the two closest points
// all pixels are "won" by the closest point
// then we'll use that to apply a color
// using the next-closest, we can create boundary effects
float closest = 3.0;
float next = 3.0;
vec2 winner = cell;
for (float i = -1.0; i <= 1.0; i += 1.0) {
for (float j = -1.0; j <= 1.0; j += 1.0) {
float d = distance(inner, cellPoint(cell + vec2(i, j)) + vec2(i, j));
if (d < closest) {
next = closest;
winner = cell + vec2(i, j);
closest = d;
}
}
}
// normalize the distance so that the boundary of the poly is always 0.5
float shade = closest / (closest + next);
// apply a sharp step if we want a boundary
float wall = 1.0 - step(shade, .48);
vec3 pigment = cellColor(winner);
gl_FragColor = vec4(mix(pigment, vec3(0.0), wall), 1.0);
// flat-shaded variant:
// gl_FragColor = vec4(pigment, 1.0);
}
`);
gl.compileShader(fragment);
var log = gl.getShaderInfoLog(fragment);
if (log) console.log(log);
/*
Everything under this point is boilerplate GL code.
We basically just throw two triangles together to
cover the screen and given the fragment shader
somewhere to work. It's not fancy.
The render function is responsible for passing in
the two uniforms that the fragment shader uses:
u_time, which contains a timestamp and is used for
dynamic animation, and u_resolution, which we can
use to normalize gl_FragCoord from screen pixels
into a 0.0 - 1.0 range.
*/
var vertex = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertex, `
attribute vec2 coord;
void main() {
gl_Position = vec4(coord, 0.0, 1.0);
}
`);
gl.compileShader(vertex);
var program = gl.createProgram();
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
gl.useProgram(program);
var u_resolution = gl.getUniformLocation(program, "u_resolution");
var u_time = gl.getUniformLocation(program, "u_time");
var polys = [
-1, 1,
1, 1,
1, -1,
-1, 1,
1, -1,
-1, -1
];
var coords = gl.getAttribLocation(program, "coord");
gl.enableVertexAttribArray(coords);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(polys), gl.STATIC_DRAW);
gl.vertexAttribPointer(coords, 2, gl.FLOAT, false, 0, 0);
var render = function(t) {
gl.uniform1f(u_time, t + 12581372.5324);
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(u_resolution, canvas.width, canvas.height);
gl.clearColor(0, 1, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, polys.length / 2);
requestAnimationFrame(render);
};
render();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment