{"slug":"raytracer","number":"05","title":"Reflective spheres","era":"Whitted · 1980","summary":"A Whitted raytracer: three spheres, checkerboard, shadows, three bounces.","description":"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.","apis":["WebGL 1","GLSL ES 1.0","requestAnimationFrame","matchMedia"],"tags":["shader","raytrace"],"hasGlsl":true,"interaction":null,"url":"https://3d-retro.com/experiments/raytracer","example_url":"https://3d-retro.com/examples/raytracer.html","api_url":"https://3d-retro.com/api/v1/experiments/raytracer","html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"description\" content=\"A Whitted raytracer: three spheres, checkerboard, shadows, three bounces.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/raytracer\" />\n  <title>Reflective spheres · 3d retro graphics</title>\n  <style>\n    html, body {\n      margin: 0;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n      background: #0b0c0e;\n      color: #e8e6e1;\n      position: absolute;\n      inset: 0;\n    }\n    canvas {\n      display: block;\n      position: absolute;\n      inset: 0;\n      width: 100%;\n      height: 100%;\n    }\n    .fallback {\n      position: absolute;\n      inset: 0;\n      display: grid;\n      place-items: center;\n      padding: 24px;\n      font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace;\n      text-align: center;\n      color: #e8e6e1;\n      background: #0b0c0e;\n    }\n    .fallback[hidden] { display: none; }\n  </style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"fallback\" class=\"fallback\" hidden></div>\n<script>\n(function () {\n  const PARAMS = {\n    metal: \"#f0f0f0\",\n    red: \"#bd291f\",\n    mint: \"#51d19e\",\n    speed: 1\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [0.94, 0.94, 0.94];\n    return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  function fail(message) {\n    fallback.hidden = false;\n    fallback.textContent = message;\n  }\n\n  const gl = canvas.getContext(\"webgl\", { alpha: false, antialias: false });\n  if (!gl) {\n    fail(\"WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.\");\n    return;\n  }\n\n  const VERT = `attribute vec2 a_pos;\nvoid main() {\n  gl_Position = vec4(a_pos, 0.0, 1.0);\n}`;\n\n  const FRAG = `#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec3 u_metal;\nuniform vec3 u_red;\nuniform vec3 u_mint;\nuniform float u_speed;\n\nfloat sph(vec3 ro, vec3 rd, vec3 ce, float ra) {\n  vec3 oc = ro - ce;\n  float b = dot(oc, rd);\n  float c = dot(oc, oc) - ra * ra;\n  float h = b * b - c;\n  if (h < 0.0) return -1.0;\n  return -b - sqrt(h);\n}\n\nvec3 checker(vec3 p) {\n  float g = mod(floor(p.x) + floor(p.z), 2.0);\n  return mix(vec3(0.12, 0.125, 0.13), vec3(0.76, 0.74, 0.68), g);\n}\n\nvoid main() {\n  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;\n  float t = u_time * 0.22 * u_speed;\n  float ct = cos(t);\n  float st = sin(t);\n  vec3 ta = vec3(0.0, 0.42, 0.0);\n  vec3 ro = vec3(3.55 * st, 1.28, 3.55 * ct);\n  vec3 ww = normalize(ta - ro);\n  vec3 uu = normalize(cross(vec3(0.0, 1.0, 0.0), ww));\n  vec3 vv = cross(ww, uu);\n  vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.65 * ww);\n\n  vec3 light = vec3(2.6, 5.2, 1.4);\n  vec3 col = vec3(0.0);\n  vec3 att = vec3(1.0);\n  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));\n\n  vec3 s1 = vec3(0.0, 0.58, 0.0);\n  vec3 s2 = vec3(-1.18, 0.38, 0.18);\n  vec3 s3 = vec3(1.12, 0.34, 0.48);\n\n  for (int bounce = 0; bounce < 3; bounce++) {\n    float tPlane = 1e5;\n    if (abs(rd.y) > 0.0001) {\n      float tp = -ro.y / rd.y;\n      if (tp > 0.002) tPlane = tp;\n    }\n    float t1 = sph(ro, rd, s1, 0.58);\n    float t2 = sph(ro, rd, s2, 0.38);\n    float t3 = sph(ro, rd, s3, 0.34);\n    if (t1 < 0.0) t1 = 1e5;\n    if (t2 < 0.0) t2 = 1e5;\n    if (t3 < 0.0) t3 = 1e5;\n\n    float tHit = tPlane;\n    int id = 0;\n    if (t1 < tHit) { tHit = t1; id = 1; }\n    if (t2 < tHit) { tHit = t2; id = 2; }\n    if (t3 < tHit) { tHit = t3; id = 3; }\n\n    if (tHit > 1e4) {\n      col += att * sky;\n      break;\n    }\n\n    vec3 p = ro + rd * tHit;\n    vec3 n;\n    vec3 albedo;\n    float metal;\n    if (id == 0) {\n      n = vec3(0.0, 1.0, 0.0);\n      albedo = checker(p);\n      metal = 0.16;\n    } else if (id == 1) {\n      n = normalize(p - s1);\n      albedo = u_metal;\n      metal = 0.96;\n    } else if (id == 2) {\n      n = normalize(p - s2);\n      albedo = u_red;\n      metal = 0.32;\n    } else {\n      n = normalize(p - s3);\n      albedo = u_mint;\n      metal = 0.42;\n    }\n\n    vec3 ldir = normalize(light - p);\n    float ldist = length(light - p);\n    vec3 sro = p + n * 0.003;\n    float sl = 1.0;\n    float h1 = sph(sro, ldir, s1, 0.58);\n    float h2 = sph(sro, ldir, s2, 0.38);\n    float h3 = sph(sro, ldir, s3, 0.34);\n    if ((h1 > 0.0 && h1 < ldist) || (h2 > 0.0 && h2 < ldist) || (h3 > 0.0 && h3 < ldist)) sl = 0.16;\n\n    float diff = max(dot(n, ldir), 0.0);\n    vec3 halfv = normalize(ldir - rd);\n    float spec = pow(max(dot(n, halfv), 0.0), 52.0);\n    vec3 direct = albedo * (0.07 + 0.92 * diff * sl) + vec3(1.0) * spec * sl * mix(0.12, 0.85, metal);\n    col += att * direct * (1.0 - metal * 0.82);\n\n    att *= mix(albedo * 0.22, vec3(0.86), metal);\n    ro = p + n * 0.003;\n    rd = reflect(rd, n);\n  }\n\n  col = pow(max(col, 0.0), vec3(0.92));\n  gl_FragColor = vec4(col, 1.0);\n}`;\n\n  function compile(type, src) {\n    const sh = gl.createShader(type);\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {\n      throw new Error(gl.getShaderInfoLog(sh) || \"Shader compile failed\");\n    }\n    return sh;\n  }\n\n  const prog = gl.createProgram();\n  gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));\n  gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));\n  gl.linkProgram(prog);\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n    throw new Error(gl.getProgramInfoLog(prog) || \"Program link failed\");\n  }\n  gl.useProgram(prog);\n\n  const buf = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);\n  const loc = gl.getAttribLocation(prog, \"a_pos\");\n  gl.enableVertexAttribArray(loc);\n  gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n\n  const uTime = gl.getUniformLocation(prog, \"u_time\");\n  const uRes = gl.getUniformLocation(prog, \"u_resolution\");\n  const uMetal = gl.getUniformLocation(prog, \"u_metal\");\n  const uRed = gl.getUniformLocation(prog, \"u_red\");\n  const uMint = gl.getUniformLocation(prog, \"u_mint\");\n  const uSpeed = gl.getUniformLocation(prog, \"u_speed\");\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  let raf = 0;\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) {\n      canvas.width = w;\n      canvas.height = h;\n    }\n    gl.viewport(0, 0, canvas.width, canvas.height);\n  }\n\n  function frame(now) {\n    resize();\n    gl.uniform1f(uTime, reduced ? 2.8 : now * 0.001);\n    gl.uniform2f(uRes, canvas.width, canvas.height);\n    gl.uniform3fv(uMetal, hexToRgb(PARAMS.metal));\n    gl.uniform3fv(uRed, hexToRgb(PARAMS.red));\n    gl.uniform3fv(uMint, hexToRgb(PARAMS.mint));\n    gl.uniform1f(uSpeed, PARAMS.speed);\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n    if (!reduced) raf = requestAnimationFrame(frame);\n  }\n\n  canvas.addEventListener(\"webglcontextlost\", function (e) {\n    e.preventDefault();\n    cancelAnimationFrame(raf);\n    fail(\"The WebGL context was lost. Reload to restore the demo.\");\n  });\n\n  try {\n    raf = requestAnimationFrame(frame);\n  } catch (err) {\n    fail(err && err.message ? err.message : String(err));\n  }\n})();\n</script>\n</body>\n</html>\n"}