Home

17. Newell · 1975

Utah teapot

Colors and knobs

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

About

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.

Move the pointer to drag the two lights around the teapot.

Browser APIs

  • WebGL 1
  • GLSL ES 1.0
  • Pointer Events
  • 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 = {
    color: "#d8c4a8",
    light: "#fff1c8",
    fill: "#6ee7b7"
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [0.85, 0.77, 0.66];
    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: true });
  if (!gl) {
    fail("WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.");
    return;
  }

  const VERT = `attribute vec3 a_pos;
attribute vec3 a_nrm;
uniform mat4 u_mvp;
uniform mat4 u_model;
varying vec3 v_nrm;
varying vec3 v_pos;
void main() {
  vec4 world = u_model * vec4(a_pos, 1.0);
  v_pos = world.xyz;
  v_nrm = mat3(u_model) * a_nrm;
  gl_Position = u_mvp * vec4(a_pos, 1.0);
}`;
  const FRAG = `precision mediump float;
varying vec3 v_nrm;
varying vec3 v_pos;
uniform vec3 u_cam;
uniform vec3 u_color;
uniform vec3 u_lightA;
uniform vec3 u_lightB;
uniform vec3 u_colA;
uniform vec3 u_colB;
void main() {
  vec3 n = normalize(v_nrm);
  vec3 v = normalize(u_cam - v_pos);
  vec3 la = normalize(u_lightA - v_pos);
  vec3 lb = normalize(u_lightB - v_pos);
  float d1 = max(dot(n, la), 0.0);
  float d2 = max(dot(n, lb), 0.0);
  float s1 = pow(max(dot(reflect(-la, n), v), 0.0), 48.0);
  float s2 = pow(max(dot(reflect(-lb, n), v), 0.0), 24.0);
  vec3 col = u_color * (0.12 + 0.72 * d1 + 0.38 * d2);
  col += u_colA * s1 * 0.85;
  col += u_colB * s2 * 0.45;
  gl_FragColor = vec4(col, 1.0);
}`;
  const BALL_FRAG = `precision mediump float;
uniform vec3 u_colA;
void main() { gl_FragColor = vec4(u_colA, 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");
    return sh;
  }
  function program(vs, fs) {
    const p = gl.createProgram();
    gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));
    gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));
    gl.linkProgram(p);
    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || "link");
    return p;
  }
  function mul(a, b) {
    const o = new Float32Array(16);
    for (let c = 0; c < 4; c++) {
      for (let r = 0; r < 4; r++) {
        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];
      }
    }
    return o;
  }
  function perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
    const o = new Float32Array(16);
    o[0] = f / aspect; o[5] = f; o[10] = (far + near) * nf; o[11] = -1; o[14] = 2 * far * near * nf;
    return o;
  }
  function translate(x, y, z) {
    const o = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
    o[12] = x; o[13] = y; o[14] = z;
    return o;
  }
  function rotateY(a) {
    const c = Math.cos(a), s = Math.sin(a);
    return new Float32Array([c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1]);
  }

  const pos = [];
  const nrm = [];
  function addTri(a, b, c, na, nb, nc) {
    pos.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
    nrm.push(na[0], na[1], na[2], nb[0], nb[1], nb[2], nc[0], nc[1], nc[2]);
  }
  function bez(p0, p1, p2, p3, t) {
    const s = 1 - t;
    return [
      s*s*s*p0[0] + 3*s*s*t*p1[0] + 3*s*t*t*p2[0] + t*t*t*p3[0],
      s*s*s*p0[1] + 3*s*s*t*p1[1] + 3*s*t*t*p2[1] + t*t*t*p3[1]
    ];
  }
  function lathe(curves, nu, nv) {
    const rings = [];
    for (let c = 0; c < curves.length; c++) {
      const cr = curves[c];
      for (let j = 0; j < nv; j++) {
        const t = j / (nv - (c === curves.length - 1 ? 1 : 0));
        if (c < curves.length - 1 && j === nv && false) break;
        rings.push(bez(cr[0], cr[1], cr[2], cr[3], Math.min(1, t)));
      }
    }
    const seen = [];
    for (let i = 0; i < rings.length; i++) {
      if (i && Math.hypot(rings[i][0] - rings[i-1][0], rings[i][1] - rings[i-1][1]) < 0.002) continue;
      seen.push(rings[i]);
    }
    for (let i = 0; i < seen.length - 1; i++) {
      const r0 = seen[i][0], y0 = seen[i][1];
      const r1 = seen[i+1][0], y1 = seen[i+1][1];
      const dr = r1 - r0, dy = y1 - y0;
      const nr = dy, ny = -dr;
      const nl = Math.hypot(nr, ny) || 1;
      for (let u = 0; u < nu; u++) {
        const a0 = (u / nu) * Math.PI * 2, a1 = ((u + 1) / nu) * Math.PI * 2;
        const c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
        const p00 = [r0 * c0, y0, r0 * s0], p10 = [r1 * c0, y1, r1 * s0];
        const p11 = [r1 * c1, y1, r1 * s1], p01 = [r0 * c1, y0, r0 * s1];
        const n0 = [nr * c0 / nl, ny / nl, nr * s0 / nl];
        const n1 = [nr * c1 / nl, ny / nl, nr * s1 / nl];
        addTri(p00, p10, p11, n0, n0, n1);
        addTri(p00, p11, p01, n0, n1, n1);
      }
    }
  }
  function tube(path, radii, nu) {
    let nx = 0, ny = 0, nz = 1;
    const frames = [];
    for (let i = 0; i < path.length; i++) {
      const p = path[i];
      const p1 = path[Math.min(path.length - 1, i + 1)];
      const p0 = path[Math.max(0, i - 1)];
      let tx = p1[0] - p0[0], ty = p1[1] - p0[1], tz = p1[2] - p0[2];
      const tl = Math.hypot(tx, ty, tz) || 1;
      tx /= tl; ty /= tl; tz /= tl;
      let px = nx - tx * (nx * tx + ny * ty + nz * tz);
      let py = ny - ty * (nx * tx + ny * ty + nz * tz);
      let pz = nz - tz * (nx * tx + ny * ty + nz * tz);
      let pl = Math.hypot(px, py, pz);
      if (pl < 0.2) {
        px = ty * 0 - 1 * tz; py = tz * 1 - tx * 0; pz = tx * 0 - ty * 1;
        pl = Math.hypot(px, py, pz) || 1;
      }
      px /= pl; py /= pl; pz /= pl;
      nx = px; ny = py; nz = pz;
      const bx = ty * pz - tz * py, by = tz * px - tx * pz, bz = tx * py - ty * px;
      frames.push({ p: p, n: [px, py, pz], b: [bx, by, bz], r: radii[i] });
    }
    function ringPt(fr, u) {
      const a = (u / nu) * Math.PI * 2, ca = Math.cos(a), sa = Math.sin(a);
      return [
        fr.p[0] + (fr.n[0] * ca + fr.b[0] * sa) * fr.r,
        fr.p[1] + (fr.n[1] * ca + fr.b[1] * sa) * fr.r,
        fr.p[2] + (fr.n[2] * ca + fr.b[2] * sa) * fr.r
      ];
    }
    function ringN(fr, u) {
      const a = (u / nu) * Math.PI * 2, ca = Math.cos(a), sa = Math.sin(a);
      return [
        fr.n[0] * ca + fr.b[0] * sa,
        fr.n[1] * ca + fr.b[1] * sa,
        fr.n[2] * ca + fr.b[2] * sa
      ];
    }
    for (let i = 0; i < frames.length - 1; i++) {
      for (let u = 0; u < nu; u++) {
        const a = ringPt(frames[i], u), b = ringPt(frames[i + 1], u);
        const c = ringPt(frames[i + 1], u + 1), d = ringPt(frames[i], u + 1);
        const na = ringN(frames[i], u), nb = ringN(frames[i + 1], u);
        const nc = ringN(frames[i + 1], u + 1), nd = ringN(frames[i], u + 1);
        addTri(a, b, c, na, nb, nc);
        addTri(a, c, d, na, nc, nd);
      }
    }
  }
  function sphere(cx, cy, cz, r, segs) {
    for (let i = 0; i < segs; i++) {
      const a0 = (i / segs) * Math.PI, a1 = ((i + 1) / segs) * Math.PI;
      for (let j = 0; j < segs * 2; j++) {
        const b0 = (j / (segs * 2)) * Math.PI * 2, b1 = ((j + 1) / (segs * 2)) * Math.PI * 2;
        function pt(a, b) {
          const x = Math.sin(a) * Math.cos(b), y = Math.cos(a), z = Math.sin(a) * Math.sin(b);
          return [[cx + x * r, cy + y * r, cz + z * r], [x, y, z]];
        }
        const p00 = pt(a0, b0), p10 = pt(a1, b0), p11 = pt(a1, b1), p01 = pt(a0, b1);
        addTri(p00[0], p10[0], p11[0], p00[1], p10[1], p11[1]);
        addTri(p00[0], p11[0], p01[0], p00[1], p11[1], p01[1]);
      }
    }
  }

  lathe([
    [[0.05, 0.0], [0.55, 0.0], [1.15, 0.08], [1.32, 0.38]],
    [[1.32, 0.38], [1.42, 0.7], [1.28, 1.05], [0.95, 1.28]],
    [[0.95, 1.28], [0.78, 1.38], [0.7, 1.42], [0.62, 1.46]]
  ], 28, 7);
  lathe([
    [[0.62, 1.46], [0.55, 1.5], [0.38, 1.55], [0.2, 1.6]],
    [[0.2, 1.6], [0.14, 1.64], [0.1, 1.7], [0.0, 1.74]]
  ], 24, 6);
  sphere(0, 1.82, 0, 0.12, 8);

  function sampleBez3(a, b, c, d, n) {
    const out = [];
    for (let i = 0; i <= n; i++) {
      const t = i / n, s = 1 - t;
      out.push([
        s*s*s*a[0]+3*s*s*t*b[0]+3*s*t*t*c[0]+t*t*t*d[0],
        s*s*s*a[1]+3*s*s*t*b[1]+3*s*t*t*c[1]+t*t*t*d[1],
        s*s*s*a[2]+3*s*s*t*b[2]+3*s*t*t*c[2]+t*t*t*d[2]
      ]);
    }
    return out;
  }
  const handle = sampleBez3([1.12, 1.18, 0], [1.78, 1.38, 0], [1.82, 0.38, 0], [1.15, 0.36, 0], 14);
  const hr = handle.map(function (_, i) { return 0.13; });
  tube(handle, hr, 12);
  const spout = sampleBez3([-1.05, 0.82, 0], [-1.65, 0.92, 0], [-1.95, 1.28, 0], [-2.05, 1.55, 0], 12);
  const sr = spout.map(function (_, i) { return 0.2 - 0.11 * (i / (spout.length - 1)); });
  tube(spout, sr, 10);

  const meshPos = new Float32Array(pos);
  const meshNrm = new Float32Array(nrm);
  const meshCount = pos.length / 3;

  const ballPos = [];
  const ballNrm = [];
  const savedPos = pos.length, savedNrm = nrm.length;
  pos.length = 0; nrm.length = 0;
  sphere(0, 0, 0, 1, 8);
  ballPos.push.apply(ballPos, pos);
  ballNrm.push.apply(ballNrm, nrm);
  pos.length = savedPos; nrm.length = savedNrm;

  const meshProg = program(VERT, FRAG);
  const ballProg = program(VERT, BALL_FRAG);
  function makeBuf(data) {
    const b = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, b);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
    return b;
  }
  const posBuf = makeBuf(meshPos);
  const nrmBuf = makeBuf(meshNrm);
  const ballP = makeBuf(new Float32Array(ballPos));
  const ballN = makeBuf(new Float32Array(ballNrm));
  const ballCount = ballPos.length / 3;

  function bind(prog, name, buf, size) {
    const loc = gl.getAttribLocation(prog, name);
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);
  }

  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  gl.clearColor(0.043, 0.047, 0.055, 1);

  let az = 0.55, el = 0.7, yaw = 0.95, last = 0;
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  canvas.addEventListener("pointermove", function (e) {
    const r = canvas.getBoundingClientRect();
    az = ((e.clientX - r.left) / r.width - 0.5) * Math.PI * 1.4;
    el = 0.25 + (1 - (e.clientY - r.top) / r.height) * 1.15;
  });

  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    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 lightPos(a, e, rad) {
    return [Math.cos(e) * Math.sin(a) * rad, Math.sin(e) * rad, Math.cos(e) * Math.cos(a) * rad];
  }

  function frame(now) {
    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);
    last = now;
    if (!reduced) yaw += dt * 0.35;
    resize();
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    const aspect = canvas.width / canvas.height;
    const proj = perspective(Math.PI / 4.2, aspect, 0.1, 40);
    const view = translate(0, -0.85, -5.4);
    const model = rotateY(yaw);
    const mvp = mul(mul(proj, view), model);
    const L1 = lightPos(az, el, 3.4);
    const L2 = lightPos(az + 2.3, el * 0.55 + 0.2, 3.2);
    const cam = [0, 1.4, 5.4];

    gl.useProgram(meshProg);
    gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, "u_mvp"), false, mvp);
    gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, "u_model"), false, model);
    gl.uniform3fv(gl.getUniformLocation(meshProg, "u_cam"), cam);
    gl.uniform3fv(gl.getUniformLocation(meshProg, "u_color"), hexToRgb(PARAMS.color));
    gl.uniform3fv(gl.getUniformLocation(meshProg, "u_lightA"), L1);
    gl.uniform3fv(gl.getUniformLocation(meshProg, "u_lightB"), L2);
    gl.uniform3fv(gl.getUniformLocation(meshProg, "u_colA"), hexToRgb(PARAMS.light));
    gl.uniform3fv(gl.getUniformLocation(meshProg, "u_colB"), hexToRgb(PARAMS.fill));
    bind(meshProg, "a_pos", posBuf, 3);
    bind(meshProg, "a_nrm", nrmBuf, 3);
    gl.drawArrays(gl.TRIANGLES, 0, meshCount);

    function drawBall(L, col) {
      const s = 0.09;
      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]));
      const bmvp = mul(mul(proj, view), m);
      gl.useProgram(ballProg);
      gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, "u_mvp"), false, bmvp);
      gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, "u_model"), false, m);
      gl.uniform3fv(gl.getUniformLocation(ballProg, "u_colA"), col);
      bind(ballProg, "a_pos", ballP, 3);
      bind(ballProg, "a_nrm", ballN, 3);
      gl.drawArrays(gl.TRIANGLES, 0, ballCount);
    }
    drawBall(L1, hexToRgb(PARAMS.light));
    drawBall(L2, hexToRgb(PARAMS.fill));

    if (!reduced) requestAnimationFrame(frame);
  }

  canvas.addEventListener("webglcontextlost", function (e) {
    e.preventDefault();
    fail("WebGL context was lost. Reload the page.");
  });

  if (reduced) { resize(); last = 0; frame(0); }
  else requestAnimationFrame(frame);
})();