{"slug": "show-hn-browser-viz-of-openai-s-spaghetti-navier-stokes-vortex-illustration", "title": "Show HN: Browser viz of OpenAI's \"spaghetti\" Navier–Stokes vortex (illustration)", "summary": "A browser-based WebGL visualization illustrates the vortex structure proposed in OpenAI's 2026 Navier–Stokes blowup paper, using parametric helices scaled by the paper's leading laws with an illustrative h = 0.005. The demo, posted on Hacker News, allows users to orbit, scrub toward the singularity time T*, and follow the core, but it is explicitly an illustration of the construction, not a numerical reproduction of the proof.", "body_md": "28. Scientific visualization · 2026\n\n# Spaghetti vortex\n\n## About\n\nParametric helices scaled by the 2026 OpenAI paper's leading laws: radius follows tau^(1/2), height tau^(1/2−h), with illustrative h = 0.005 and tau = 1−t/T*. Both shrink; radius shrinks faster. Drag to orbit, scrub toward T*, or follow the core with a labeled uniform zoom. Color shows normalized radius. Oscillatory corrections are omitted. Illustration of the proposed blowup construction; not a numerical reproduction of the proof.\n\nDrag to orbit; auto-rotation stops after the first drag. Play, pause, scrub the approach, or follow the core.\n\n## Browser APIs\n\n- WebGL 1\n- GLSL ES 1.0\n- Pointer Events\n- requestAnimationFrame\n- matchMedia\n\nIf WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.\n\n## Source\n\n``` js\n(function () {\n  const PARAMS = {\n    color: \"#6ee7b7\",\n    core: \"#f2b45c\",\n    trail: 0.35,\n    speed: 1\n  };\n  // Leading scales: OpenAI (2026), section 2.1. h is illustrative.\n  // These scales do not define a velocity field or simulate its corrections.\n  const H = 0.005, MAX_S = 12, COUNT = 960, SAMPLES = 24;\n  function tau(s) { return Math.pow(10, -s); }\n  function scales(s) {\n    const t = tau(s);\n    return { radial: Math.pow(t, 0.5), axial: Math.pow(t, 0.5 - H), speed: Math.pow(t, -0.5 - H) };\n  }\n  function seed(i) {\n    // Fixed low-discrepancy seeds: reset and reverse seeking need no RNG state.\n    return { phase: (i * 0.61803398875) % 1, angle: (i * 2.60258057) % (2 * Math.PI), side: i % 2 ? 1 : -1 };\n  }\n  function position(p, s) {\n    const k = scales(s), cycle = p.phase + s * 0.65;\n    const q = cycle - Math.floor(cycle);\n    // An inward helix bends into two axial exits. Recycling is a schematic\n    // seeding device; history segments across a recycle boundary are omitted.\n    const r = 0.14 + 1.36 * Math.exp(-3.6 * q);\n    const angle = p.angle + s * 6 + q * 10;\n    return [k.radial * r * Math.cos(angle), k.axial * p.side * (0.08 + 2.1 * q * q), k.radial * r * Math.sin(angle), r / 1.5, Math.floor(cycle)];\n  }\n  function rgb(hex) {\n    const n = parseInt(hex.slice(1), 16);\n    return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];\n  }\n  const canvas = document.getElementById(\"c\"), fallback = document.getElementById(\"fallback\");\n  const play = document.getElementById(\"play\"), slider = document.getElementById(\"approach\");\n  const follow = document.getElementById(\"follow\"), reset = document.getElementById(\"reset\");\n  const timeText = document.getElementById(\"time\"), zoomText = document.getElementById(\"zoom\"), ruler = document.getElementById(\"ruler\");\n  document.getElementById(\"ramp\").style.background = \"linear-gradient(90deg,\" + PARAMS.core + \",\" + PARAMS.color + \")\";\n  let gl;\n  try { gl = canvas.getContext(\"webgl\", { alpha: false, antialias: true }); } catch (_) { /* Fallback below. */ }\n  function fail(message) {\n    fallback.hidden = false;\n    fallback.textContent = message;\n    for (const el of [play, slider, follow, reset]) el.disabled = true;\n    // A failed WebGL creation still allows a Canvas 2D explanation.\n    if (!gl) {\n      const ctx = canvas.getContext(\"2d\");\n      if (ctx) {\n        canvas.width = Math.max(1, canvas.clientWidth); canvas.height = Math.max(1, canvas.clientHeight);\n        ctx.fillStyle = \"#000\"; ctx.fillRect(0, 0, canvas.width, canvas.height);\n        ctx.fillStyle = \"#c8d7d0\"; ctx.font = \"12px monospace\"; ctx.textAlign = \"center\";\n        ctx.fillText(\"WebGL unavailable\", canvas.width / 2, canvas.height / 2);\n      }\n    }\n  }\n  if (!gl) { fail(\"WebGL is unavailable. Enable WebGL to view this illustration.\"); return; }\n  const VERT = `attribute vec3 a_pos;\nattribute vec2 a_style;\nuniform mat4 u_mvp;\nuniform float u_zoom;\nuniform float u_point;\nvarying vec2 v_style;\nvarying float v_depth;\nvoid main() {\n  gl_Position = u_mvp * vec4(a_pos * u_zoom, 1.0);\n  gl_PointSize = u_point;\n  v_style = a_style;\n  v_depth = clamp(1.5 - gl_Position.w * 0.1, 0.25, 1.0);\n}`;\n  const FRAG = `precision mediump float;\nuniform vec3 u_outer;\nuniform vec3 u_core;\nvarying vec2 v_style;\nvarying float v_depth;\nvoid main() {\n  vec3 color = mix(u_core, u_outer, smoothstep(0.08, 0.85, v_style.x));\n  gl_FragColor = vec4(color, v_style.y * v_depth);\n}`;\n  function compile(type, source) {\n    const shader = gl.createShader(type);\n    gl.shaderSource(shader, source); gl.compileShader(shader);\n    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(\"Could not compile the vortex shader.\");\n    return shader;\n  }\n  // Column-major orbit matrices shared with the gallery's lowpoly example.\n  function mul(a, b) {\n    const out = new Float32Array(16);\n    for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {\n      out[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    return out;\n  }\n  function camera(aspect, pitch, yaw) {\n    const f = 1 / Math.tan(Math.PI / 8), near = 0.1, far = 50;\n    const proj = new Float32Array([f/aspect,0,0,0, 0,f,0,0, 0,0,(far+near)/(near-far),-1, 0,0,2*far*near/(near-far),0]);\n    const cx = Math.cos(pitch), sx = Math.sin(pitch), cy = Math.cos(yaw), sy = Math.sin(yaw);\n    const rx = new Float32Array([1,0,0,0, 0,cx,sx,0, 0,-sx,cx,0, 0,0,0,1]);\n    const ry = new Float32Array([cy,0,-sy,0, 0,1,0,0, sy,0,cy,0, 0,0,0,1]);\n    const view = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,-8.8*Math.max(1,0.8/aspect),1]);\n    return mul(proj, mul(view, mul(rx, ry)));\n  }\n  let prog, buffer, loc;\n  function setup() {\n    prog = gl.createProgram();\n    gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));\n    gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog);\n    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(\"Could not link the vortex shader.\");\n    buffer = gl.createBuffer();\n    loc = {};\n    for (const key of [\"mvp\", \"zoom\", \"point\", \"outer\", \"core\"]) loc[key] = gl.getUniformLocation(prog, \"u_\" + key);\n    gl.useProgram(prog); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    for (const [name, size, offset] of [[\"a_pos\", 3, 0], [\"a_style\", 2, 12]]) {\n      const at = gl.getAttribLocation(prog, name);\n      gl.enableVertexAttribArray(at); gl.vertexAttribPointer(at, size, gl.FLOAT, false, 20, offset);\n    }\n    gl.uniform3fv(loc.outer, rgb(PARAMS.color)); gl.uniform3fv(loc.core, rgb(PARAMS.core));\n    gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE);\n    gl.clearColor(0, 0, 0, 1);\n  }\n  try { setup(); } catch (err) { fail(err.message); return; }\n  const motion = matchMedia(\"(prefers-reduced-motion: reduce)\");\n  let s = motion.matches ? 6 : 0.6, playing = !motion.matches, auto = !motion.matches;\n  let yaw = 0.7, pitch = 0.34, pointer = null, lastX = 0, lastY = 0;\n  let raf = 0, last = 0, lost = false, dirty = true;\n  let count = COUNT, slow = 0;\n  const seeds = Array.from({ length: COUNT }, (_, i) => seed(i));\n  // Bounded world-space history ring, regenerated analytically on every seek.\n  // Fixed sample spacing makes history independent of playback frame rate.\n  const history = new Float32Array(COUNT * SAMPLES * 5);\n  const vertices = new Float32Array(COUNT * (SAMPLES - 1) * 2 * 5 + COUNT * 5);\n  let lineCount = 0, vertexCount = 0;\n  function rebuild() {\n    const step = PARAMS.trail / (SAMPLES - 1), tick = Math.floor(s / step), head = tick % SAMPLES;\n    let n = 0;\n    for (let i = 0; i < count; i++) {\n      for (let age = 0; age < SAMPLES; age++) {\n        const slot = (head - age + SAMPLES) % SAMPLES;\n        const t = age === 0 ? s : Math.max(0, (tick - age + 1) * step);\n        history.set(position(seeds[i], t), (i * SAMPLES + slot) * 5);\n      }\n      for (let age = SAMPLES - 1; age > 0; age--) {\n        const a = (i * SAMPLES + (head - age + SAMPLES) % SAMPLES) * 5;\n        const b = (i * SAMPLES + (head - age + 1 + SAMPLES) % SAMPLES) * 5;\n        if (history[a + 4] !== history[b + 4]) continue;\n        for (const at of [a, b]) {\n          vertices[n++] = history[at]; vertices[n++] = history[at+1]; vertices[n++] = history[at+2];\n          vertices[n++] = history[at+3]; vertices[n++] = 0.16 * (1 - age / SAMPLES);\n        }\n      }\n    }\n    lineCount = n / 5;\n    for (let i = 0; i < count; i++) {\n      const at = (i * SAMPLES + head) * 5;\n      vertices[n++] = history[at]; vertices[n++] = history[at+1]; vertices[n++] = history[at+2];\n      vertices[n++] = history[at+3]; vertices[n++] = 0.65;\n    }\n    vertexCount = n / 5;\n    dirty = false;\n  }\n  function draw() {\n    const dpr = Math.min(devicePixelRatio || 1, 2);\n    const w = Math.max(1, Math.round(canvas.clientWidth * dpr)), h = Math.max(1, Math.round(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n    gl.viewport(0, 0, canvas.width, canvas.height); gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.uniformMatrix4fv(loc.mvp, false, camera(canvas.width / canvas.height, pitch, yaw));\n    const mag = follow.checked ? 1 / scales(s).radial : 1;\n    gl.uniform1f(loc.zoom, 1); gl.uniform1f(loc.point, Math.min(2, dpr * 1.3));\n    // Reference ticks are in view units; the label converts back to world units.\n    const axes = [];\n    function line(a, b) { for (const p of [a, b]) axes.push(...p, 1, 0.13); }\n    line([0,-2.7,0], [0,2.7,0]);\n    for (let i = -3; i <= 3; i++) {\n      line([i*0.5,-2.5,-1.5], [i*0.5,-2.5,1.5]);\n      line([-1.5,-2.5,i*0.5], [1.5,-2.5,i*0.5]);\n    }\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(axes), gl.DYNAMIC_DRAW);\n    gl.drawArrays(gl.LINES, 0, axes.length / 5);\n    if (dirty) rebuild();\n    gl.uniform1f(loc.zoom, mag);\n    gl.bufferData(gl.ARRAY_BUFFER, vertices.subarray(0, vertexCount * 5), gl.DYNAMIC_DRAW);\n    gl.drawArrays(gl.LINES, 0, lineCount);\n    gl.drawArrays(gl.POINTS, lineCount, vertexCount - lineCount);\n    timeText.textContent = \"1−t/T* = \" + tau(s).toExponential(2);\n    zoomText.textContent = (follow.checked ? \"FOLLOW · \" : \"FIXED · \") + mag.toExponential(2) + \"×\";\n    ruler.textContent = \"grid Δ = \" + (0.5 / mag).toExponential(1) + \" initial units\";\n    slider.value = String(s);\n    slider.setAttribute(\"aria-valuetext\", \"s \" + s.toFixed(2) + \", remaining time \" + tau(s).toExponential(2));\n    play.textContent = s >= MAX_S ? \"Replay\" : playing ? \"Pause\" : \"Play\";\n    play.setAttribute(\"aria-pressed\", String(playing));\n  }\n  function wake() { if (!raf && !lost && !document.hidden) raf = requestAnimationFrame(frame); }\n  function frame(now) {\n    raf = 0;\n    const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;\n    last = now;\n    if (playing) { s = Math.min(MAX_S, s + dt * 0.24 * PARAMS.speed); dirty = true; if (s >= MAX_S) playing = false; }\n    if (auto) yaw += dt * 0.09;\n    const start = performance.now();\n    draw();\n    // Reduce geometry only after sustained expensive frames, never change seeds.\n    slow = performance.now() - start > 24 ? slow + 1 : 0;\n    if (slow > 45 && count > 480) { count = 480; dirty = true; slow = 0; }\n    if (playing || auto) wake(); else last = 0;\n  }\n  play.addEventListener(\"click\", function () {\n    if (s >= MAX_S) { s = 0.6; dirty = true; }\n    playing = !playing; last = 0; wake();\n  });\n  slider.addEventListener(\"input\", function () {\n    s = Math.max(0, Math.min(MAX_S, Number(slider.value))); playing = false; dirty = true; last = 0; wake();\n  });\n  follow.addEventListener(\"change\", wake);\n  reset.addEventListener(\"click\", function () {\n    s = motion.matches ? 6 : 0.6; yaw = 0.7; pitch = 0.34; count = COUNT; slow = 0;\n    playing = !motion.matches; auto = !motion.matches; follow.checked = true; pointer = null; dirty = true; last = 0; wake();\n  });\n  canvas.addEventListener(\"pointerdown\", function (e) {\n    if (pointer !== null) return;\n    pointer = e.pointerId; lastX = e.clientX; lastY = e.clientY;\n    canvas.setPointerCapture(e.pointerId);\n  });\n  canvas.addEventListener(\"pointermove\", function (e) {\n    if (e.pointerId !== pointer) return;\n    auto = false; yaw += (e.clientX - lastX) * 0.008;\n    pitch = Math.max(-1.2, Math.min(1.2, pitch + (e.clientY - lastY) * 0.008));\n    lastX = e.clientX; lastY = e.clientY; wake();\n  });\n  for (const name of [\"pointerup\", \"pointercancel\", \"lostpointercapture\"]) canvas.addEventListener(name, function () { pointer = null; });\n  motion.addEventListener(\"change\", function () { if (motion.matches) { playing = false; auto = false; } wake(); });\n  document.addEventListener(\"visibilitychange\", function () {\n    cancelAnimationFrame(raf); raf = 0; last = 0; if (!document.hidden) wake();\n  });\n  new ResizeObserver(wake).observe(document.getElementById(\"scene\"));\n  canvas.addEventListener(\"webglcontextlost\", function (e) {\n    e.preventDefault(); lost = true; cancelAnimationFrame(raf); raf = 0;\n    fail(\"WebGL context lost. Waiting for the browser to restore it.\");\n  });\n  canvas.addEventListener(\"webglcontextrestored\", function () {\n    try {\n      setup(); lost = false; dirty = true; last = 0; fallback.hidden = true;\n      for (const el of [play, slider, follow, reset]) el.disabled = false;\n      wake();\n    } catch (err) { fail(err.message); }\n  });\n  wake();\n})();\n```\n\n", "url": "https://wpnews.pro/news/show-hn-browser-viz-of-openai-s-spaghetti-navier-stokes-vortex-illustration", "canonical_source": "https://3d-retro.com/experiments/vortex", "published_at": "2026-09-08 20:28:33+00:00", "updated_at": "2026-09-08 20:54:42.173011+00:00", "lang": "en", "topics": ["ai-research", "ai-products", "computer-vision"], "entities": ["OpenAI", "Hacker News", "WebGL"], "alternates": {"html": "https://wpnews.pro/news/show-hn-browser-viz-of-openai-s-spaghetti-navier-stokes-vortex-illustration", "markdown": "https://wpnews.pro/news/show-hn-browser-viz-of-openai-s-spaghetti-navier-stokes-vortex-illustration.md", "text": "https://wpnews.pro/news/show-hn-browser-viz-of-openai-s-spaghetti-navier-stokes-vortex-illustration.txt", "jsonld": "https://wpnews.pro/news/show-hn-browser-viz-of-openai-s-spaghetti-navier-stokes-vortex-illustration.jsonld"}}