Ornith 9B —
why the run failed
The Ornith 1.5 9B 8-bit build never gets past its own "Descending…" loading screen. No frame is ever rendered. This is the full post-mortem: what crashed, where, and why the smaller model's architecture choices both helped and doomed it.
An ambitious, modern Three.js build — that was never run.
The script crashes with an uncaught exception during initial scene construction, before the first frame. Because the loading overlay is only faded after the first successful render, the page sits on "Descending…" forever, with no error surfaced in the UI. Patching the first crash immediately reveals a second fatal crash in the animation loop. Two non-fatal logic bugs are hiding behind those.
deckWidth referenced out of scope
Console output when loading the file (headless Chrome, console piped to stderr):
Uncaught ReferenceError: deckWidth is not defined (line 559)
deckWidth is declared as a const inside makeBridge():
function makeBridge() {
// ...
const deckWidth = 4.6; // line 507 — function-scoped to makeBridge()
// ...
const lantern = makeLantern(z); // line 550 — calls a *separate* function
}
…but referenced inside makeLantern(), a different top-level function where it does not exist:
function makeLantern(z) {
const g = new THREE.Group();
const lx = rand(-deckWidth * 0.35, deckWidth * 0.35); // line 559 — ✕ ReferenceError
makeBridge() is invoked at module top level (line 581), so execution dies right there. Everything after — kelp, coral, jellyfish, fish, bubbles, rays, the moon aperture, the composer, the animation loop — never runs.
Why the loader never clears: the "Descending…" overlay is only hidden after the first rendered frame:
// Fade out loading screen once the first frame renders. (lines 973–978)
renderer.render(scene, camera);
requestAnimationFrame(() => {
loadingEl.classList.add('hidden');
animate();
});
Line 974 is never reached, so the overlay stays up indefinitely. A nice pattern — but it also means the failure is completely silent in the UI.
userData.path shape mismatch
With bug 01 patched in a scratch copy, the next error fires every frame:
Uncaught TypeError: Cannot read properties of undefined (reading 'speed') (line 916)
makeFish() stores the swim-path object directly on userData:
const path = { cx, cz, radius, height, speed };
g.userData = path; // line 740 — userData IS the path
…but the animation loop reads it through a .path property that was never set:
fishes.forEach((f) => {
const p = f.group.userData.path; // line 915 — undefined
const a = t * p.speed + p.cx; // line 916 — ✕ TypeError
});
Either g.userData.path = path (write side) or const p = f.group.userData (read side) would fix it. As shipped, the two sides of the data contract disagree.
Non-fatal bugs hiding behind the crashes
Inverted gap guard line 549
if (z > gapEnd && z < gapStart) continue; — gapStart (≈17.1) is always less than gapEnd (≈22.9), so this condition can never be true. The guard is dead code and lanterns spawn across the broken section of the bridge. Intended: z > gapStart && z < gapEnd.
Pause button doesn't pause line 960
if (!paused) controls.update(); — the flag only gates the OrbitControls damping update. Every animation (jellyfish, fish, kelp, bubbles, dust, rays, lanterns, lights) keeps running while the button claims "Resume".
Per-frame RNG in the light-ray update line 949
r.mesh.position.set(…, rand(-80, 30)) re-rolls a random z-position every frame, so the god-ray cones teleport instead of swaying smoothly. The seeded RNG was clearly meant for build-time placement.
Dead instances in coral / sea-fan loops lines 631, 652
continue skips setMatrixAt for positions near the temple approach, leaving those instance slots zero-initialized — they collapse to a degenerate point at the origin. Invisible, but wasteful and sloppy.
Style smells
The lantern bulb material is created twice (lines 568–569), and makeLantern() adds itself to the scene (line 576) only to be re-parented into the bridge group by its caller (line 551) — works by accident because Three.js silently re-parents.
9B 8-bit vs. the working 35B 4-bit run
| Ornith 1.5 9B 8-bit (this run) | Ornith 1.5 35B 4-bit MoE (working) | |
|---|---|---|
| Module system | ES modules + importmap, Three 0.160.1 + addons | Classic script, global THREE r128 |
| Code structure | Many small top-level functions, const/let block scope | One giant IIFE, var, single shared scope |
| Post-processing | EffectComposer + UnrealBloomPass | None (additive glow materials instead) |
| Camera | OrbitControls (damped auto-orbit) | Hand-rolled spherical orbit + drag/zoom |
| Randomness | Seeded RNG (mulberry32) — deterministic scene | Math.random() — different every load |
| Extras | FPS counter, gradient shader dome, caustic shimmer plane | Focus button, touch pinch zoom |
| Pause behavior | Broken (gates camera only) | Correct — wraps all updates |
| Result | Crashes before first frame | Runs |
Where 9B's approach was genuinely better
- Modern stack done right (on paper) — ES modules, import maps, the official addons for OrbitControls and UnrealBloom. This is how a Three.js project should be written in 2026; the 35B file uses a 2021-era global build.
- Determinism — the seeded RNG means every visitor sees the same scene. The right instinct for a prompt test artifact.
- Performance hygiene — instanced coral and sea fans, shared box geometry, glow sprite textures instead of extra lights.
Where 35B's approach saved it
- One shared scope — every helper lives in the same IIFE;
lanternXs,lanternLights,doorLight,chasmLightare declared once at the top and visible everywhere they're used. The 9B file's split into many small functions is architecturally nicer — but the model lost track of which variables live in which scope, which is exactly what killed it (deckWidth). - Simpler moving parts — no post-processing chain, no module graph, fewer cross-function data contracts; fewer places for a shape mismatch like
userData.pathto hide.
9B wrote the better blueprint; 35B shipped the working product. The prompt test measures shipped products.
The 9B output contains two independent fatal runtime errors that any single execution — even just opening the file once — would have exposed instantly. Neither bug is subtle; both throw on the very first code path. The 9B model produced a convincing looking program (good comments, sensible structure, considerate loading UX) without ever checking that it works. For a prompt test whose prompt says "actually generate the complete working HTML file," that's a failed task regardless of architectural taste.