This is a Gray-Scott system, using parameters from Karl Sims: F=.0545, K=.062.
Last active
January 12, 2023 06:16
A reaction-diffusion simulation using WebGL
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> | |
<meta charset="utf-8"> | |
<title>Gray-Scott coral using WebGL</title> | |
<style> | |
body { font-family: sans-serif; } | |
canvas { display: block; margin: auto; } | |
#fail { color: red; white-space: pre-wrap; max-width: 600px; } | |
</style> | |
<script type="x-webgl/x-vertex-shader" id="vertex-shader"> | |
attribute vec2 a_position; | |
void main() { | |
gl_Position = vec4(a_position, 0, 1); | |
} | |
</script> | |
<script type="x-webgl/x-fragment-shader" id="timestep-shader"> | |
precision mediump float; | |
uniform sampler2D u_image; | |
uniform vec2 u_size; | |
const float F = 0.0545, K = 0.062, | |
D_a = 0.2, D_b = 0.1; | |
const float TIMESTEP = 1.0; | |
void main() { | |
vec2 p = gl_FragCoord.xy, | |
n = p + vec2(0.0, 1.0), | |
e = p + vec2(1.0, 0.0), | |
s = p + vec2(0.0, -1.0), | |
w = p + vec2(-1.0, 0.0); | |
vec2 val = texture2D(u_image, p / u_size).xy, | |
laplacian = texture2D(u_image, n / u_size).xy | |
+ texture2D(u_image, e / u_size).xy | |
+ texture2D(u_image, s / u_size).xy | |
+ texture2D(u_image, w / u_size).xy | |
- 4.0 * val; | |
vec2 delta = vec2(D_a * laplacian.x - val.x*val.y*val.y + F * (1.0-val.x), | |
D_b * laplacian.y + val.x*val.y*val.y - (K+F) * val.y); | |
gl_FragColor = vec4(val + delta * TIMESTEP, 0, 0); | |
} | |
</script> | |
<script type="x-webgl/x-fragment-shader" id="render-shader"> | |
precision mediump float; | |
uniform sampler2D u_image; | |
uniform vec2 u_size; | |
const float COLOR_MIN = 0.2, COLOR_MAX = 0.4; | |
void main() { | |
float v = (COLOR_MAX - texture2D(u_image, gl_FragCoord.xy / u_size).y) / (COLOR_MAX - COLOR_MIN); | |
gl_FragColor = vec4(v, v, v, 1); | |
} | |
</script> | |
<body> | |
<script> | |
var W = 512, H = 512; | |
function init() { | |
var canvas = document.createElement("canvas"); | |
canvas.id = "canvas"; | |
canvas.width = W; | |
canvas.height = H; | |
document.body.appendChild(canvas); | |
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); | |
checkCompatibility(gl); | |
var vertex_shader = createShader(gl, gl.VERTEX_SHADER, "vertex-shader"), | |
timestep_shader = createShader(gl, gl.FRAGMENT_SHADER, "timestep-shader"), | |
render_shader = createShader(gl, gl.FRAGMENT_SHADER, "render-shader"); | |
var timestep_prog = createAndLinkProgram(gl, vertex_shader, timestep_shader), | |
render_prog = createAndLinkProgram(gl, vertex_shader, render_shader); | |
gl.useProgram(render_prog); | |
loadVertexData(gl, render_prog); | |
gl.uniform2f(gl.getUniformLocation(render_prog, "u_size"), W, H); | |
gl.useProgram(timestep_prog); | |
loadVertexData(gl, timestep_prog); | |
gl.uniform2f(gl.getUniformLocation(timestep_prog, "u_size"), W, H); | |
var initial_state = getInitialState(); | |
var t1 = newTexture(gl, initial_state), | |
t2 = newTexture(gl, null), | |
fb1 = newFramebuffer(gl, t1), | |
fb2 = newFramebuffer(gl, t2); | |
// Check the hardware can render to a float framebuffer | |
// (https://developer.mozilla.org/en-US/docs/Web/WebGL/WebGL_best_practices) | |
gl.useProgram(timestep_prog); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, fb1); | |
var fb_status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); | |
if (fb_status != gl.FRAMEBUFFER_COMPLETE) { | |
fail("Cannot render to framebuffer: " + fb_status); | |
} | |
function step() { | |
gl.useProgram(timestep_prog); | |
for (var i=0; i<40; i++) { | |
gl.bindTexture(gl.TEXTURE_2D, [t1, t2][i % 2]); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, [fb2, fb1][i % 2]); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
} | |
gl.useProgram(render_prog); | |
gl.bindTexture(gl.TEXTURE_2D, t1); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
} | |
var af; | |
function frame() { | |
step(); | |
af = requestAnimationFrame(frame); | |
} | |
af = requestAnimationFrame(frame); | |
} | |
function getInitialState() { | |
var a = new Float32Array(4 * W * H); | |
for (var y=0; y<H; y++) { | |
for (var x=0; x<W; x++) { | |
var i = W*y + x; | |
var central_square = (x > W/2-10 && x < W/2 + 10 && y > H/2-10 && y < H/2+10); | |
if (central_square) { | |
a[4*i + 0] = 0.5 + Math.random() * 0.02 - 0.01; | |
a[4*i + 1] = 0.25 + Math.random() * 0.02 - 0.01; | |
} else { | |
a[4*i + 0] = 1.0; | |
a[4*i + 1] = 0; | |
} | |
} | |
} | |
return a; | |
} | |
// Create, initialise, and bind a new texture | |
function newTexture(gl, initial_state) { | |
var texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.FLOAT, initial_state); | |
return texture; | |
} | |
function newFramebuffer(gl, texture) { | |
var fb = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, fb); | |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); | |
return fb; | |
} | |
function loadVertexData(gl, prog) { | |
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1,-1, 1,-1, -1,1, 1,1 ]), gl.STATIC_DRAW); | |
var a_position = gl.getAttribLocation(prog, "a_position"); | |
gl.enableVertexAttribArray(a_position); | |
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0); | |
} | |
function createAndLinkProgram(gl, vertex_shader, fragment_shader) { | |
var prog = gl.createProgram(); | |
gl.attachShader(prog, vertex_shader); | |
gl.attachShader(prog, fragment_shader); | |
gl.linkProgram(prog); | |
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { | |
fail("Failed to link program: " + gl.getProgramInfoLog(prog)); | |
} | |
return prog; | |
} | |
function createShader(gl, shader_type, shader_code_id) { | |
var shader = gl.createShader(shader_type); | |
gl.shaderSource(shader, document.getElementById(shader_code_id).text); | |
gl.compileShader(shader); | |
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
var err = gl.getShaderInfoLog(shader); | |
fail("Failed to compile shader: " + err); | |
} | |
return shader | |
} | |
function checkCompatibility(gl) { | |
if (!gl) fail("WebGL is not supported"); | |
var float_texture_ext = gl.getExtension("OES_texture_float"); | |
if (!float_texture_ext) fail("Your browser does not support the WebGL extension OES_texture_float"); | |
window.float_texture_ext = float_texture_ext; // Hold onto it | |
var max_texture_size = gl.getParameter(gl.MAX_TEXTURE_SIZE); | |
if (max_texture_size < 512) fail("Your browser only supports "+max_texture_size+"×"+max_texture_size+" WebGL textures"); | |
} | |
function fail(message) { | |
var fail = document.createElement("p"); | |
fail.id = "fail"; | |
fail.appendChild(document.createTextNode(message)); | |
document.body.removeChild(document.getElementById("canvas")); | |
document.body.appendChild(fail); | |
throw message; | |
} | |
init(); | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment