Skip to content
 
 

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EXILUM

An isometric action RPG that runs in the browser. Three.js, no game engine, and no art assets — every texture, mesh, animation, and sound is generated procedurally at load time.

▶ Play it now, no download: exilum.vercel.app

Built with Dimension + Claude Opus 5 — one prompt fanned out into 14 parallel subagents (rendering, procedural geometry, combat, loot, audio, VFX…), coordinating over an IRC channel and iterating against blind A/B comparisons with real Path of Exile 2 frames.

npm install
npm run dev        # http://localhost:5188

Controls

input action
W A S D move — camera-relative, so W is always "up the screen"
left-click attack (Cleave)
right-click (hold) walk to cursor
Q F E R Fireball · Storm Lance · Ground Slam · Void Beam
1 2 3 4 Frost Nova · Caustic Field · Blink Step · Hex Seeker
Space dodge roll (i-frames)
mouse wheel zoom · I inventory · F3 debug overlay

Follow the compass at the bottom of the screen to The Sunder, the boss arena. Kill Ordolth, the Sundered to win. Die and you start over. R restarts from either end screen.

What's in here

A complete game loop — generate a dungeon, fight through it, find loot, level up, beat a three-phase boss — built on a from-scratch engine layer.

Rendering. Cascaded shadow maps, ACES filmic tonemapping, GTAO ambient occlusion with a separate contact-AO pass, SMAA, god rays, bloom, coloured aerial-perspective fog, an image-based-lighting ambient term, and a screen-space cutaway that punches a dithered hole in any wall standing between the camera and the player.

Procedural content. A seeded dungeon generator (7 room types, walls/columns/props/decals), a stone atlas with albedo + normal + ORM maps painted on canvas, a skinned character rig with hand-keyframed animation clips, four enemy archetypes plus a boss, and an item system with affixes and rarity tiers.

Simulation. Fixed-timestep loop with interpolation, capsule-vs-grid collision with per-axis sliding and step-up, a spatial hash for neighbour queries, ragdolls, a flow-field pathfinder shared by every enemy, and an encounter director that composes packs into readable formations rather than spawning them at random.

Audio. Synthesised through the Web Audio API — no sound files.

The interesting part: it was built against measurements, not vibes

The renderer was tuned over 13 rounds against a numeric rubric, with blind A/B comparisons against real Path of Exile 2 frames judged by agents who didn't know which side was which. tools/analyze.mjs is a zero-dependency frame analyser (it decodes PNG by hand) that reports exposure, colour balance, step-energy distribution, and structure metrics.

PoE2 still wins those blind comparisons 4/4. That was never really in doubt — it's ~100 developers × 7 years on a proprietary engine against procedural generation in a browser. What the process produced instead is a precise account of why, and most of the value in this repo is in MEASUREMENT_NOTES.md (220KB of it): the wrong turns, the metrics that had to be thrown away, and the bugs that only measurement could find.

A few examples of what that turned up:

  • A protocol gate was 96% brightness. localContrast moved +55.2% under a pure exposure change, and correlated with mean luminance at +0.963. Agents had been "fixing" it by adding light for rounds. On the exposure-invariant form the verdict reversed completely.
  • Fine detail was already at reference parity (1.05×); the deficit was large steps — 5.5× short. "Reads as pasted-on" is what a frame looks like when objects lack strong luminance steps at their boundaries. Adding more texture would have moved a metric already at parity.
  • Three judges called the floors "corduroy striped" and two separate fixes failed — because every instrument measured luminance anisotropy, which was correctly in range. The defect was in chroma directionality, which nothing had ever measured.
  • The ACES inflection sits at linear input x = 0.1203. Below it the tonemap expands scene steps; above it it compresses them. Three of four camera stations were operating past it, unmeasured for thirteen rounds.
  • A "+32% fps" win didn't reproduce — it was monster-count drift between A/B legs. Pinning the roster took the noise floor from ±14% to ±0.4%, which retroactively invalidated several earlier "wins".
  • The character-not-centred bug was devicePixelRatio. setSize(w, h, false) skips the canvas CSS size, and a canvas's attributes are its intrinsic size, so on a Retina display it rendered 2× too large and you saw the top-left quarter. Three of my own tests agreed it was centred — all three measured canvas space, not screen space.
  • 2.4× came from lights that were mathematically contributing nothing. A PointLight with decay = 2 attenuates to exactly zero at its distance, so one whose influence sphere misses the frustum cannot alter a pixel — skylight-arena has reach 28.1 and sat 98.3 world units away. 14 of 23 were like that. Culling them: 42 → 102 fps, and 0 of 9,072,000 pixels differ across all seven rooms. The catch is that Three.js keys its shader cache on the light count, so a free-running cull recompiled every material continuously — worst frame 4,295 ms. Quantising to five prewarmed rungs fixes it.
  • A depth prepass reported a 12.5ms win that was an empty screen. Overdraw genuinely measures 2.32×, and the prepass "saved" more than 2.32× could possibly yield — the tell. The pixel diff found 90.7% of pixels changed and the whole dungeon missing: depth went to the canvas while the composer renders into its own framebuffer, so EqualDepth rejected everything. Done correctly it loses 1.6ms.
  • PCFSoftShadowMap is deprecated in three 0.185 — it silently becomes PCFShadowMap and sets needsUpdate on every material in the scene. Asking for the nicer filter bought the same filter plus a guaranteed full-scene recompile. Found with a setter tripwire on shadowMap.type.
  • Ambient occlusion was switched off in the shipped build and nobody noticed: a medium quality default disabled GTAO to buy frame rate. It's back on, and 2.3× faster than medium used to be.

