Skip to content

Draw the Matrix ourselves: WebGPU and WebGL 2, and no three.js - #6

Merged
h1ddenpr0cess20 merged 3 commits into
mainfrom
claude/hopeful-lovelace-5qatt1
Oct 2, 2026
Merged

h1ddenpr0cess20 merged 3 commits into
mainfrom
claude/hopeful-lovelace-5qatt1

Conversation

@h1ddenpr0cess20

@h1ddenpr0cess20 h1ddenpr0cess20 commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

What changed

three.js is gone. src/gpu/ is a small renderer written for these games, with two backends: WebGPU (shaders in WGSL) and WebGL 2 (shaders in GLSL). The shaders are separate files in src/gpu/shaders/ (*.glsl, *.wgsl). The engine is the same file set in all three eggs.

  • Which backend draws comes down to antialiasing. WebGPU only does 4× multisampling. Chrome on a desktop GPU gives WebGL 8×, which is what the three.js build was drawn with. So:
    • where WebGL can do more than 4×, WebGL draws;
    • elsewhere (most phones), WebGPU draws;
    • either falls back to the other if it won't start;
    • ?renderer=webgl or ?renderer=webgpu in the URL forces one.
  • It looks the same. I rendered the old three.js build and the new one headlessly, frame by frame down the same seeded run. WebGL matches within 1/255 on every frame. WebGPU differs only by a pixel here and there along the lines and the egg's edge.
  • It plays the same. src/core/ is untouched, and so are the camera rig, pools and timings.
  • The bundle is smaller: about 56 kB of game JS plus whichever backend loads, instead of three.js.
  • Lost GPUs. If the browser drops the WebGL context or WebGPU device, the renderer rebuilds itself instead of leaving a black screen.

How it works

  • The wireframe is native one-pixel lines, additive, faded toward the black by the fog before they're added. The deck under them is a black fill, pushed back slightly in depth and drawn first among the see-through things. Both are the same as before.
  • The rain is a canvas stretched over the whole frame behind everything, at 0.32 brightness. It's re-uploaded with fresh mipmaps on each of its 18 ticks a second, as before.
  • Marc's shell keeps three r185's physical lighting, term for term: GGX with multiscatter compensation from the same DFG table, Charlie sheen, clearcoat, bump map, the studio's hemisphere light and two keys. His studio environment is prefiltered on the CPU in a worker, with three's PMREM ported exactly. The first frame waits for it.
  • frame.js makes the decisions, once per frame. It culls, sorts and packs every uniform into vec4/mat4 blocks that std140 and WGSL lay out identically, so the two backends can't disagree.

One quirk is preserved on purpose: three takes a hemisphere light's "up" from the light's world position. The studio travels with the egg, so its "up" tilts toward +z as you run. That's part of how the game looks today, so the new renderer copies it.

Testing

  • npm test: 110 pass. The new test/gpu.test.js checks the GLSL blocks and WGSL structs against frame.js, shape winding, the prefilter, draw order and per-material animation. The rig test now projects through the new camera.
  • npm run lint is clean, and npm run build succeeds.
  • Pixel diffs against the three.js build in headless Chromium (SwiftShader, both backends), as above. That diffing caught one real bug: the WebGPU backdrop bind group was missing two texture slots, and only this game has a backdrop. It's fixed in the engine everywhere.
  • Backend choice was tested by faking an 8-sample GPU: WebGL is chosen. At 4 samples WebGPU is chosen, and ?renderer=webgpu overrides.
  • Not tested: real GPUs and real phones. Headless SwiftShader is all I had.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DAsxgyQ3z7SBtRq9X4N3R7

claude added 2 commits October 2, 2026 00:56
src/gpu/ is a renderer written for these games: a scene graph, three
shaders (lit, unlit, backdrop), and two backends. WebGPU is tried first,
WebGL 2 is the fallback, and ?renderer=webgl|webgpu forces one. The
shaders live in src/gpu/shaders/ as .glsl and .wgsl and are the same
programs twice. frame.js culls, sorts and packs every uniform once a
frame into vec4/mat4-only blocks that std140 and WGSL lay out
identically, so the backends only issue draws and cannot disagree.

The wireframe is native one-pixel lines, additive, fogged into the black
before they are added; the deck fill keeps its polygon offset and its
place at the front of the see-through queue; the rain is a full-frame
backdrop at 0.32. Marc's shell keeps three r185's physical lighting term
for term — GGX with multiscatter compensation off the same DFG table,
Charlie sheen, clearcoat, the studio's hemisphere and two keys — and the
studio environment is prefiltered on the CPU in a worker, same cube-UV
layout and GGX VNDF sampling as before.

Checked by rendering the old and new builds headlessly from the same
seeded run: WebGL agrees within 1/255 on every frame; WebGPU differs only
by a pixel here and there along the lines and the egg's edge.

test/gpu.test.js checks the GLSL and WGSL blocks against frame.js, shape
winding, the prefilter on a uniform world, draw order and per-material
animation; the rig test projects through the new camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAsxgyQ3z7SBtRq9X4N3R7
A phone reclaiming memory from a backgrounded tab, or a driver reset,
takes the WebGL context or the WebGPU device with it. The renderer now
stops drawing when that happens and builds a fresh backend on the same
canvas when it can: on webglcontextrestored for WebGL, straight away
for a lost (not destroyed) WebGPU device. Everything it draws is
re-uploaded from the scene and the canvases on the next frame.

Checked with WEBGL_lose_context: lose, keep calling render, restore —
no errors, and the frame after comes back the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAsxgyQ3z7SBtRq9X4N3R7
@vercel

vercel Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
eggscape Ready Ready Preview Oct 2, 2026 1:17am UTC

WebGPU multisamples at 4x and no more. Chrome gives a WebGL canvas 8x
on a desktop GPU, and that is what the three.js build was drawn with —
so on a desktop the WebGPU backend came out with jaggier edges and
small bright things (the pickups) a little less glowing than before.

The renderer now asks a throwaway WebGL context how many samples it
could take: more than 4 and WebGL draws, otherwise (most phones) WebGPU
does. Either falls back to the other if it will not start, and
?renderer=webgl|webgpu still forces one. The WebGL backend makes the
same calls as three did on the same API, so on a desktop the picture is
the one it always was.

Checked headlessly by faking an 8-sample GPU: WebGL is chosen; at 4
samples WebGPU is; ?renderer=webgpu overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAsxgyQ3z7SBtRq9X4N3R7
@h1ddenpr0cess20
h1ddenpr0cess20 merged commit f88f060 into main Oct 2, 2026
6 checks passed

This branch was successfully deployed

1 active deployment
Preview — a4459d27 Deployed Oct 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants