Last active
June 30, 2023 21:03
-
-
Save voidbert/3cbaf619b64ea6d865bb59d286e11ed1 to your computer and use it in GitHub Desktop.
WebGL (GPU compute) Game of Life
This file contains 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> | |
<style> | |
#canvas { | |
position: absolute; | |
top: 0px; left: 0px; | |
} | |
</style> | |
<script src="shaders.js"></script> | |
<script src="script.js"></script> | |
<meta encoding="utf-8"/> | |
</head> | |
<body> | |
<canvas id="canvas"> | |
</body> | |
</html> |
This file contains 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
const frameTime = 16.666; | |
let lastRender; | |
let canvas, gl; | |
let renderProgram, computeProgram; // OpenGL programs for rendering and calculating cells | |
let quad, framebuffer; // Needed tools for rendering (screen quad and framebuffer) | |
let computeTex, renderTex; // Textures for cell calculation and rendering, respectively | |
function onrender(timeStamp) { | |
if (lastRender && timeStamp - lastRender < frameTime) { | |
requestAnimationFrame(onrender); | |
return; | |
} | |
lastRender = timeStamp; | |
// Compute next cell generation | |
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); | |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, renderTex, 0); | |
gl.useProgram(computeProgram); | |
gl.bindTexture(gl.TEXTURE_2D, computeTex); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
// Render new generation | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
gl.useProgram(renderProgram); | |
gl.bindTexture(gl.TEXTURE_2D, renderTex); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
// Swap render and compute buffers | |
let tmp = computeTex; | |
computeTex = renderTex; | |
renderTex = tmp; | |
requestAnimationFrame(onrender); | |
} | |
function onresize() { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); | |
// Resize game of life texture (will delete current game) | |
gl.bindTexture(gl.TEXTURE_2D, renderTex); | |
clearTexture(gl.drawingBufferWidth, gl.drawingBufferHeight, false); | |
gl.bindTexture(gl.TEXTURE_2D, computeTex); | |
clearTexture(gl.drawingBufferWidth, gl.drawingBufferHeight, true); | |
// Reset shader dx and dy | |
gl.useProgram(computeProgram); | |
gl.uniform2f(gl.getUniformLocation(computeProgram, "delta"), | |
1 / gl.drawingBufferWidth, 1 / gl.drawingBufferHeight); | |
} | |
function createShader(source, type) { | |
const shader = gl.createShader(type); | |
gl.shaderSource(shader, source); | |
gl.compileShader(shader); | |
return shader; | |
} | |
function createProgram(vertexSource, fragmentSource) { | |
const vertexShader = createShader(vertexSource, gl.VERTEX_SHADER); | |
const fragmentShader = createShader(fragmentSource, gl.FRAGMENT_SHADER); | |
let program = gl.createProgram(); | |
gl.attachShader(program, vertexShader); | |
gl.attachShader(program, fragmentShader); | |
gl.linkProgram(program); | |
gl.detachShader(program, vertexShader); | |
gl.detachShader(program, fragmentShader); | |
gl.deleteShader(vertexShader); | |
gl.deleteShader(fragmentShader); | |
gl.useProgram(program); | |
return program; | |
} | |
// Fills the currently bound texture with the color black or with random data | |
function clearTexture(width, height, fillRandom) { | |
const pixel_count = 4 * width * height; | |
const buffer = new ArrayBuffer(4 * pixel_count); | |
const data = new Uint8Array(buffer); | |
if (fillRandom) { | |
for (let i = 0; i < pixel_count; i += 4) { | |
data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; | |
data[i + 3] = (Math.random() < 0.25) * 255; // Fill 25% of the cells | |
} | |
} else { | |
for (let i = 0; i < pixel_count; i++) { | |
data[i] = 0; | |
} | |
} | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); | |
} | |
function createTexture(width, height) { | |
let 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_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
return texture; | |
} | |
function initGL() { | |
// Compile shaders | |
renderProgram = createProgram(renderVertexShaderSource, renderFragmentShaderSource); | |
gl.uniform1i(gl.getUniformLocation(renderProgram, "sampler"), 0); | |
computeProgram = createProgram(computeVertexShaderSource, computeFragmentShaderSource); | |
gl.uniform1i(gl.getUniformLocation(computeProgram, "sampler"), 0); | |
// Create vertex buffer for square on the screen | |
const bufferData = new Float32Array([ | |
-1.0, -1.0, 0.0, 0.0, | |
-1.0, 1.0, 0.0, 1.0, | |
1.0, -1.0, 1.0, 0.0, | |
1.0, 1.0, 1.0, 1.0 | |
]); | |
buffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // This buffer is permanently bound | |
gl.bufferData(gl.ARRAY_BUFFER, bufferData, gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(0); // These attributes are also permanent | |
gl.enableVertexAttribArray(1); | |
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0); | |
gl.vertexAttribPointer(1, 2, gl.FLOAT, true , 16, 8); | |
// Create textures for game of life | |
gl.activeTexture(gl.TEXTURE0); | |
computeTex = createTexture(gl.drawingBufferWidth, gl.drawingBufferHeight); | |
renderTex = createTexture(gl.drawingBufferWidth, gl.drawingBufferHeight); | |
// Create target framebuffer | |
framebuffer = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); | |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, renderTex, 0); | |
} | |
function onload() { | |
canvas = document.getElementById("canvas"); | |
gl = canvas.getContext("webgl"); | |
if (gl instanceof WebGLRenderingContext) { | |
initGL(); | |
onresize(); | |
requestAnimationFrame(onrender); | |
} else { | |
alert("WebGL not available!"); | |
} | |
} | |
window.addEventListener("load", onload); | |
window.addEventListener("resize", onresize); |
This file contains 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
const renderVertexShaderSource = ` | |
#version 100 | |
precision highp float; | |
attribute vec2 pos; | |
attribute vec2 in_tex_coord; | |
varying highp vec2 tex_coord; | |
void main() { | |
gl_Position = vec4(pos, 0.0, 1.0); | |
tex_coord = in_tex_coord; | |
} | |
`; | |
const renderFragmentShaderSource = ` | |
#version 100 | |
precision mediump float; | |
varying mediump vec2 tex_coord; | |
uniform sampler2D sampler; | |
void main() { | |
gl_FragColor = texture2D(sampler, tex_coord) + vec4(0.0, 0.0, 0.0, 1.0); | |
} | |
`; | |
const computeVertexShaderSource = renderVertexShaderSource; | |
const computeFragmentShaderSource = ` | |
#version 100 | |
precision mediump float; | |
varying highp vec2 tex_coord; | |
uniform sampler2D sampler; | |
uniform highp vec2 delta; // Needs to be highp because of high resolutions | |
void main() { | |
bool current = texture2D(sampler, tex_coord).w == 1.0; | |
float neighbors = texture2D(sampler, tex_coord + vec2( delta.x, 0)).w + | |
texture2D(sampler, tex_coord + vec2(- delta.x, 0)).w + | |
texture2D(sampler, tex_coord + vec2( 0, delta.y)).w + | |
texture2D(sampler, tex_coord + vec2( 0, - delta.y)).w + | |
texture2D(sampler, tex_coord + vec2( delta.x, delta.y)).w + | |
texture2D(sampler, tex_coord + vec2(- delta.x, delta.y)).w + | |
texture2D(sampler, tex_coord + vec2( delta.x, - delta.y)).w + | |
texture2D(sampler, tex_coord + vec2(- delta.x, - delta.y)).w; | |
bool alive = (current && (neighbors == 2.0 || neighbors == 3.0)) || | |
(!current && neighbors == 3.0); | |
gl_FragColor = vec4(float(alive)); | |
} | |
`; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment