Skip to content

Repository files navigation

Operation Blackout

A first-person shooter that runs in a browser tab and is trying very hard not to look like it. WebGL2 and Three.js, no engine, no asset files of any kind — every texture, mesh, sound and skybox in these screenshots is generated by code at load time.

Golden hour

npm install && npm run dev     # http://localhost:5173 — click to capture the mouse

What it looks like

Every image below is a real capture from the running game on an Apple M4, taken by the screenshot harness in tools/, not a mock-up.

Lane midfield Down a lane toward the contested centre. Concrete with aggregate and air pockets, brick courses, window reveals — all procedurally generated PBR.

Combat moment Smoke, dust, and the HUD under load: compass, minimap contacts, objective markers with live ranges, killfeed, low-ammo state.

Interior lighting Interior bounce and contact shadows. Nothing here reads as unlit, which is harder than it sounds without baked lightmaps.

Material close-up Close range. Texel density, normal micro-detail and roughness breakup are the difference between "a wall" and "a grey box with a photo on it".


The interesting part: a critic that can't be talked round

The hard problem in a project like this isn't writing a GTAO pass. It's that you stop being able to see your own work. You look at a frame for the thirtieth time and it looks fine, because you've been staring at it while it got worse.

So the quality bar is enforced by tooling instead of opinion.

tools/shot.mjs — deterministic GPU capture

Launches real Chrome on the Apple GPU (verified ANGLE Metal, never SwiftShader — a software fallback silently invalidates every visual judgement), loads the game in a no-rAF mode, steps the simulation by hand so temporal effects converge identically every run, and captures. It fails loudly rather than producing a misleading frame:

  • reads Vite's error-overlay shadow DOM, so a broken build can never be screenshotted and graded as a bad-looking one
  • raycasts around the camera and refuses to shoot from inside geometry
  • explicit timeouts, because page.evaluate has no deadline and one hung system will otherwise wedge an entire run

Cameras anchor to Level.spawnPoints rather than literal coordinates — hardcoded positions are fine until the level regenerates and they end up inside a wall.

tools/analyze.mjs — metrics that argue back

Twelve measurements of the specific, measurable ways a hobby WebGL frame differs from a shipped one: fraction of the image sitting in flat untextured blocks, Laplacian detail density, shadow detail retention, highlight clipping, gradient banding, 1px edge aliasing, hue entropy, luminance-weighted saturation.

Each has an envelope, and each threshold is calibrated against frames judged by eye — in both directions. A threshold has to pass frames that look good and fail one that looks bad, or it isn't measuring anything. When five scenarios failed on saturation at once, the metric turned out to be wrong rather than the renderer: plain HSV saturation is unstable in shadow, so frames with deep shade scored as garish while looking muted. It's luminance-weighted now, and the regression frame it failed to catch is caught by hue entropy instead.

