{"slug":"teapot","number":"17","title":"Utah teapot","era":"Newell · 1975","summary":"A Gouraud teapot. Move the pointer to drag the lights.","description":"Martin Newell's teapot, lathed and swept rather than the original 32 patches, Gouraud/Phong shaded. Two point lights sit in the scene as little orbs. Move the pointer: the key light follows, the fill stays opposite. Ceramic, lamp, and fill colors are on the tweak row.","apis":["WebGL 1","GLSL ES 1.0","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["webgl","3d"],"hasGlsl":true,"interaction":"Move the pointer to drag the two lights around the teapot.","url":"https://3d-retro.com/experiments/teapot","example_url":"https://3d-retro.com/examples/teapot.html","api_url":"https://3d-retro.com/api/v1/experiments/teapot","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=\"Gouraud Utah-style teapot. Move the pointer to drag the lights.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/teapot\" />\n  <title>Utah teapot · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #0b0c0e; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }\n    .fallback { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace; text-align: center; color: #e8e6e1; background: #0b0c0e; }\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    color: \"#d8c4a8\",\n    light: \"#fff1c8\",\n    fill: \"#6ee7b7\"\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [0.85, 0.77, 0.66];\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  const gl = canvas.getContext(\"webgl\", { alpha: false, antialias: true });\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 vec3 a_pos;\nattribute vec3 a_nrm;\nuniform mat4 u_mvp;\nuniform mat4 u_model;\nvarying vec3 v_nrm;\nvarying vec3 v_pos;\nvoid main() {\n  vec4 world = u_model * vec4(a_pos, 1.0);\n  v_pos = world.xyz;\n  v_nrm = mat3(u_model) * a_nrm;\n  gl_Position = u_mvp * vec4(a_pos, 1.0);\n}`;\n  const FRAG = `precision mediump float;\nvarying vec3 v_nrm;\nvarying vec3 v_pos;\nuniform vec3 u_cam;\nuniform vec3 u_color;\nuniform vec3 u_lightA;\nuniform vec3 u_lightB;\nuniform vec3 u_colA;\nuniform vec3 u_colB;\nvoid main() {\n  vec3 n = normalize(v_nrm);\n  vec3 v = normalize(u_cam - v_pos);\n  vec3 la = normalize(u_lightA - v_pos);\n  vec3 lb = normalize(u_lightB - v_pos);\n  float d1 = max(dot(n, la), 0.0);\n  float d2 = max(dot(n, lb), 0.0);\n  float s1 = pow(max(dot(reflect(-la, n), v), 0.0), 48.0);\n  float s2 = pow(max(dot(reflect(-lb, n), v), 0.0), 24.0);\n  vec3 col = u_color * (0.12 + 0.72 * d1 + 0.38 * d2);\n  col += u_colA * s1 * 0.85;\n  col += u_colB * s2 * 0.45;\n  gl_FragColor = vec4(col, 1.0);\n}`;\n  const BALL_FRAG = `precision mediump float;\nuniform vec3 u_colA;\nvoid main() { gl_FragColor = vec4(u_colA, 1.0); }`;\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)) throw new Error(gl.getShaderInfoLog(sh) || \"shader\");\n    return sh;\n  }\n  function program(vs, fs) {\n    const p = gl.createProgram();\n    gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));\n    gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));\n    gl.linkProgram(p);\n    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || \"link\");\n    return p;\n  }\n  function mul(a, b) {\n    const o = new Float32Array(16);\n    for (let c = 0; c < 4; c++) {\n      for (let r = 0; r < 4; r++) {\n        o[c * 4 + r] = a[r]*b[c*4] + a[4+r]*b[c*4+1] + a[8+r]*b[c*4+2] + a[12+r]*b[c*4+3];\n      }\n    }\n    return o;\n  }\n  function perspective(fovy, aspect, near, far) {\n    const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);\n    const o = new Float32Array(16);\n    o[0] = f / aspect; o[5] = f; o[10] = (far + near) * nf; o[11] = -1; o[14] = 2 * far * near * nf;\n    return o;\n  }\n  function translate(x, y, z) {\n    const o = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);\n    o[12] = x; o[13] = y; o[14] = z;\n    return o;\n  }\n  function rotateY(a) {\n    const c = Math.cos(a), s = Math.sin(a);\n    return new Float32Array([c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1]);\n  }\n\n  const pos = [];\n  const nrm = [];\n  function addTri(a, b, c, na, nb, nc) {\n    pos.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);\n    nrm.push(na[0], na[1], na[2], nb[0], nb[1], nb[2], nc[0], nc[1], nc[2]);\n  }\n  function bez(p0, p1, p2, p3, t) {\n    const s = 1 - t;\n    return [\n      s*s*s*p0[0] + 3*s*s*t*p1[0] + 3*s*t*t*p2[0] + t*t*t*p3[0],\n      s*s*s*p0[1] + 3*s*s*t*p1[1] + 3*s*t*t*p2[1] + t*t*t*p3[1]\n    ];\n  }\n  function lathe(curves, nu, nv) {\n    const rings = [];\n    for (let c = 0; c < curves.length; c++) {\n      const cr = curves[c];\n      for (let j = 0; j < nv; j++) {\n        const t = j / (nv - (c === curves.length - 1 ? 1 : 0));\n        if (c < curves.length - 1 && j === nv && false) break;\n        rings.push(bez(cr[0], cr[1], cr[2], cr[3], Math.min(1, t)));\n      }\n    }\n    const seen = [];\n    for (let i = 0; i < rings.length; i++) {\n      if (i && Math.hypot(rings[i][0] - rings[i-1][0], rings[i][1] - rings[i-1][1]) < 0.002) continue;\n      seen.push(rings[i]);\n    }\n    for (let i = 0; i < seen.length - 1; i++) {\n      const r0 = seen[i][0], y0 = seen[i][1];\n      const r1 = seen[i+1][0], y1 = seen[i+1][1];\n      const dr = r1 - r0, dy = y1 - y0;\n      const nr = dy, ny = -dr;\n      const nl = Math.hypot(nr, ny) || 1;\n      for (let u = 0; u < nu; u++) {\n        const a0 = (u / nu) * Math.PI * 2, a1 = ((u + 1) / nu) * Math.PI * 2;\n        const c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);\n        const p00 = [r0 * c0, y0, r0 * s0], p10 = [r1 * c0, y1, r1 * s0];\n        const p11 = [r1 * c1, y1, r1 * s1], p01 = [r0 * c1, y0, r0 * s1];\n        const n0 = [nr * c0 / nl, ny / nl, nr * s0 / nl];\n        const n1 = [nr * c1 / nl, ny / nl, nr * s1 / nl];\n        addTri(p00, p10, p11, n0, n0, n1);\n        addTri(p00, p11, p01, n0, n1, n1);\n      }\n    }\n  }\n  function tube(path, radii, nu) {\n    let nx = 0, ny = 0, nz = 1;\n    const frames = [];\n    for (let i = 0; i < path.length; i++) {\n      const p = path[i];\n      const p1 = path[Math.min(path.length - 1, i + 1)];\n      const p0 = path[Math.max(0, i - 1)];\n      let tx = p1[0] - p0[0], ty = p1[1] - p0[1], tz = p1[2] - p0[2];\n      const tl = Math.hypot(tx, ty, tz) || 1;\n      tx /= tl; ty /= tl; tz /= tl;\n      let px = nx - tx * (nx * tx + ny * ty + nz * tz);\n      let py = ny - ty * (nx * tx + ny * ty + nz * tz);\n      let pz = nz - tz * (nx * tx + ny * ty + nz * tz);\n      let pl = Math.hypot(px, py, pz);\n      if (pl < 0.2) {\n        px = ty * 0 - 1 * tz; py = tz * 1 - tx * 0; pz = tx * 0 - ty * 1;\n        pl = Math.hypot(px, py, pz) || 1;\n      }\n      px /= pl; py /= pl; pz /= pl;\n      nx = px; ny = py; nz = pz;\n      const bx = ty * pz - tz * py, by = tz * px - tx * pz, bz = tx * py - ty * px;\n      frames.push({ p: p, n: [px, py, pz], b: [bx, by, bz], r: radii[i] });\n    }\n    function ringPt(fr, u) {\n      const a = (u / nu) * Math.PI * 2, ca = Math.cos(a), sa = Math.sin(a);\n      return [\n        fr.p[0] + (fr.n[0] * ca + fr.b[0] * sa) * fr.r,\n        fr.p[1] + (fr.n[1] * ca + fr.b[1] * sa) * fr.r,\n        fr.p[2] + (fr.n[2] * ca + fr.b[2] * sa) * fr.r\n      ];\n    }\n    function ringN(fr, u) {\n      const a = (u / nu) * Math.PI * 2, ca = Math.cos(a), sa = Math.sin(a);\n      return [\n        fr.n[0] * ca + fr.b[0] * sa,\n        fr.n[1] * ca + fr.b[1] * sa,\n        fr.n[2] * ca + fr.b[2] * sa\n      ];\n    }\n    for (let i = 0; i < frames.length - 1; i++) {\n      for (let u = 0; u < nu; u++) {\n        const a = ringPt(frames[i], u), b = ringPt(frames[i + 1], u);\n        const c = ringPt(frames[i + 1], u + 1), d = ringPt(frames[i], u + 1);\n        const na = ringN(frames[i], u), nb = ringN(frames[i + 1], u);\n        const nc = ringN(frames[i + 1], u + 1), nd = ringN(frames[i], u + 1);\n        addTri(a, b, c, na, nb, nc);\n        addTri(a, c, d, na, nc, nd);\n      }\n    }\n  }\n  function sphere(cx, cy, cz, r, segs) {\n    for (let i = 0; i < segs; i++) {\n      const a0 = (i / segs) * Math.PI, a1 = ((i + 1) / segs) * Math.PI;\n      for (let j = 0; j < segs * 2; j++) {\n        const b0 = (j / (segs * 2)) * Math.PI * 2, b1 = ((j + 1) / (segs * 2)) * Math.PI * 2;\n        function pt(a, b) {\n          const x = Math.sin(a) * Math.cos(b), y = Math.cos(a), z = Math.sin(a) * Math.sin(b);\n          return [[cx + x * r, cy + y * r, cz + z * r], [x, y, z]];\n        }\n        const p00 = pt(a0, b0), p10 = pt(a1, b0), p11 = pt(a1, b1), p01 = pt(a0, b1);\n        addTri(p00[0], p10[0], p11[0], p00[1], p10[1], p11[1]);\n        addTri(p00[0], p11[0], p01[0], p00[1], p11[1], p01[1]);\n      }\n    }\n  }\n\n  lathe([\n    [[0.05, 0.0], [0.55, 0.0], [1.15, 0.08], [1.32, 0.38]],\n    [[1.32, 0.38], [1.42, 0.7], [1.28, 1.05], [0.95, 1.28]],\n    [[0.95, 1.28], [0.78, 1.38], [0.7, 1.42], [0.62, 1.46]]\n  ], 28, 7);\n  lathe([\n    [[0.62, 1.46], [0.55, 1.5], [0.38, 1.55], [0.2, 1.6]],\n    [[0.2, 1.6], [0.14, 1.64], [0.1, 1.7], [0.0, 1.74]]\n  ], 24, 6);\n  sphere(0, 1.82, 0, 0.12, 8);\n\n  function sampleBez3(a, b, c, d, n) {\n    const out = [];\n    for (let i = 0; i <= n; i++) {\n      const t = i / n, s = 1 - t;\n      out.push([\n        s*s*s*a[0]+3*s*s*t*b[0]+3*s*t*t*c[0]+t*t*t*d[0],\n        s*s*s*a[1]+3*s*s*t*b[1]+3*s*t*t*c[1]+t*t*t*d[1],\n        s*s*s*a[2]+3*s*s*t*b[2]+3*s*t*t*c[2]+t*t*t*d[2]\n      ]);\n    }\n    return out;\n  }\n  const handle = sampleBez3([1.12, 1.18, 0], [1.78, 1.38, 0], [1.82, 0.38, 0], [1.15, 0.36, 0], 14);\n  const hr = handle.map(function (_, i) { return 0.13; });\n  tube(handle, hr, 12);\n  const spout = sampleBez3([-1.05, 0.82, 0], [-1.65, 0.92, 0], [-1.95, 1.28, 0], [-2.05, 1.55, 0], 12);\n  const sr = spout.map(function (_, i) { return 0.2 - 0.11 * (i / (spout.length - 1)); });\n  tube(spout, sr, 10);\n\n  const meshPos = new Float32Array(pos);\n  const meshNrm = new Float32Array(nrm);\n  const meshCount = pos.length / 3;\n\n  const ballPos = [];\n  const ballNrm = [];\n  const savedPos = pos.length, savedNrm = nrm.length;\n  pos.length = 0; nrm.length = 0;\n  sphere(0, 0, 0, 1, 8);\n  ballPos.push.apply(ballPos, pos);\n  ballNrm.push.apply(ballNrm, nrm);\n  pos.length = savedPos; nrm.length = savedNrm;\n\n  const meshProg = program(VERT, FRAG);\n  const ballProg = program(VERT, BALL_FRAG);\n  function makeBuf(data) {\n    const b = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, b);\n    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n    return b;\n  }\n  const posBuf = makeBuf(meshPos);\n  const nrmBuf = makeBuf(meshNrm);\n  const ballP = makeBuf(new Float32Array(ballPos));\n  const ballN = makeBuf(new Float32Array(ballNrm));\n  const ballCount = ballPos.length / 3;\n\n  function bind(prog, name, buf, size) {\n    const loc = gl.getAttribLocation(prog, name);\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.enableVertexAttribArray(loc);\n    gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);\n  }\n\n  gl.enable(gl.DEPTH_TEST);\n  gl.enable(gl.CULL_FACE);\n  gl.clearColor(0.043, 0.047, 0.055, 1);\n\n  let az = 0.55, el = 0.7, yaw = 0.95, last = 0;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n  canvas.addEventListener(\"pointermove\", function (e) {\n    const r = canvas.getBoundingClientRect();\n    az = ((e.clientX - r.left) / r.width - 0.5) * Math.PI * 1.4;\n    el = 0.25 + (1 - (e.clientY - r.top) / r.height) * 1.15;\n  });\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\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 lightPos(a, e, rad) {\n    return [Math.cos(e) * Math.sin(a) * rad, Math.sin(e) * rad, Math.cos(e) * Math.cos(a) * rad];\n  }\n\n  function frame(now) {\n    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);\n    last = now;\n    if (!reduced) yaw += dt * 0.35;\n    resize();\n    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n    const aspect = canvas.width / canvas.height;\n    const proj = perspective(Math.PI / 4.2, aspect, 0.1, 40);\n    const view = translate(0, -0.85, -5.4);\n    const model = rotateY(yaw);\n    const mvp = mul(mul(proj, view), model);\n    const L1 = lightPos(az, el, 3.4);\n    const L2 = lightPos(az + 2.3, el * 0.55 + 0.2, 3.2);\n    const cam = [0, 1.4, 5.4];\n\n    gl.useProgram(meshProg);\n    gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, \"u_mvp\"), false, mvp);\n    gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, \"u_model\"), false, model);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_cam\"), cam);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_color\"), hexToRgb(PARAMS.color));\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_lightA\"), L1);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_lightB\"), L2);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_colA\"), hexToRgb(PARAMS.light));\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_colB\"), hexToRgb(PARAMS.fill));\n    bind(meshProg, \"a_pos\", posBuf, 3);\n    bind(meshProg, \"a_nrm\", nrmBuf, 3);\n    gl.drawArrays(gl.TRIANGLES, 0, meshCount);\n\n    function drawBall(L, col) {\n      const s = 0.09;\n      const m = mul(translate(L[0], L[1], L[2]), new Float32Array([s,0,0,0, 0,s,0,0, 0,0,s,0, 0,0,0,1]));\n      const bmvp = mul(mul(proj, view), m);\n      gl.useProgram(ballProg);\n      gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, \"u_mvp\"), false, bmvp);\n      gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, \"u_model\"), false, m);\n      gl.uniform3fv(gl.getUniformLocation(ballProg, \"u_colA\"), col);\n      bind(ballProg, \"a_pos\", ballP, 3);\n      bind(ballProg, \"a_nrm\", ballN, 3);\n      gl.drawArrays(gl.TRIANGLES, 0, ballCount);\n    }\n    drawBall(L1, hexToRgb(PARAMS.light));\n    drawBall(L2, hexToRgb(PARAMS.fill));\n\n    if (!reduced) requestAnimationFrame(frame);\n  }\n\n  canvas.addEventListener(\"webglcontextlost\", function (e) {\n    e.preventDefault();\n    fail(\"WebGL context was lost. Reload the page.\");\n  });\n\n  if (reduced) { resize(); last = 0; frame(0); }\n  else requestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>\n"}