Two AI coding agents were given the same brief: build a hot-air balloon flight over late-winter Cappadocia that runs in a browser and looks as close to Unreal Engine 5 as a laptop allows.
| Astra (OpenAI Codex) | Opus (Claude Opus 5.5) |
|---|---|
![]() |
![]() |
Both builds are live at shiphrahx.github.io/ascent.
The experiment compares how each agent approaches an open-ended real-time graphics problem: the techniques it picks, how it measures its own work, and how it reports what's still missing.
Each agent built its version from scratch, then worked through two rounds of fidelity plans. Every plan follows the same loop:
- a review of what looks wrong, from the code and from captures of the running game
- phased tasks, one commit per task
- before and after captures from the same camera and hour
- GPU cost and frame pacing measured after every step
- a ranked list at the end of what's still short of the UE look
The plans and results: Astra/FIDELITY_PLAN.md, Astra/FIDELITY_PLAN_2.md, Opus/FIDELITY_PLAN.md, Opus/FIDELITY_PLAN_2.md and Opus/PERF_FIX.md.
- One standalone HTML file, every asset embedded, no network requests.
- three.js on WebGL2, bundled with esbuild.
- Procedural world: terrain, fairy chimneys, river, village, vegetation, a fleet of balloons, birds and horses.
- A timelapse day cycle from sunrise to night, plus a snowstorm.
- Three quality tiers (
high,medium,lite).mediumhas to hold 60 fps on an AMD Radeon 890M integrated GPU.
Astra builds on three.js. It extends the standard PBR materials through onBeforeCompile and uses three's addons for the main systems: CSM for cascaded shadows, Sky for scattering, Reflector for the river, EffectComposer and UnrealBloomPass for post. Its own passes add temporal filtering, histogram exposure, shadow history and terrain horizon fields.
Opus replaces most of three.js's rendering. Raw ShaderMaterials share a hand-written GLSL library, and each frame runs through an explicit pipeline:
sky LUT → terrain shadow heights → shadow cascades → jittered HDR scene
→ AO → volumetrics → transparents → temporal upsample → post
three.js is used mainly for buffers, render targets and draw calls.
| Area | Astra | Opus |
|---|---|---|
| three.js | r180 | r170 |
| Anti-aliasing | 4× MSAA on scene geometry, plus a temporal filter with depth reprojection and neighbourhood clipping (after Playdead's GDC 2016 talk). No FXAA. | Jittered TAA with variance clipping in YCoCg, then temporal upsampling to display resolution |
| Resolution | Fixed pixel budget per tier, native at 1440×900 | Dynamic resolution; targets allocated once and drawn into a viewport so TAA history survives scale changes; frame pacing locked to the refresh rate |
| Sky | three's Sky model, with a cloud ray-march (96 steps) cached in a 4096-wide texture and refreshed in tiles |
Physically based sky LUT (Rayleigh, Mie, ozone, planet shadow), plus moon, stars, Milky Way, 22° halo and sun dogs |
| Clouds | Part of the cached sky texture; their shadows move across the ground | Separate pass at a third of render size on high: Worley erosion, Beer and powder terms, multiple-scattering octaves, temporal accumulation |
| Atmosphere | Aerial perspective coloured by the sky model, replacing three's grey FogExp2 |
Closed-form RGB aerial perspective from the LUT's coefficients; quarter-res ray-marched light shafts and snowstorm haze |
| Shadows | CSM, 3 cascades out to 1.9 km, 4096 near map on high, fixed Poisson-disk filter, reprojected shadow history blending on lower tiers. Distant terrain shadows from a directional horizon field in a 3D texture. |
Own cascades (4 on high) fitted to frustum-slice spheres and texel-snapped to a fixed anchor, PCSS with 16 blue-noise taps, a 2048×1024 heightmap shadow with distance-based penumbra, screen-space contact shadows |
| Indirect light | Terrain sky visibility from horizon angles, blurred ground radiance as a single bounce | GTAO-style sky visibility over the heightmap, a 512×256 ground bounce map with mips, SSAO on indirect light only |
| Materials | Fully procedural: seamless maps generated at build time from seeded noise fields, rock strata in world space with analytic filtering | CC0 Poly Haven photo scans packed into 1024px JPEGs (albedo with baked AO, normal with height), triplanar on steep faces, parallax occlusion within 60 m, specular AA |
| Terrain | Geological fields for strata terracing, drainage erosion and talus; far terrain as LOD rings | 2.5 m core heightmap; on high a 1.25 m relief built on the GPU at load with gullies, strata ledges and scree |
| Water | three's Reflector |
Mirrored-camera reflection with simplified shading, rendered every frame |
| Tone mapping | AgX, chosen over ACES because it keeps detail in snow highlights | UE5's filmic curve at its default settings |
| Exposure | Histogram auto exposure | Histogram eye adaptation plus local exposure |
| Post | Bloom with no threshold, vignette on high, no motion blur or grain |
7-level bloom, motion blur, close-up depth of field, edge chromatic aberration, film grain, heat shimmer |
| Draw calls | Rigid balloon, bird and horse parts batched into instances (362 meshes in 26 batches), 22% fewer draws | Instanced rocks, vegetation and village, with dithered LOD cross-fades |
| Test harness | Playwright on Chromium | puppeteer-core driving Edge |
| Source size | ~217 KB | ~500 KB |
| Output | Astra/Ascent.html, 8.0 MB |
Opus/cappadocia.html, 6.3 MB |
Astra runs the production shader functions against known inputs in GPU fixtures, and stamps every report with the build hash so stale numbers are rejected. Its light-stability tool runs a spectrum over frame luminance, which found the burner light pulsing at 3 Hz.
Opus reprojects every pixel through depth, the same way TAA does, and scores the second difference along that track against a flat-grey control. It splits the score by source (balloon, terrain, village, sky edges) and switches features off one at a time. This showed SSAO noise, not shadows, caused most of the flicker in flight.
Each build's numbers come from its own write-up. The methods differ, so they aren't a direct comparison.
Astra on the Radeon 890M at 1440×900:
mediumandlitehold 59.9 fps with a p99 of 17 ms.highon the 890M runs at 41 fps.- Landscape flicker fell by about 20% on
medium, short of the 70% target. - Remaining gaps: landform geometry, material scale, subpixel stability.
Opus at 1600×900:
hightakes 9.9 ms of GPU time on an RTX 5070 Ti laptop.mediumtakes 15.5 ms on the 890M and holds 60 fps at 0.9 render scale or above.- Shadow flicker in flight fell to 0.10 / 0.04, against a target of 0.30 / 0.20.
- Remaining gaps: village and crew models, richer GI, cliffs that are still a heightfield.

