Last active
August 23, 2025 09:21
-
-
Save benc-uk/9edcefb9453f5b8cabc76ec0ae619252 to your computer and use it in GitHub Desktop.
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
<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> |
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
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