Layout

src/core/      engine, fixed-step clock, input, physics, collision, spatial hash, ragdoll
src/render/    renderer pipeline, sky + lighting, camera, wall cutaway, light culling, shaders
src/world/     dungeon generator, level builder, props, terrain, decals
src/entity/    player rig, skeletal animation
src/combat/    damage model, skills, projectiles, particles, VFX
src/game/      enemy AI, encounter director, loot, items, affixes, set-pieces
src/gen/       procedural materials, character/enemy/prop/sprite generators, noise
src/ui/        HUD — orbs, skill bar, minimap, inventory, boss bar, FPS meter, end screen
src/audio/     Web Audio synthesis
tools/         frame analysers and capture harnesses

refs/ (Path of Exile 2 press screenshots used as measurement targets) and shots/ (capture output) are gitignored. They're Grinding Gear Games' material and not ours to redistribute — everything here was measured against them and contains none of them.

Performance

Measured at 1568×1015 on a Retina display (DPR 2), rAF-clocked, real keyboard and mouse input:

mean p50 p90 p99
exploration 101 fps 100 96 89
combat, ≤16 lights in frustum ~70 fps
combat, 28–34 lights in frustum 22–38 fps

Before this work the same exploration scene ran 42 fps. The gain is point-light frustum culling (src/render/lightcull.js), and it is pixel-identical — 0 of 9,072,000 pixels differ across all seven rooms, because a decay = 2 PointLight attenuates to exactly zero at its distance and culling one whose sphere misses the frustum is arithmetic, not approximation.

Ambient occlusion is on (it had been silently disabled by a medium quality default) and nothing is ever dropped by defaultF3 shows lights dropped, which reads 0.

The floor case is a dense fight with several spell VFX alive at once, where 28–34 lights are genuinely in frustum. Every one contributes, so the default keeps them all. If your GPU needs the headroom, set graphics.lightBudget in src/core/config.js (try 20): +78% in that frame, at a measured 8.1 dimmest lights dropped per frame.

These numbers are a floor, not a forecast. They come from a headless software rasteriser (SwiftShader) with no GPU, and the per-light cost is 100% fragment work — 0.585 ms/light at full resolution, 0.087 at quarter resolution, fixed CPU component ~0. That is precisely the work a real GPU spreads across hundreds of cores.

Docs

  • MEASUREMENT_NOTES.md — the full record. Every metric, confound, and correction.
  • STATE.md — current state and what's next.
  • CAPTURE_PROTOCOL.md — the six conditions that make two frame measurements comparable.
  • POE2_RUBRIC.md — the numeric targets.
  • NEXT_ROUND_ITEM_1.md — the highest-value open defect, diagnosed down to the line of code.

Licence

MIT. Path of Exile 2 is a trademark of Grinding Gear Games; this project is unaffiliated and uses their published screenshots only as private local reference during development.

About

Stateloop publishing fork of Sonali-Sharma-tech/exilum. Original game and MIT license by its upstream authors.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages