CRT Filter
The key words MUST, MUST NOT, MAY, and REQUIRED below are used as in RFC 2119.
An optional full-screen post-process that makes the game look like it is being played on a CRT. It is purely cosmetic: it MUST NOT change gameplay, timing, input, or any value the game reads back — it only changes what reaches the glass.
Modes
The setting is a three-state cycle, not a boolean. Off is the default.
- Off — no filter. The overlay MUST be hidden (not merely transparent), because a transparent canvas item still forces the per-frame back-buffer copy the shader needs. Off MUST cost nothing.
- Scanlines — horizontal scanlines and a light vignette, with brightness raised to pay back the energy the scanlines remove. No geometric warp. This is the conservative preset and the one to pick on a phone: because it does not move any pixel, on-screen control buttons stay exactly where a tap registers.
- Full — Scanlines plus an aperture-grille mask, barrel curvature, chromatic aberration, halation, and a stronger vignette.
Scope — what goes through the tube
The filter treats the composited frame. The 3D world, every HUD canvas, the start menu, and the SceneManager fade MUST all pass through it — a UI layer floating crisp and unwarped above a curved image gives the illusion away. Concretely, the overlay MUST sit above every other CanvasLayer in the project (today: layer 260, above the fade at 250).
Curvature MUST NOT crop
Barrel distortion pushes the corners of the image outside the sampled area. Rather than let that clip content, the warp MUST be rescaled by exactly its own corner factor, so the corners land back on the frame edge. No pixel that the game drew is lost — which matters because the HP/PP panel, the minimap and the action palette all live in corners.
The cost of that guarantee is a thin black band at the middles of the edges, which reads as the tube's bezel. Full therefore keeps its curvature small; a larger value would trade a wider bezel for a bulge nobody asked for.
Resolution independence
Scanlines are counted against a fixed virtual line count (240), not against physical pixels, so the look is identical at 720p and at 4K instead of the lines dissolving into the pixel grid on a large display. The aperture grille is the opposite: it is measured in physical pixels (3px RGB triads), because a grille is meant to be a fine texture, not a visible stripe pattern that scales up with the window.
Persistence
The mode persists to user://video_settings.cfg under [video] crt_mode, as one of the string ids off / scanlines / full. It is stored as a string rather than an enum ordinal so that reordering the modes can never silently reinterpret a saved config as a different filter.
This is a video setting, so it belongs to the machine, not to the character: it MUST NOT ride along with SaveManager.save_game() and MUST survive deleting or switching characters. An unrecognised crt_mode id (hand-edited, or written by a build with a different mode set) MUST fall back to Off.
Where it is suppressed
- Autopilot (
PSZ_AUTOPILOT) — the filter MUST stay Off. The matrix greps its own rendered frames, and the back-buffer copy perturbs the frame timings it measures. - Headless — nothing to filter.
Implemented by
scripts/autoloads/crt_filter.gd— theCrtFilterautoload: mode enum, the preset table, persistence, and the overlayCanvasLayer.scripts/shaders/crt.gdshader— the single canvas-item shader. Every mode is a set of uniform values inCrtFilter.PRESETS; a new look is a preset entry, not a second shader.scripts/3d/field/pso_start_menu.gd/start_menu_input.gd— the CRT Filter row in Options.
The shader file MUST stay under res://scripts/. It is reachable from an autoload, and autoloads must compile before the asset pack mounts — see the compile contract in Field Stage Lifecycle.