05. Whitted · 1980
Reflective spheres
About
Each pixel shoots a primary ray, intersects a plane and three spheres, then follows reflections for two extra bounces. Shadows are shadow rays toward a point light. The chrome ball is almost pure metal. The red and mint balls mix diffuse with a little reflection. The whole scene is one GLSL fragment shader.
Browser APIs
- WebGL 1
- GLSL ES 1.0
- requestAnimationFrame
- matchMedia
If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.
Source
(function () {
const PARAMS = {
metal: "#f0f0f0",
red: "#bd291f",
mint: "#51d19e",
speed: 1
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [0.94, 0.94, 0.94];
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
}
const canvas = document.getElementById("c");
const fallback = document.getElementById("fallback");
function fail(message) {
fallback.hidden = false;
fallback.textContent = message;
}
const gl = canvas.getContext("webgl", { alpha: false, antialias: false });
if (!gl) {
fail("WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.");
return;
}
const VERT = `attribute vec2 a_pos;
void main() {
gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
const FRAG = `#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
uniform vec3 u_metal;
uniform vec3 u_red;
uniform vec3 u_mint;
uniform float u_speed;
float sph(vec3 ro, vec3 rd, vec3 ce, float ra) {
vec3 oc = ro - ce;
float b = dot(oc, rd);
float c = dot(oc, oc) - ra * ra;
float h = b * b - c;
if (h < 0.0) return -1.0;
return -b - sqrt(h);
}
vec3 checker(vec3 p) {
float g = mod(floor(p.x) + floor(p.z), 2.0);
return mix(vec3(0.12, 0.125, 0.13), vec3(0.76, 0.74, 0.68), g);
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;
float t = u_time * 0.22 * u_speed;
float ct = cos(t);
float st = sin(t);
vec3 ta = vec3(0.0, 0.42, 0.0);
vec3 ro = vec3(3.55 * st, 1.28, 3.55 * ct);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0.0, 1.0, 0.0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.65 * ww);
vec3 light = vec3(2.6, 5.2, 1.4);
vec3 col = vec3(0.0);
vec3 att = vec3(1.0);
vec3 sky = mix(vec3(0.58, 0.66, 0.72), vec3(0.04, 0.045, 0.055), clamp(rd.y * 0.9 + 0.35, 0.0, 1.0));
vec3 s1 = vec3(0.0, 0.58, 0.0);
vec3 s2 = vec3(-1.18, 0.38, 0.18);
vec3 s3 = vec3(1.12, 0.34, 0.48);
for (int bounce = 0; bounce < 3; bounce++) {
float tPlane = 1e5;
if (abs(rd.y) > 0.0001) {
float tp = -ro.y / rd.y;
if (tp > 0.002) tPlane = tp;
}
float t1 = sph(ro, rd, s1, 0.58);
float t2 = sph(ro, rd, s2, 0.38);
float t3 = sph(ro, rd, s3, 0.34);
if (t1 < 0.0) t1 = 1e5;
if (t2 < 0.0) t2 = 1e5;
if (t3 < 0.0) t3 = 1e5;
float tHit = tPlane;
int id = 0;
if (t1 < tHit) { tHit = t1; id = 1; }
if (t2 < tHit) { tHit = t2; id = 2; }
if (t3 < tHit) { tHit = t3; id = 3; }
if (tHit > 1e4) {
col += att * sky;
break;
}
vec3 p = ro + rd * tHit;
vec3 n;
vec3 albedo;
float metal;
if (id == 0) {
n = vec3(0.0, 1.0, 0.0);
albedo = checker(p);
metal = 0.16;
} else if (id == 1) {
n = normalize(p - s1);
albedo = u_metal;
metal = 0.96;
} else if (id == 2) {
n = normalize(p - s2);
albedo = u_red;
metal = 0.32;
} else {
n = normalize(p - s3);
albedo = u_mint;
metal = 0.42;
}
vec3 ldir = normalize(light - p);
float ldist = length(light - p);
vec3 sro = p + n * 0.003;
float sl = 1.0;
float h1 = sph(sro, ldir, s1, 0.58);
float h2 = sph(sro, ldir, s2, 0.38);
float h3 = sph(sro, ldir, s3, 0.34);
if ((h1 > 0.0 && h1 < ldist) || (h2 > 0.0 && h2 < ldist) || (h3 > 0.0 && h3 < ldist)) sl = 0.16;
float diff = max(dot(n, ldir), 0.0);
vec3 halfv = normalize(ldir - rd);
float spec = pow(max(dot(n, halfv), 0.0), 52.0);
vec3 direct = albedo * (0.07 + 0.92 * diff * sl) + vec3(1.0) * spec * sl * mix(0.12, 0.85, metal);
col += att * direct * (1.0 - metal * 0.82);
att *= mix(albedo * 0.22, vec3(0.86), metal);
ro = p + n * 0.003;
rd = reflect(rd, n);
}
col = pow(max(col, 0.0), vec3(0.92));
gl_FragColor = vec4(col, 1.0);
}`;
function compile(type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(sh) || "Shader compile failed");
}
return sh;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(prog) || "Program link failed");
}
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, "a_pos");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(prog, "u_time");
const uRes = gl.getUniformLocation(prog, "u_resolution");
const uMetal = gl.getUniformLocation(prog, "u_metal");
const uRed = gl.getUniformLocation(prog, "u_red");
const uMint = gl.getUniformLocation(prog, "u_mint");
const uSpeed = gl.getUniformLocation(prog, "u_speed");
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let raf = 0;
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
function frame(now) {
resize();
gl.uniform1f(uTime, reduced ? 2.8 : now * 0.001);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform3fv(uMetal, hexToRgb(PARAMS.metal));
gl.uniform3fv(uRed, hexToRgb(PARAMS.red));
gl.uniform3fv(uMint, hexToRgb(PARAMS.mint));
gl.uniform1f(uSpeed, PARAMS.speed);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (!reduced) raf = requestAnimationFrame(frame);
}
canvas.addEventListener("webglcontextlost", function (e) {
e.preventDefault();
cancelAnimationFrame(raf);
fail("The WebGL context was lost. Reload to restore the demo.");
});
try {
raf = requestAnimationFrame(frame);
} catch (err) {
fail(err && err.message ? err.message : String(err));
}
})();