{
  "streamed": true,
  "usage": {
    "prompt_tokens": 160,
    "completion_tokens": 52386,
    "total_tokens": 52546,
    "cost": 1.31045,
    "is_byok": false,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "cache_write_tokens": 0,
      "audio_tokens": 0,
      "video_tokens": 0
    },
    "cost_details": {
      "upstream_inference_cost": 1.31045,
      "upstream_inference_prompt_cost": 0.0008,
      "upstream_inference_completions_cost": 1.30965
    },
    "completion_tokens_details": {
      "reasoning_tokens": 6247,
      "image_tokens": 0,
      "audio_tokens": 0
    }
  },
  "finishReason": "stop",
  "content": "```html\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n<title>Big Mac — three.js</title>\n<style>\n  html, body { margin:0; padding:0; height:100%; overflow:hidden; background:#08080a; }\n  canvas { display:block; width:100%; height:100%; }\n  #hint {\n    position:fixed; left:50%; bottom:16px; transform:translateX(-50%);\n    font:11px/1.6 \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n    letter-spacing:.18em; text-transform:uppercase;\n    color:rgba(255,255,255,.42); pointer-events:none; user-select:none; text-align:center;\n  }\n  #load {\n    position:fixed; inset:0; z-index:5; display:flex; align-items:center; justify-content:center;\n    background:#08080a; color:rgba(255,255,255,.7);\n    font:12px/1.6 \"Helvetica Neue\", Helvetica, Arial, sans-serif; letter-spacing:.22em; text-transform:uppercase;\n    transition:opacity .8s ease; \n  }\n</style>\n</head>\n<body>\n<div id=\"load\">grilling…</div>\n<div id=\"hint\">drag to orbit &nbsp;·&nbsp; scroll to zoom</div>\n\n<script type=\"importmap\">\n{\n  \"imports\": {\n    \"three\": \"https://unpkg.com/three@0.161.0/build/three.module.js\",\n    \"three/addons/\": \"https://unpkg.com/three@0.161.0/examples/jsm/\"\n  }\n}\n</script>\n\n<script type=\"module\">\nimport * as THREE from 'three';\nimport { OrbitControls } from 'three/addons/controls/OrbitControls.js';\nimport { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';\nimport { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';\nimport { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/addons/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';\nimport { OutputPass } from 'three/addons/postprocessing/OutputPass.js';\n\n/* ------------------------------------------------------------------ *\n *  Random + noise utilities\n * ------------------------------------------------------------------ */\nfunction mulberry32(a){\n  return function(){\n    a |= 0; a = a + 0x6D2B79F5 | 0;\n    let t = Math.imul(a ^ a >>> 15, 1 | a);\n    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;\n    return ((t ^ t >>> 14) >>> 0) / 4294967296;\n  };\n}\nconst RND = mulberry32(20240813);\n\nfunction hash3(x, y, z){\n  const s = Math.sin(x * 127.1 + y * 311.7 + z * 74.7) * 43758.5453123;\n  return s - Math.floor(s);\n}\nconst lerp = (a, b, t) => a + (b - a) * t;\nfunction vnoise(x, y, z){\n  const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);\n  const xf = x - xi, yf = y - yi, zf = z - zi;\n  const u = xf*xf*(3-2*xf), v = yf*yf*(3-2*yf), w = zf*zf*(3-2*zf);\n  const c = (i,j,k) => hash3(xi+i, yi+j, zi+k);\n  const x00 = lerp(c(0,0,0), c(1,0,0), u), x10 = lerp(c(0,1,0), c(1,1,0), u);\n  const x01 = lerp(c(0,0,1), c(1,0,1), u), x11 = lerp(c(0,1,1), c(1,1,1), u);\n  return lerp(lerp(x00, x10, v), lerp(x01, x11, v), w);\n}\nfunction fbm(x, y, z, oct = 4){\n  let a = 0.5, s = 0, n = 0;\n  for (let i = 0; i < oct; i++){\n    s += a * vnoise(x, y, z); n += a;\n    x *= 2.03; y *= 2.01; z *= 1.97; a *= 0.5;\n  }\n  return s / n;\n}\n\n/* ------------------------------------------------------------------ *\n *  Procedural canvas texture helpers\n * ------------------------------------------------------------------ */\nfunction C(rgb, a = 1){ return `rgba(${rgb[0]|0},${rgb[1]|0},${rgb[2]|0},${a})`; }\nfunction jit(base, amt, rnd){\n  return [ base[0] + (rnd()-0.5)*amt, base[1] + (rnd()-0.5)*amt, base[2] + (rnd()-0.5)*amt ];\n}\n/* soft blob, drawn three times so the texture tiles horizontally */\nfunction blob(ctx, W, x, y, r, rgb, alpha){\n  for (const dx of [-W, 0, W]){\n    const g = ctx.createRadialGradient(x+dx, y, 0, x+dx, y, r);\n    g.addColorStop(0, C(rgb, alpha));\n    g.addColorStop(1, C(rgb, 0));\n    ctx.fillStyle = g;\n    ctx.beginPath(); ctx.arc(x+dx, y, r, 0, 6.28319); ctx.fill();\n  }\n}\nfunction makeCanvas(W, H){\n  const cv = document.createElement('canvas'); cv.width = W; cv.height = H;\n  return [cv, cv.getContext('2d')];\n}\nfunction tex(cv, srgb, repX = 1, repY = 1){\n  const t = new THREE.CanvasTexture(cv);\n  t.wrapS = t.wrapT = THREE.RepeatWrapping;\n  t.repeat.set(repX, repY);\n  t.anisotropy = 8;\n  if (srgb) t.colorSpace = THREE.SRGBColorSpace;\n  return t;\n}\n\n/* ---- bun colour map (crust + crumb regions) ---- */\nfunction makeBunTex(kind){\n  const W = 1024, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(kind === 'crown' ? 11 : kind === 'heel' ? 29 : 47);\n  const crust = kind === 'club' ? [198,138,72] : [199,134,70];\n  const crumb = kind === 'club' ? [230,193,136] : [240,223,187];\n\n  ctx.fillStyle = C(crust); ctx.fillRect(0, 0, W, H);\n\n  const wav = (base, amp, k, ph) => (x) =>\n    base*H + amp*Math.sin(2*Math.PI*k*x/W + ph) + amp*0.45*Math.sin(2*Math.PI*k*2.7*x/W + ph*2.3);\n  const band = (topFn, botFn) => {\n    const p = new Path2D();\n    p.moveTo(0, topFn(0));\n    for (let x = 0; x <= W; x += 8) p.lineTo(x, topFn(x));\n    for (let x = W; x >= 0; x -= 8) p.lineTo(x, botFn(x));\n    p.closePath(); return p;\n  };\n  const flat = (v) => () => v;\n\n  let bands;\n  if (kind === 'crown'){\n    const b = wav(0.545, 10, 3, 0.7);\n    bands = [ {t: flat(-4), b: b, type:'crust'}, {t: b, b: flat(H+4), type:'crumb'} ];\n  } else if (kind === 'heel'){\n    const b = wav(0.455, 10, 4, 1.9);\n    bands = [ {t: flat(-4), b: b, type:'crumb'}, {t: b, b: flat(H+4), type:'crust'} ];\n  } else {\n    const b1 = wav(0.435, 8, 5, 0.3), b2 = wav(0.575, 8, 4, 2.4);\n    bands = [\n      {t: flat(-4), b: b1, type:'crumb'},\n      {t: b1, b: b2, type:'crust'},\n      {t: b2, b: flat(H+4), type:'crumb'}\n    ];\n  }\n\n  for (const bd of bands){\n    ctx.save();\n    ctx.clip(band(bd.t, bd.b));\n    if (bd.type === 'crumb'){\n      ctx.fillStyle = C(crumb); ctx.fillRect(0, 0, W, H);\n      for (let i = 0; i < 900; i++) blob(ctx, W, rnd()*W, rnd()*H, 3+rnd()*9, jit([210,184,143], 30, rnd), 0.30);\n      for (let i = 0; i < 700; i++) blob(ctx, W, rnd()*W, rnd()*H, 1+rnd()*3.2, jit([158,127,92], 30, rnd), 0.40);\n      if (kind === 'club')\n        for (let i = 0; i < 320; i++) blob(ctx, W, rnd()*W, rnd()*H, 10+rnd()*46, jit([196,140,70], 46, rnd), 0.24);\n    } else {\n      ctx.fillStyle = C(crust); ctx.fillRect(0, 0, W, H);\n      for (let i = 0; i < 300; i++) blob(ctx, W, rnd()*W, rnd()*H, 18+rnd()*95, jit([200,133,68], 58, rnd), 0.30);\n      for (let i = 0; i < 150; i++) blob(ctx, W, rnd()*W, rnd()*H, 6+rnd()*26, jit([126,70,28], 36, rnd), 0.32);\n      for (let i = 0; i < 900; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.9+rnd()*2.4, jit([243,220,182], 24, rnd), 0.28);\n    }\n    ctx.restore();\n  }\n  return tex(cv, true);\n}\n\n/* ---- shared bun bump ---- */\nfunction makeBunBump(){\n  const W = 1024, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(777);\n  ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 900; i++) blob(ctx, W, rnd()*W, rnd()*H, 10+rnd()*50, jit([128,128,128], 90, rnd), 0.28);\n  for (let i = 0; i < 5200; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.7+rnd()*2.3, [58,58,58], 0.30);\n  for (let i = 0; i < 1800; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.7+rnd()*2.0, [212,212,212], 0.25);\n  return tex(cv, false);\n}\n\n/* ---- patty ---- */\nfunction makePattyTex(){\n  const W = 1024, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(1234);\n  ctx.fillStyle = C([88,55,33]); ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 420; i++) blob(ctx, W, rnd()*W, rnd()*H, 12+rnd()*80, jit([84,50,30], 46, rnd), 0.42);\n  for (let i = 0; i < 260; i++) blob(ctx, W, rnd()*W, rnd()*H, 8+rnd()*46, jit([44,25,15], 22, rnd), 0.55);\n  for (let i = 0; i < 200; i++) blob(ctx, W, rnd()*W, rnd()*H, 6+rnd()*30, jit([132,84,48], 34, rnd), 0.32);\n  // grain streaks\n  ctx.lineCap = 'round';\n  for (let i = 0; i < 900; i++){\n    const x = rnd()*W, y = rnd()*H, l = 6+rnd()*26, a = rnd()*Math.PI;\n    ctx.strokeStyle = C(jit([60,34,20], 40, rnd), 0.22);\n    ctx.lineWidth = 0.8+rnd()*2.2;\n    ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+Math.cos(a)*l, y+Math.sin(a)*l); ctx.stroke();\n  }\n  for (let i = 0; i < 2600; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.7+rnd()*2.0, jit([150,110,70], 60, rnd), 0.22);\n  return tex(cv, true);\n}\nfunction makePattyBump(){\n  const W = 1024, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(4321);\n  ctx.fillStyle = '#7d7d7d'; ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 700; i++) blob(ctx, W, rnd()*W, rnd()*H, 8+rnd()*46, jit([128,128,128], 120, rnd), 0.34);\n  for (let i = 0; i < 6000; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.8+rnd()*3.2, rnd() > .5 ? [40,40,40] : [220,220,220], 0.30);\n  return tex(cv, false);\n}\n\n/* ---- cheese ---- */\nfunction makeCheeseTex(){\n  const W = 512, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(99);\n  ctx.fillStyle = C([236,166,58]); ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 200; i++) blob(ctx, W, rnd()*W, rnd()*H, 20+rnd()*110, jit([238,170,62], 40, rnd), 0.35);\n  for (let i = 0; i < 900; i++) blob(ctx, W, rnd()*W, rnd()*H, 1+rnd()*4, jit([210,140,44], 40, rnd), 0.25);\n  return tex(cv, true);\n}\n\n/* ---- sauce ---- */\nfunction makeSauceTex(){\n  const W = 512, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(555);\n  ctx.fillStyle = C([238,180,72]); ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 220; i++) blob(ctx, W, rnd()*W, rnd()*H, 20+rnd()*90, jit([243,196,96], 38, rnd), 0.4);\n  for (let i = 0; i < 180; i++) blob(ctx, W, rnd()*W, rnd()*H, 10+rnd()*45, jit([214,148,48], 30, rnd), 0.3);\n  // relish specks\n  for (let i = 0; i < 260; i++) blob(ctx, W, rnd()*W, rnd()*H, 1.4+rnd()*3.6, jit([132,152,62], 40, rnd), 0.75);\n  for (let i = 0; i < 220; i++) blob(ctx, W, rnd()*W, rnd()*H, 1.2+rnd()*3.0, jit([250,238,200], 26, rnd), 0.6);\n  for (let i = 0; i < 90; i++) blob(ctx, W, rnd()*W, rnd()*H, 1.2+rnd()*2.6, jit([196,120,48], 30, rnd), 0.6);\n  return tex(cv, true);\n}\nfunction makeSauceBump(){\n  const W = 512, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(556);\n  ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 400; i++) blob(ctx, W, rnd()*W, rnd()*H, 6+rnd()*40, jit([128,128,128], 80, rnd), 0.3);\n  for (let i = 0; i < 500; i++) blob(ctx, W, rnd()*W, rnd()*H, 1.5+rnd()*4, rnd() > .5 ? [30,30,30] : [230,230,230], 0.35);\n  return tex(cv, false);\n}\n\n/* ---- pickle ---- */\nfunction makePickleTex(){\n  const W = 512, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(31);\n  const g = ctx.createRadialGradient(W/2, H/2, 10, W/2, H/2, W*0.52);\n  g.addColorStop(0, C([186,199,116]));\n  g.addColorStop(0.55, C([150,172,84]));\n  g.addColorStop(1, C([98,124,44]));\n  ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 260; i++) blob(ctx, W, rnd()*W, rnd()*H, 8+rnd()*46, jit([140,166,76], 46, rnd), 0.3);\n  // seed cluster\n  for (let i = 0; i < 26; i++){\n    const a = rnd()*6.283, r = 30 + rnd()*80;\n    const x = W/2 + Math.cos(a)*r, y = H/2 + Math.sin(a)*r;\n    ctx.save(); ctx.translate(x, y); ctx.rotate(rnd()*6.283);\n    ctx.fillStyle = C([228,231,190], 0.75);\n    ctx.beginPath(); ctx.ellipse(0, 0, 5+rnd()*4, 3+rnd()*2, 0, 0, 6.283); ctx.fill();\n    ctx.restore();\n  }\n  for (let i = 0; i < 1200; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.8+rnd()*2.4, jit([120,146,64], 60, rnd), 0.25);\n  return tex(cv, true);\n}\n\n/* ---- floor ---- */\nfunction makeFloorTex(){\n  const W = 1024, H = 1024;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(8);\n  ctx.fillStyle = C([44,41,38]); ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 700; i++) blob(ctx, W, rnd()*W, rnd()*H, 20+rnd()*160, jit([46,42,39], 26, rnd), 0.4);\n  for (let i = 0; i < 6000; i++) blob(ctx, W, rnd()*W, rnd()*H, 0.8+rnd()*2.6, jit([60,56,52], 46, rnd), 0.3);\n  return tex(cv, true, 3, 3);\n}\nfunction makeFloorRough(){\n  const W = 512, H = 512;\n  const [cv, ctx] = makeCanvas(W, H);\n  const rnd = mulberry32(9);\n  ctx.fillStyle = '#9c9c9c'; ctx.fillRect(0, 0, W, H);\n  for (let i = 0; i < 600; i++) blob(ctx, W, rnd()*W, rnd()*H, 10+rnd()*90, jit([156,156,156], 90, rnd), 0.35);\n  return tex(cv, false, 3, 3);\n}\n\n/* ------------------------------------------------------------------ *\n *  Renderer / scene / camera\n * ------------------------------------------------------------------ */\nconst renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.02;\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\nrenderer.shadowMap.enabled = true;\nrenderer.shadowMap.type = THREE.PCFSoftShadowMap;\ndocument.body.appendChild(renderer.domElement);\n\nconst scene = new THREE.Scene();\nscene.fog = new THREE.Fog(0x0a0a0c, 16, 52);\n\nconst camera = new THREE.PerspectiveCamera(33, window.innerWidth/window.innerHeight, 0.1, 200);\ncamera.position.set(7.6, 5.1, 9.8);\n\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.target.set(0, 2.45, 0);\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.06;\ncontrols.minDistance = 6.5;\ncontrols.maxDistance = 28;\ncontrols.maxPolarAngle = Math.PI * 0.495;\ncontrols.enablePan = false;\n\n/* background gradient */\n(function(){\n  const [cv, ctx] = makeCanvas(16, 512);\n  const g = ctx.createLinearGradient(0, 0, 0, 512);\n  g.addColorStop(0.00, '#242429');\n  g.addColorStop(0.42, '#14141a');\n  g.addColorStop(0.62, '#0a0a0d');\n  g.addColorStop(1.00, '#050506');\n  ctx.fillStyle = g; ctx.fillRect(0, 0, 16, 512);\n  const t = new THREE.CanvasTexture(cv);\n  t.mapping = THREE.EquirectangularReflectionMapping;\n  t.colorSpace = THREE.SRGBColorSpace;\n  scene.background = t;\n})();\n\n/* studio environment for reflections */\nconst pmrem = new THREE.PMREMGenerator(renderer);\npmrem.compileEquirectangularShader();\nconst envRT = pmrem.fromScene(new RoomEnvironment(), 0.03);\nscene.environment = envRT.texture;\npmrem.dispose();\n\n/* ------------------------------------------------------------------ *\n *  Lights\n * ------------------------------------------------------------------ */\nconst key = new THREE.DirectionalLight(0xfff1de, 3.1);\nkey.position.set(6.5, 10.5, 7.0);\nkey.castShadow = true;\nkey.shadow.mapSize.set(2048, 2048);\nkey.shadow.camera.left = -7; key.shadow.camera.right = 7;\nkey.shadow.camera.top = 7; key.shadow.camera.bottom = -7;\nkey.shadow.camera.near = 1; key.shadow.camera.far = 40;\nkey.shadow.bias = -0.0004;\nkey.shadow.normalBias = 0.025;\nkey.shadow.radius = 2.2;\nscene.add(key);\n\nconst rim = new THREE.DirectionalLight(0xdce8ff, 1.35);\nrim.position.set(-6.5, 7.5, -7.5);\nscene.add(rim);\n\nconst fill = new THREE.DirectionalLight(0xffd9b0, 0.45);\nfill.position.set(-5.0, 2.0, 6.5);\nscene.add(fill);\n\nconst under = new THREE.PointLight(0xffe6c8, 8, 14, 2.0);\nunder.position.set(0, 0.9, 3.2);\nscene.add(under);\n\n/* ------------------------------------------------------------------ *\n *  Geometry helpers\n * ------------------------------------------------------------------ */\nfunction revolveProfile({ R, yc, h1, h2, p1 = 4, q1 = 3.2, p2 = 4, q2 = 3.2, n = 30 }){\n  const pts = [];\n  for (let i = 0; i <= n; i++){\n    const t = i / n;\n    const r = Math.max(0.0006, R * Math.sin(t * Math.PI / 2));\n    const k = Math.max(0, 1 - Math.pow(r / R, p1));\n    pts.push(new THREE.Vector2(r, yc - h1 * Math.pow(k, 1 / q1)));\n  }\n  for (let i = n - 1; i >= 0; i--){\n    const t = i / n;\n    const r = Math.max(0.0006, R * Math.sin(t * Math.PI / 2));\n    const k = Math.max(0, 1 - Math.pow(r / R, p2));\n    pts.push(new THREE.Vector2(r, yc + h2 * Math.pow(k, 1 / q2)));\n  }\n  return pts;\n}\nfunction topSurfaceFn({ R, yc, h2, p2, q2 }){\n  return (r) => yc + h2 * Math.pow(Math.max(0, 1 - Math.pow(Math.min(r, R) / R, p2)), 1 / q2);\n}\nfunction displaceGeo(geo, o){\n  const amp = o.amp ?? 0.04, freq = o.freq ?? 1, oct = o.oct ?? 4;\n  const rAmp = o.radialAmp ?? 0, rFreq = o.radialFreq ?? 1, seed = o.seed ?? 0;\n  const pos = geo.attributes.position, nor = geo.attributes.normal;\n  const v = new THREE.Vector3(), n = new THREE.Vector3();\n  for (let i = 0; i < pos.count; i++){\n    v.fromBufferAttribute(pos, i);\n    n.fromBufferAttribute(nor, i);\n    const d = (fbm(v.x*freq + seed, v.y*freq, v.z*freq + seed, oct) - 0.5) * 2 * amp;\n    v.addScaledVector(n, d);\n    if (rAmp > 0){\n      const hl = Math.hypot(v.x, v.z);\n      if (hl > 1e-4){\n        const dx = v.x / hl, dz = v.z / hl;\n        const rd = (fbm(dx*rFreq*3 + 11.3 + seed, 5.5, dz*rFreq*3 + 7.1, 2) - 0.5) * 2 * rAmp;\n        const s = rd * Math.min(1, hl / 2.2);\n        v.x += dx * s; v.z += dz * s;\n      }\n    }\n    pos.setXYZ(i, v.x, v.y, v.z);\n  }\n  geo.computeVertexNormals();\n}\nfunction fixLatheSeam(geo, segments, pcount){\n  const nor = geo.attributes.normal;\n  for (let j = 0; j < pcount; j++){\n    const a = j, b = segments * pcount + j;\n    let x = nor.getX(a) + nor.getX(b), y = nor.getY(a) + nor.getY(b), z = nor.getZ(a) + nor.getZ(b);\n    const l = Math.hypot(x, y, z) || 1; x /= l; y /= l; z /= l;\n    nor.setXYZ(a, x, y, z); nor.setXYZ(b, x, y, z);\n  }\n  nor.needsUpdate = true;\n}\nfunction latheMesh(profile, segments, mat, disp){\n  const geo = new THREE.LatheGeometry(profile, segments);\n  if (disp) displaceGeo(geo, disp);\n  fixLatheSeam(geo, segments, profile.length);\n  const m = new THREE.Mesh(geo, mat);\n  m.castShadow = true; m.receiveShadow = true;\n  return m;\n}\n\n/* ------------------------------------------------------------------ *\n *  Materials\n * ------------------------------------------------------------------ */\nconst ENV = 0.62;\nconst bunBump = makeBunBump();\nfunction bunMaterial(kind){\n  return new THREE.MeshPhysicalMaterial({\n    map: makeBunTex(kind),\n    bumpMap: bunBump,\n    bumpScale: kind === 'club' ? 0.03 : 0.05,\n    roughness: 0.82,\n    metalness: 0.0,\n    sheen: 0.3, sheenRoughness: 0.9, sheenColor: new THREE.Color(0xffe0b0),\n    clearcoat: kind === 'crown' ? 0.10 : 0.05,\n    clearcoatRoughness: 0.7,\n    envMapIntensity: ENV\n  });\n}\nconst matCrown = bunMaterial('crown');\nconst matHeel  = bunMaterial('heel');\nconst matClub  = bunMaterial('club');\n\nconst matPatty = new THREE.MeshPhysicalMaterial({\n  map: makePattyTex(), bumpMap: makePattyBump(), bumpScale: 0.085,\n  roughness: 0.63, metalness: 0.0,\n  clearcoat: 0.22, clearcoatRoughness: 0.42,\n  sheen: 0.12, sheenColor: new THREE.Color(0x996633),\n  envMapIntensity: ENV\n});\n\nconst matCheese = new THREE.MeshPhysicalMaterial({\n  map: makeCheeseTex(), color: 0xffffff,\n  roughness: 0.40, metalness: 0.0,\n  clearcoat: 0.42, clearcoatRoughness: 0.32,\n  sheen: 0.25, sheenColor: new THREE.Color(0xffd9a0),\n  side: THREE.DoubleSide, envMapIntensity: ENV\n});\n\nconst matSauce = new THREE.MeshPhysicalMaterial({\n  map: makeSauceTex(), bumpMap: makeSauceBump(), bumpScale: 0.02,\n  roughness: 0.26, metalness: 0.0,\n  clearcoat: 0.85, clearcoatRoughness: 0.14,\n  side: THREE.DoubleSide, envMapIntensity: ENV * 1.15\n});\n\nconst matLettuce = new THREE.MeshPhysicalMaterial({\n  vertexColors: true, roughness: 0.32, metalness: 0.0,\n  clearcoat: 0.6, clearcoatRoughness: 0.22,\n  sheen: 0.3, sheenColor: new THREE.Color(0xd8ffb0),\n  side: THREE.DoubleSide, envMapIntensity: ENV * 1.2\n});\n\nconst matOnion = new THREE.MeshPhysicalMaterial({\n  color: 0xf5efe2, roughness: 0.34, metalness: 0.0,\n  transmission: 0.22, thickness: 0.12, ior: 1.36,\n  clearcoat: 0.35, clearcoatRoughness: 0.3, envMapIntensity: ENV\n});\n\nconst matPickle = new THREE.MeshPhysicalMaterial({\n  map: makePickleTex(), roughness: 0.30, metalness: 0.0,\n  transmission: 0.25, thickness: 0.3, ior: 1.4,\n  clearcoat: 0.6, clearcoatRoughness: 0.2, envMapIntensity: ENV\n});\n\nconst matSeed = new THREE.MeshPhysicalMaterial({\n  color: 0xe6d3a2, roughness: 0.42, metalness: 0.0,\n  clearcoat: 0.35, clearcoatRoughness: 0.3, envMapIntensity: ENV\n});\n\n/* ------------------------------------------------------------------ *\n *  Floor\n * ------------------------------------------------------------------ */\nconst floor = new THREE.Mesh(\n  new THREE.PlaneGeometry(90, 90),\n  new THREE.MeshStandardMaterial({\n    map: makeFloorTex(), roughnessMap: makeFloorRough(),\n    color: 0x8f8880, roughness: 0.65, metalness: 0.06, envMapIntensity: 0.35\n  })\n);\nfloor.rotation.x = -Math.PI / 2;\nfloor.position.y = -0.008;\nfloor.receiveShadow = true;\nscene.add(floor);\n\n/* ------------------------------------------------------------------ *\n *  Build the Big Mac\n * ------------------------------------------------------------------ */\nconst burger = new THREE.Group();\nscene.add(burger);\n\nconst SEG = 150;\n\n/* --- bottom bun (heel) --- */\nconst heelP = { R: 3.02, yc: 0.60, h1: 0.60, h2: 0.62, p1: 4.5, q1: 3.2, p2: 5.0, q2: 3.0 };\nconst heel = latheMesh(revolveProfile(heelP), SEG, matHeel,\n  { amp: 0.042, freq: 1.05, radialAmp: 0.075, radialFreq: 1.1, oct: 4, seed: 1.7 });\nburger.add(heel);\nconst heelTop = topSurfaceFn(heelP);\n\n/* --- middle bun (club) --- */\nconst clubP = { R: 2.86, yc: 0.30, h1: 0.30, h2: 0.34, p1: 7, q1: 3.6, p2: 7, q2: 3.6 };\nconst club = latheMesh(revolveProfile(clubP), SEG, matClub,\n  { amp: 0.032, freq: 1.25, radialAmp: 0.07, radialFreq: 1.3, oct: 4, seed: 5.2 });\nclub.position.y = 1.74;\nburger.add(club);\nconst clubTop = topSurfaceFn(clubP);\n\n/* --- top bun (crown) --- */\nconst crownP = { R: 3.06, yc: 0.52, h1: 0.52, h2: 1.52, p1: 5, q1: 3.0, p2: 2.4, q2: 1.85, n: 34 };\nconst crownProfile = revolveProfile(crownP);\nconst crownGeo = new THREE.LatheGeometry(crownProfile, SEG);\ndisplaceGeo(crownGeo, { amp: 0.045, freq: 0.95, radialAmp: 0.08, radialFreq: 1.0, oct: 4, seed: 9.1 });\nfixLatheSeam(crownGeo, SEG, crownProfile.length);\nconst crown = new THREE.Mesh(crownGeo, matCrown);\ncrown.castShadow = crown.receiveShadow = true;\ncrown.position.y = 3.06;\nburger.add(crown);\n\n/* --- sesame seeds on the crown --- */\n(function(){\n  const seedGeo = new THREE.SphereGeometry(0.5, 16, 11);\n  seedGeo.scale(0.215, 0.072, 0.145);\n  const sp = seedGeo.attributes.position;\n  for (let i = 0; i < sp.count; i++){\n    const x = sp.getX(i), y = sp.getY(i), z = sp.getZ(i);\n    const t = (x / 0.1075 + 1) * 0.5;               // 0..1 along length\n    const s = 1 - 0.55 * Math.pow(Math.max(0, t), 2.2);\n    sp.setXYZ(i, x, y * s, z * s);\n  }\n  seedGeo.computeVertexNormals();\n\n  const pos = crownGeo.attributes.position, nor = crownGeo.attributes.normal;\n  const cand = [];\n  for (let i = 0; i < pos.count; i++){\n    if (nor.getY(i) > 0.34 && pos.getY(i) > 0.78) cand.push(i);\n  }\n  for (let i = cand.length - 1; i > 0; i--){\n    const j = Math.floor(RND() * (i + 1));\n    [cand[i], cand[j]] = [cand[j], cand[i]];\n  }\n  const chosen = [];\n  const p = new THREE.Vector3(), n = new THREE.Vector3();\n  for (const i of cand){\n    p.fromBufferAttribute(pos, i);\n    let ok = true;\n    for (const c of chosen){ if (c.p.distanceTo(p) < 0.32){ ok = false; break; } }\n    if (!ok) continue;\n    n.fromBufferAttribute(nor, i);\n    chosen.push({ p: p.clone(), n: n.clone().normalize() });\n    if (chosen.length >= 135) break;\n  }\n\n  const inst = new THREE.InstancedMesh(seedGeo, matSeed, chosen.length);\n  inst.castShadow = true; inst.receiveShadow = true;\n  const m = new THREE.Matrix4(), q = new THREE.Quaternion(), q2 = new THREE.Quaternion();\n  const up = new THREE.Vector3(0, 1, 0), sc = new THREE.Vector3();\n  const col = new THREE.Color();\n  chosen.forEach((c, i) => {\n    q.setFromUnitVectors(up, c.n);\n    q2.setFromAxisAngle(up, RND() * Math.PI * 2);\n    q.multiply(q2);\n    const s = 0.85 + RND() * 0.35;\n    sc.set(s, s * (0.8 + RND() * 0.5), s);\n    m.compose(c.p.clone().addScaledVector(c.n, -0.015), q, sc);\n    inst.setMatrixAt(i, m);\n    const t = RND();\n    col.setRGB(1, 1, 1).lerp(new THREE.Color(0xb08b4a), t * t * 0.85);\n    inst.setColorAt(i, col);\n  });\n  inst.position.y = 3.06;\n  burger.add(inst);\n})();\n\n/* --- patties --- */\nconst pattyP = { R: 3.18, yc: 0.25, h1: 0.25, h2: 0.27, p1: 9, q1: 4.6, p2: 9, q2: 4.6, n: 26 };\nfunction makePatty(seed){\n  return latheMesh(revolveProfile(pattyP), SEG, matPatty,\n    { amp: 0.055, freq: 1.7, radialAmp: 0.10, radialFreq: 1.45, oct: 5, seed });\n}\nconst patty1 = makePatty(2.3); patty1.position.y = 1.24; burger.add(patty1);\nconst patty2 = makePatty(17.6); patty2.position.y = 2.62; patty2.rotation.y = 1.1; burger.add(patty2);\n\n/* --- sauce puddles --- */\nfunction makeSauceGeo({ R, thick, seed, baseFn }){\n  const NR = 26, NT = 112;\n  const positions = [], uvs = [], indices = [];\n  for (let i = 0; i <= NR; i++){\n    const t = i / NR;\n    for (let j = 0; j <= NT; j++){\n      const a = j / NT * Math.PI * 2;\n      const ca = Math.cos(a), sa = Math.sin(a);\n      const Re = R * (0.90 + 0.20 * fbm(ca * 1.7 + seed, 3.1, sa * 1.7 + seed, 3)\n                          + 0.045 * Math.sin(a * 6 + seed));\n      const r = Re * Math.pow(t, 0.82);\n      let y = baseFn(r) + thick * Math.pow(Math.max(0, 1 - Math.pow(t, 2.7)), 0.55);\n      y += (fbm(ca * r * 1.7 + seed, 9.2, sa * r * 1.7, 3) - 0.5) * 0.035 * (1 - t * 0.6);\n      positions.push(r * ca, y, r * sa);\n      uvs.push(0.5 + 0.5 * t * ca, 0.5 + 0.5 * t * sa);\n    }\n  }\n  for (let i = 0; i < NR; i++){\n    for (let j = 0; j < NT; j++){\n      const a = i * (NT + 1) + j, b = a + 1, c = a + NT + 1, d = c + 1;\n      indices.push(a, b, c, b, d, c);\n    }\n  }\n  const g = new THREE.BufferGeometry();\n  g.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));\n  g.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));\n  g.setIndex(indices);\n  g.computeVertexNormals();\n  return g;\n}\nconst sauce1 = new THREE.Mesh(makeSauceGeo({ R: 2.68, thick: 0.115, seed: 3.4, baseFn: r => heelTop(r) - 0.03 }), matSauce);\nsauce1.castShadow = true; sauce1.receiveShadow = true;\nburger.add(sauce1);\n\nconst sauce2 = new THREE.Mesh(makeSauceGeo({ R: 2.52, thick: 0.115, seed: 12.9, baseFn: r => clubTop(r) - 0.03 }), matSauce);\nsauce2.position.y = 1.74; sauce2.castShadow = true; sauce2.receiveShadow = true;\nburger.add(sauce2);\n\n/* --- helper: sit an object on a dome surface --- */\nfunction placeOnDome(obj, r, a, fn, lift){\n  const eps = 0.03;\n  obj.position.set(r * Math.cos(a), fn(r) + lift, r * Math.sin(a));\n  const slope = (fn(r + eps) - fn(Math.max(0, r - eps))) / (2 * eps);\n  obj.rotation.set(0, 0, 0);\n  obj.rotateY(-a);\n  obj.rotateZ(Math.atan(slope));\n}\n\n/* --- pickles --- */\n(function(){\n  const g = new THREE.CylinderGeometry(0.94, 0.90, 0.155, 84, 2, false);\n  const pos = g.attributes.position;\n  for (let i = 0; i < pos.count; i++){\n    const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);\n    const a = Math.atan2(z, x);\n    const s = 1 + 0.055 * Math.sin(a * 11) + 0.022 * Math.sin(a * 23 + 1.1);\n    let ny = y;\n    if (Math.abs(y) > 0.066) ny = y + Math.sign(y) * (fbm(x * 4.2, 0, z * 4.2, 3) - 0.5) * 0.05;\n    pos.setXYZ(i, x * s, ny, z * s);\n  }\n  g.computeVertexNormals();\n  const spots = [\n    { r: 2.30, a: 0.55 }, { r: 2.24, a: 2.65 }, { r: 1.30, a: 4.35 }, { r: 2.32, a: 5.05 }\n  ];\n  for (const s of spots){\n    const p = new THREE.Mesh(g, matPickle);\n    p.castShadow = true; p.receiveShadow = true;\n    placeOnDome(p, s.r, s.a, heelTop, 0.10);\n    p.rotateY(RND() * 3.14);\n    p.rotateX((RND() - 0.5) * 0.16);\n    burger.add(p);\n  }\n})();\n\n/* --- diced onions --- */\nfunction makeOnionGeo(){\n  const g = new THREE.BoxGeometry(0.30, 0.075, 0.235, 3, 1, 3).toNonIndexed();\n  const pos = g.attributes.position;\n  for (let i = 0; i < pos.count; i++){\n    const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);\n    const n = (fbm(x * 7 + 2, y * 7, z * 7, 2) - 0.5) * 0.1;\n    pos.setXYZ(i, x * (1 + n), y + (fbm(x * 9, y * 9, z * 9 + 5, 2) - 0.5) * 0.028, z * (1 + n));\n  }\n  g.computeVertexNormals();\n  return g;\n}\nfunction addOnions(count, baseFn, yOffset, rMax){\n  const inst = new THREE.InstancedMesh(makeOnionGeo(), matOnion, count);\n  inst.castShadow = true; inst.receiveShadow = true;\n  const o = new THREE.Object3D();\n  for (let i = 0; i < count; i++){\n    const a = RND() * Math.PI * 2;\n    const r = Math.sqrt(RND()) * rMax;\n    placeOnDome(o, r, a, baseFn, yOffset + 0.03 + RND() * 0.05);\n    o.rotateY(RND() * Math.PI * 2);\n    o.rotateX((RND() - 0.5) * 0.7);\n    const s = 0.75 + RND() * 0.6;\n    o.scale.set(s, s * (0.7 + RND() * 0.6), s);\n    o.updateMatrix();\n    inst.setMatrixAt(i, o.matrix);\n  }\n  return inst;\n}\nconst on1 = addOnions(70, heelTop, 0.10, 2.35);\nburger.add(on1);\nconst on2 = addOnions(66, clubTop, 0.11, 2.25);\non2.position.y = 1.74;\nburger.add(on2);\n\n/* --- shredded lettuce --- */\n(function(){\n  const pieces = [];\n  const tmp = new THREE.Object3D();\n  const count = 118;\n  for (let k = 0; k < count; k++){\n    const len = 0.95 + RND() * 1.65;\n    const wid = 0.32 + RND() * 0.42;\n    const g = new THREE.PlaneGeometry(len, wid, 13, 5);\n    const pos = g.attributes.position;\n    const ph1 = RND() * 6.283, ph2 = RND() * 6.283;\n    const curl = 0.7 + RND() * 1.8, wave = 0.09 + RND() * 0.24;\n    const hue = 0.215 + RND() * 0.075;\n    const cols = [];\n    for (let i = 0; i < pos.count; i++){\n      const x = pos.getX(i), y = pos.getY(i);\n      let z = Math.sin(x * curl + ph1) * wave\n            + Math.sin(y * 7.5 + ph2) * 0.055\n            + (fbm(x * 2.6 + ph1, y * 2.6, 0.5, 3) - 0.5) * 0.17;\n      pos.setZ(i, z);\n      // ruffled edge\n      const edge = Math.abs(y) / (wid / 2);\n      if (edge > 0.85) pos.setY(i, y * (1 + 0.12 * Math.sin(x * 12 + ph2)));\n      const t = Math.min(1, edge);\n      const c = new THREE.Color().setHSL(\n        hue - 0.02 * t,\n        0.30 + 0.34 * t,\n        0.72 - 0.34 * t - RND() * 0.05\n      );\n      cols.push(c.r, c.g, c.b);\n    }\n    g.setAttribute('color', new THREE.Float32BufferAttribute(cols, 3));\n    g.computeVertexNormals();\n    g.rotateX(-Math.PI / 2);\n\n    const a = (k / count) * Math.PI * 2 + (RND() - 0.5) * 0.5;\n    const r = 2.18 + RND() * 1.12;\n    const y = 2.31 + (RND() - 0.5) * 0.30 - Math.max(0, r - 2.8) * 0.30;\n    tmp.position.set(r * Math.cos(a), y, r * Math.sin(a));\n    tmp.rotation.set(0, 0, 0);\n    tmp.rotation.order = 'YXZ';\n    tmp.rotation.y = -a + Math.PI / 2 + (RND() - 0.5) * 1.5;\n    tmp.rotation.x = (RND() - 0.35) * 0.85;\n    tmp.rotation.z = (RND() - 0.5) * 0.9;\n    tmp.scale.set(1, 1, 1);\n    tmp.updateMatrix();\n    g.applyMatrix4(tmp.matrix);\n    pieces.push(g);\n  }\n  const merged = mergeGeometries(pieces, false);\n  pieces.forEach(p => p.dispose());\n  const mesh = new THREE.Mesh(merged, matLettuce);\n  mesh.castShadow = true; mesh.receiveShadow = true;\n  burger.add(mesh);\n})();\n\n/* --- cheese slice --- */\n(function(){\n  const S = 3.22;\n  const g = new THREE.PlaneGeometry(S * 2, S * 2, 44, 44);\n  g.rotateX(-Math.PI / 2);\n  const pos = g.attributes.position;\n  const edge = 2.52;\n  for (let i = 0; i < pos.count; i++){\n    const x = pos.getX(i), z = pos.getZ(i);\n    const d = Math.hypot(x, z);\n    let y = 0;\n    if (d > edge) y = -Math.min(1.35, 0.30 * Math.pow(d - edge, 1.55));\n    y += (fbm(x * 1.15 + 3.3, 0.5, z * 1.15, 3) - 0.5) * 0.085;\n    y += -0.03 * Math.max(0, 1 - d / edge);\n    pos.setY(i, y);\n  }\n  g.computeVertexNormals();\n  const cheese = new THREE.Mesh(g, matCheese);\n  cheese.castShadow = true; cheese.receiveShadow = true;\n  cheese.position.y = 2.60;\n  cheese.rotation.y = 0.32;\n  burger.add(cheese);\n})();\n\n/* --- a few crumbs / seeds on the plate --- */\n(function(){\n  const seedGeo = new THREE.SphereGeometry(0.5, 12, 8);\n  seedGeo.scale(0.20, 0.07, 0.14);\n  const n = 12;\n  const inst = new THREE.InstancedMesh(seedGeo, matSeed, n);\n  inst.castShadow = true; inst.receiveShadow = true;\n  const o = new THREE.Object3D();\n  for (let i = 0; i < n; i++){\n    const a = RND() * Math.PI * 2, r = 3.7 + RND() * 3.4;\n    o.position.set(Math.cos(a) * r, 0.033, Math.sin(a) * r);\n    o.rotation.set(0, RND() * Math.PI * 2, 0);\n    const s = 0.8 + RND() * 0.5;\n    o.scale.set(s, s, s);\n    o.updateMatrix();\n    inst.setMatrixAt(i, o.matrix);\n  }\n  scene.add(inst);\n})();\n\n/* ------------------------------------------------------------------ *\n *  Post processing\n * ------------------------------------------------------------------ */\nconst composer = new EffectComposer(renderer);\ncomposer.addPass(new RenderPass(scene, camera));\nconst bloom = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.16, 0.55, 0.86);\ncomposer.addPass(bloom);\ncomposer.addPass(new OutputPass());\n\n/* ------------------------------------------------------------------ *\n *  Loop\n * ------------------------------------------------------------------ */\nconst clock = new THREE.Clock();\nlet firstFrame = true;\n\nfunction onResize(){\n  const w = window.innerWidth, h = window.innerHeight;\n  camera.aspect = w / h;\n  camera.updateProjectionMatrix();\n  renderer.setSize(w, h);\n  composer.setSize(w, h);\n  bloom.setSize(w, h);\n}\nwindow.addEventListener('resize', onResize);\n\nfunction animate(){\n  requestAnimationFrame(animate);\n  const dt = Math.min(clock.getDelta(), 0.05);\n  burger.rotation.y += dt * 0.16;\n  controls.update();\n  composer.render();\n\n  if (firstFrame){\n    firstFrame = false;\n    const el = document.getElementById('load');\n    el.style.opacity = '0';\n    setTimeout(() => el.remove(), 900);\n  }\n}\nanimate();\n</script>\n</body>\n</html>\n```"
}