Exporting a whole 3D game as one HTML file
Awaken exports a game as one self-contained HTML file. Double-click it, it runs - the renderer, the scene, the scripts, all inlined. It's a genuinely nice thing to hand someone: no install, no server, "here's a link." It's also, the first time you try it, 350 MB, at which point the browser tab makes a noise you didn't know browsers could make.
Most of that is geometry - textures arrive already compressed as WebP/PNG, so there's little left to squeeze there, but raw mesh data is fat, regular, and full of numbers at more precision than any pixel will ever show. So the pipeline is: shrink each number, drop the text, then let a wide-window compressor eat the repetition across the whole scene.
flowchart LR A["Scene + assets"] --> B["keep only
referenced assets"] B --> C["compact + round
scene JSON"] C --> D["textures →
WebP / PNG"] D --> E["quantise + pack
geometry (binary)"] E --> F["Zstd-19
(whole scene)"] F --> G["inline into
game.html"]
Quantise: a float you can't tell you lost
A vertex position is three float32s - 12 bytes. You almost never need that. Awaken maps each axis into a u16 over the mesh's bounding box:
q[i] = Math.round((p[i] - min) / (max - min) * 65535); // decode: min + q/65535 * (max-min)12 bytes → 6, and the error is extent / 65535 - for a 10 m prop, 0.15 mm. Normals get a nicer trick: they're unit vectors, so the third component is redundant - project onto an octahedron and store two int8s, 12 bytes → 2. There's one lovely footgun in there: a stock Math.sign(0) returns 0 and silently collapses an axis of the encoding, so you need a ±1-only sign. Net: ~32 bytes/vertex → ~12, before a compressor runs. And it's idempotent - a decoded value lands exactly on the grid, so re-saving a project never compounds the loss.
Text is the enemy; wide windows are the friend
Scenes started as JSON. JSON is a disaster for floats - an f32 promoted to f64 stringifies as "0.6000000238418579", eighteen bytes to say 0.6. So geometry goes into a small binary container (12-byte header, a JSON metadata block, one raw blob indexed by [offset, length] spans), 4-byte aligned so you can point typed arrays straight at it.
Then Zstd, level 19 - and here's the biggest single lever, which isn't a cleverer algorithm, it's window size. Gzip looks back ~32 KB; a scene reuses the same wall and barrel hundreds of times, megabytes apart in the stream, so gzip never sees two copies at once. Zstd's window is measured in megabytes and catches the lot.
The real geometry numbers, measured on a castle of 3,870 meshes / 5.4M verts:
| Stage | Size |
|---|---|
Raw mesh data (float32 binary) |
209 MB |
+ quantise (u16/i8) |
85 MB - 2.5× smaller |
| + gzip | ~30 MB |
| + Zstd-19 | ~15 MB - ~13.5× smaller than raw |
gzip → 30, Zstd → 15, same input. That gap is the whole window argument in two numbers.
The file:// twist (worth knowing before it bites you)
The bit that ate an afternoon: a game opened by double-clicking game.html runs from a file:// origin, which the browser treats as a unique, locked-down security origin - no fetch, no dynamically importing blob: URLs. So the scene bytes get base64-inlined into the HTML, and scripts are compiled to self-executing globals instead of imported modules (much more on that in post #5). Everything self-contained, nothing fetched, because the file's own origin trusts nothing - not even itself.
Next: the same game, but not in a browser
One file in a browser is step one. The plan (from post #1) is to ship that same build natively:
flowchart LR H["game.html
(self-contained)"] --> W["Browser"] H --> D["Desktop app
WebView in Electron/Tauri"] H --> M["Mobile app
WebView in React Native"]
The desktop shell can drop the base64-inlining entirely (a real filesystem means the scene can be a sidecar file, faster to load and easier to patch), and gets proper "Save"/"Open" dialogs and more memory. Mobile trades the compression budget differently again - bandwidth and storage matter more, thermals throttle the GPU. Same engine, same exported content, three packagers around it. The single-file build is the hard mode; everything native is a relaxation of it.
📷 Screenshot needed: the Export dialog / progress, and/or an exported
game.htmlrunning standalone (ideally opened from the desktop, not a dev server).