node tools/shot.mjs                       # capture all scenarios
node tools/analyze.mjs shots/*.png        # grade them
node tools/shot.mjs --params "bloom=0"    # A/B any effect in isolation

This is a floor, not a certificate. 8/8 inside the envelope means no known failure mode is present. It does not mean the frames look like Call of Duty. They don't, yet.


Rendering

A hand-written hybrid-forward HDR frame graph — no post-processing library.

Stage
G-buffer prepass MRT: view normals + roughness, screen velocity, linear depth
GTAO horizon-based, cosine-weighted, spatially denoised and temporally accumulated
SSR Hi-Z min-depth pyramid march, roughness-aware cone tracing, IBL fallback
TAA Halton(2,3) jitter, YCoCg variance clipping, closest-depth velocity dilation
Motion blur / DOF tile-max dilation; physical CoC with separate near/far fields
Bloom 6-mip Karis-average dual filter, thresholded scene-referred
Composite AgX (ACES and Khronos-neutral selectable), ASC-CDL, split tone, chroma rolloff
Final edge-only chromatic aberration, CAS sharpen, luminance-dependent grain, triangular dither

AO and SSR are not pasted over the finished image. A MaterialInjector patches every lit material at Three's aomap_fragment and lights_fragment_maps include markers, so GTAO multiplies indirect diffuse and specular occlusion only, and SSR replaces the IBL radiance term by confidence rather than adding on top of it.

Buffer inspector for when something looks wrong and you need to know why: ?rpdebug=ao|normals|velocity|depth|ssr|bloom|hiz

Atmosphere is physically-based scattering to a PMREM cube, raymarched clouds at half resolution with mip-aware sampling and temporal accumulation, froxel volumetrics with blue-noise jitter, and 4-cascade shadows with texel-snapped stable projections and PCSS contact hardening.

Materials are baked on the GPU at boot: domain-warped FBM, Worley aggregate, cavity-driven grime, edge wear, and triplanar/stochastic sampling to kill periodicity. Full PBR set per material — albedo, normal from a real height field via Sobel, roughness, metalness, AO.


Everything else

Physics — Rapier at a fixed 120Hz with interpolated visual transforms. Articulated ragdolls with realistic joint limits, Voronoi glass shatter, blast-impulse prop displacement. The ground is a heightfield, not a 180k-triangle trimesh.

Weapons — procedurally constructed from machined parts, with first-person arms and gloved hands. Real projectile ballistics: muzzle velocity, drop, drag, travel time, material penetration with energy loss, ricochet at shallow angles. Deterministic per-shot recoil patterns, so they're learnable.

Feel — acceleration/friction movement with slide, mantle and vault; gait-cycle weapon bob with footplants synced to the audio; trauma-squared camera shake driven by noise; critically damped springs throughout, no linear lerps.

Audio — every sound synthesised in the WebAudio graph. No files. Layered gunshot transients, procedurally generated convolution impulse responses per environment, distance-modelled air absorption, surface-dependent footsteps.

AI — runtime-voxelised navmesh with A* and string pulling, graded awareness rather than binary detection, squad blackboard with role assignment, suppression and flanking.

HUD — DOM, not in-scene sprites, so text is crisp at any resolution and costs no shader time.


Controls

Move · sprint WASD · Shift
Crouch · prone · jump Ctrl/C · Z · Space
Fire · aim Mouse 1 · Mouse 2
Reload · swap · weapons R · Q · 1 2
Grenade · melee · use G · V · F
Flashlight · inspect L · H
Scoreboard · pause Tab · Esc

Any key in src/core/Settings.js works as a URL parameter: ?preset=ultra, ?renderScale=0.75, ?hud=0, ?exposure=0.8.


Honest status

Built by parallel agents against the interface contract in ARCHITECTURE.md, reviewed against the rubric in CRITIQUE.md.

The three rendering-foundation subsystems completed their self-critique loops. The eight gameplay subsystems were cut short partway through, so their code is first-draft and has never been through visual iteration. It renders correctly and you can walk around it. Weapons, AI and audio are largely unverified in actual play.

Where it stands against the real thing: materials, tonal response and atmosphere are genuinely close. The weapon viewmodel still reads closer to plastic than machined aluminium, ground cover is sparse, and there is no character work in frame. Nobody would mistake a still from this for Modern Warfare — but it is no longer obvious in 200 milliseconds that it's a browser tab.

Round one, for reference:

Round one

Requires WebGL2 and a GPU that isn't pretending. ~2s boot for material baking, Rapier WASM init and level generation.

Code Statistics

Language Files Lines Blanks Comments Code Complexity
JavaScript 121 47,966 3,598 6,075 38,293 5,496
Markdown 3 613 130 0 483 0
HTML 1 47 0 0 47 0
JSON 1 20 0 0 20 0
Total 126 48,646 3,728 6,075 38,843 5,496
  • Estimated Cost to Develop (organic): $1,260,009
  • Estimated Schedule Effort (organic): 15.02 months
  • Estimated People Required (organic): 7.45
  • Processed: 1,897,519 bytes (1.898 megabytes)

Generated with scc on 2026-08-07

Repository analysis

Live report: repostats.app/r/binRick/fps — regenerate with the repostats MCP server (claude mcp add --transport sse repostats https://repostats.app/mcp/sse), then call analyze_repo / get_metrics / get_top_files.

Files · languages 126 · 4
Total lines 48,630
Code · comments · blank 38,837 · 6,066 · 3,727
Comment ratio 13.5%
Cyclomatic complexity 5,495
Repository size 1.90 MB
COCOMO effort 111.9 person-months · 7.45 people · 15.0 months
COCOMO cost to develop $2,462,305
LOCOMO cost to regenerate $27.02

That last pair is the one worth staring at. The same codebase COCOMO prices at $2.46M of human effort costs $27 in tokens to regenerate — a ratio of about 91,000:1. It was, in fact, generated by agents.

Most complex files

File Lines Code Complexity
physics/PhysicsWorld.js 1,252 968 309
ai/Navmesh.js 955 789 256
ai/Agent.js 1,125 911 222
fx/EffectsSystem.js 1,876 1,483 205
ai/AISystem.js 775 641 193
world/Buildings.js 1,200 951 191
player/PlayerController.js 905 683 173
player/Collision.js 665 517 166

The complexity ranking is a fair map of where the difficulty actually lives: collision and navigation dominate, and the two heaviest files are the ones that have to reconcile a continuous simulation with a discrete world.

COCOMO figures above are repostats' own model settings and differ from the organic-mode estimate in the scc block, which uses different constants.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages