Skip to content

Instantly share code, notes, and snippets.

@benc-uk
Last active October 25, 2025 10:37
Show Gist options
  • Save benc-uk/9edcefb9453f5b8cabc76ec0ae619252 to your computer and use it in GitHub Desktop.
Save benc-uk/9edcefb9453f5b8cabc76ec0ae619252 to your computer and use it in GitHub Desktop.
<html lang="en">
<head>
<title>Single Page GPU Raytracer - Ben Coleman</title>
</head>
<body style="margin: 0">
<canvas style="width: 100%" width="100" height="50"></canvas>
<script id="vs" type="x-shader/x-vertex">
#version 300 es
in vec4 position;
void main() { gl_Position = position; }
</script>
<script id="fs" type="x-shader/x-fragment">
#version 300 es
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
out vec4 fragColor;
struct Sphere { vec3 pos; float rad; vec3 color; };
Sphere scene[4] = Sphere[4](
Sphere(vec3(-2.4, 0.0, 0.0), 1.3, vec3(1.0, 0.2, 0.2)),
Sphere(vec3(1.4, 0.5, 4.8), 0.6, vec3(0.2, 0.8, 0.2)),
Sphere(vec3(0.0, 0.0, 3.0), 1.3, vec3(0.2, 0.2, 0.9)),
Sphere(vec3(0.0, -20002.5, 3.0), 20000.0, vec3(0.8, 0.7, 0.4)) // ground
);
float sphereHit(vec3 ro, vec3 rd, Sphere sph) {
vec3 oc = ro - sph.pos;
float b = dot(oc, rd);
float c = dot(oc, oc) - sph.rad * sph.rad;
float h = b * b - c;
if (h < 0.0) return -1.0; else return -b - sqrt(h);
}
void main() {
scene[0].pos.y += cos(u_time*3.0);
scene[1].pos.y += cos(u_time*3.5);
scene[2].pos.y += cos(u_time*1.5);
vec2 screenPos = (gl_FragCoord.xy / u_resolution) * vec2(1.0, 0.5) + vec2(0.0, 0.25);
vec3 ro = vec3(0.0, 0.0, 13.0);
vec3 rd = normalize(vec3(screenPos - 0.5, -1.0));
float minT = 1e9;
int hitIndex = -1;
for (int i = 0; i < scene.length(); i++) {
float t = sphereHit(ro, rd, scene[i]);
if (t > 0.0 && t < minT) {
minT = t;
hitIndex = i;
}
}
vec3 color = vec3(0.0);
vec3 lightPos = vec3(9.0, 13.0, 8.0);
if (hitIndex >= 0) {
vec3 pos = ro + rd * minT;
vec3 normal = normalize(pos - scene[hitIndex].pos);
vec3 lightDir = normalize(lightPos - pos);
float diff = max(dot(normal, lightDir), 0.0);
float specular = pow(max(dot(reflect(-lightDir, normal), -rd), 0.0), 50.0);
float shadowT = 1e9;
for (int i = 0; i < scene.length(); i++) {
if (i == hitIndex) continue;
float t = sphereHit(pos + normal * 0.001, lightDir, scene[i]);
if (t > 0.0 && t < shadowT) {
shadowT = t;
}
}
if (shadowT < 1e8) { diff *= 0.1; specular = 0.0; }
color = scene[hitIndex].color * diff + 0.02 + specular;
}
fragColor = vec4(color, 1.0);
}
</script>
<script type="module">
import * as twgl from "https://esm.sh/twgl.js";
const gl = document.querySelector("canvas").getContext("webgl2");
const progInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
gl.useProgram(progInfo.program);
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: [-1, -1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0],
});
twgl.setBuffersAndAttributes(gl, progInfo, bufferInfo);
function render(time) {
twgl.setUniforms(progInfo, {
u_time: time * 0.001,
u_resolution: [gl.canvas.width, gl.canvas.height],
});
twgl.drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
<html lang="en">
<head>
<title>Single Page GPU SDF Ray Marcher - Ben Coleman</title>
</head>
<body style="margin: 0">
<canvas style="width: 100%" width="640" height="480"></canvas>
<script id="vs" type="x-shader/x-vertex">
#version 300 es
in vec4 position;
void main() { gl_Position = position; }
</script>
<script id="fs" type="x-shader/x-fragment">
#version 300 es
precision highp float;
uniform vec2 u_resolution; uniform float u_time;
out vec4 fragColor;
float sdfSphere(vec3 p, float s) { return length(p) - s; }
float sdfCube(vec3 p, float s) {
vec3 d = abs(p) - vec3(s);
return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0));
}
float smoothMin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
float map(vec3 p) {
vec3 p1 = p; vec3 p2 = p;
p1.x += sin(u_time * 1.85) * 1.3;
p2.y += cos(u_time * 1.33) * 1.2;
float d1 = sdfSphere(p1 - vec3(-1.0, 0.0, 0.0), 1.0);
float d2 = sdfSphere(p2 - vec3(1.0, 0.0, 0.0), 1.3);
float d3 = sdfCube(p - vec3(0.0, -1.2, -0.2), 0.7);
return smoothMin(smoothMin(d1, d2, 0.5), d3, 0.8);
}
vec2 trace(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < 40; i++) {
float d = map(ro + rd * t);
if (d < 0.0001) { return vec2(t, 1.0); } // hit
t += d;
if (t > 5.0) break;
}
return vec2(0.0, 0.0); // miss
}
vec3 normal(vec3 p) {
return normalize(vec3(
map(p + vec3(0.001, 0.0, 0.0)) - map(p - vec3(0.001, 0.0, 0.0)),
map(p + vec3(0.0, 0.001, 0.0)) - map(p - vec3(0.0, 0.001, 0.0)),
map(p + vec3(0.0, 0.0, 0.001)) - map(p - vec3(0.0, 0.0, 0.001))
));
}
void main() {
const vec3 sphereCol = vec3(1.0, 0.45, 0.3);
vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution.xy) / u_resolution.y;
vec3 lightDir = normalize(vec3(-2.0, 3.0, 1.5));
vec3 col = vec3(0.2, 0.22, 0.3);
vec3 ro = vec3(0.0, 0.0, 5.0);
vec3 rd = normalize(vec3(uv, -1.8));
vec2 hit = trace(ro, rd);
if(hit.y != 0.0) {
vec3 n = normal(ro + rd * hit.x);
float diff = max(dot(lightDir, n), 0.0);
float spec = pow(max(dot(reflect(-lightDir, n), -rd), 0.0), 26.0);
col = sphereCol * (diff + spec*1.8);
}
fragColor = vec4(col, 1.0);
}
</script>
<script type="module">
alert(
"View source to see the code! A GPU ray marcher in 100 lines of code."
);
import * as twgl from "https://esm.sh/twgl.js";
const gl = document.querySelector("canvas").getContext("webgl2");
const progInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
gl.useProgram(progInfo.program);
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: [-1, -1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0],
});
twgl.setBuffersAndAttributes(gl, progInfo, bufferInfo);
function render(time) {
twgl.setUniforms(progInfo, {
u_time: time * 0.001,
u_resolution: [gl.canvas.width, gl.canvas.height],
});
twgl.drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
import * as twgl from 'https://esm.sh/twgl.js'
const canvas = document.querySelector('canvas')
const gl = canvas.getContext('webgl2')
const vertShader = `
#version 300 es
precision highp float;
in vec2 position;
out vec2 uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0); // Set the position of the vertex
uv = position.xy * 0.5 + 0.5; // Convert from [-1, 1] to [0, 1]
}
`
// Shader that outputs pretty coloured grid
const frag1Shader = `
#version 300 es
precision highp float;
uniform vec2 window_size; // Size of the window
in vec2 uv;
out vec4 color;
const float scale = 20.0; // Scale factor for the grid
float rand(vec2 co) {
// Mad bullshit that acts like a random function
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
vec2 pixel_pos = uv * window_size; // Convert UV to pixel coordinates
vec2 grid_pos = floor(pixel_pos / scale); // Create a grid every 10 pixels
float r = rand(grid_pos + 0.0); // Random value for red
float g = rand(grid_pos + 37.0); // Random value for red
float b = rand(grid_pos + 123.0); // Random value for red
if (r > 0.5) {
r = 1.0; // Set red to 1 if condition is met
} else {
r = 0.0; // Set red to 0 otherwise
}
color = vec4(r, g, b, 1.0); // Output the color
}`
// Shader that renders the input texture to the screen
// with a slight adjustment to the red channel for visibility
const frag2Shader = `
#version 300 es
precision highp float;
in vec2 uv;
uniform sampler2D image;
out vec4 color;
void main() {
color = texture(image, uv);
color.r = color.r * 0.2 + 0.8; // Adjust red channel for visibility
}
`
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
const quadBuffers = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1],
},
})
const prog1 = twgl.createProgramInfo(gl, [vertShader, frag1Shader])
const prog2 = twgl.createProgramInfo(gl, [vertShader, frag2Shader])
// Create a framebuffer to render the first pass into
const framebuffer = twgl.createFramebufferInfo(gl, undefined, gl.canvas.width, gl.canvas.height)
// ==== Pass 1: Render the grid ====
gl.useProgram(prog1.program)
twgl.setBuffersAndAttributes(gl, prog1, quadBuffers)
twgl.setUniforms(prog1, {
window_size: [gl.canvas.width, gl.canvas.height],
})
// This tells WebGL to render to the framebuffer instead of the screen
twgl.bindFramebufferInfo(gl, framebuffer)
twgl.drawBufferInfo(gl, quadBuffers, gl.TRIANGLES)
// ==== Pass 2: Render the framebuffer to the screen ====
gl.useProgram(prog2.program)
twgl.setBuffersAndAttributes(gl, prog2, quadBuffers)
twgl.setUniforms(prog2, {
// This is the main trick: we use the texture from the framebuffer
image: framebuffer.attachments[0],
})
// This tells WebGL to render to the screen
twgl.bindFramebufferInfo(gl, null)
twgl.drawBufferInfo(gl, quadBuffers, gl.TRIANGLES)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment