Home

05. Whitted · 1980

Reflective spheres

Colors and knobs

These rewrite the live file. Copy HTML picks up whatever you set.

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));
  }
})();