HelpImStuckInAFractal

Help I’m Stuck In A Fractal

A from-scratch Godot 4.6 Mandelbox viewer that emulates the renderer at icefractal.com/mandelbox/: the same default view, the four shape sliders, the Ice Fractal material, the precision control, Julia mode and an orbit camera. On top of the site it adds three more fractals — a kaleidoscopic IFS (the Menger sponge, the Sierpinski tetrahedron and their kin) and a pseudo-Kleinian limit set, ported from Fractacular, and the Mandelbulb — and cylinder, octahedral and abs-fold hybrid folds for the Mandelbox, a distance-scaled free-fly camera, a resolution governor with automatic load shedding currently paused, a scene of shapes (up to eight fractals, each with its own knobs, position, rotation and size, so they can be nested inside one another; see Shapes and nesting), and a console window — a collapsible Shape panel per shape with every fractal knob and Fractacular’s binding model, Light, Material and Environment tabs with editable material gradients and presets, up to four lights per shape (suns, glowing lamps placed in the world, lamps riding with you) with shadows, the atmosphere and the camera’s scale cues, and noise-driven texture and bump, and a Movement pane of virtual axes driven by key pairs. It is built on the GL Compatibility renderer (WebGL 2) so it is web-exportable.

GDScript only — no C#, even though the Godot build on disk is the Mono build.

Run

./run.sh            # run the project with the call centre
./run.sh --demo     # straight to the cockpit, with live geometry navigation
./run.sh --editor   # open it in the Godot editor instead

run.sh always delegates to call_center/run.sh, forwarding all arguments. That launcher finds godot4 on your PATH (or falls back to Godot_mono.app / Godot.app), refreshes Godot’s script-class cache when a script has changed, and then launches the main scene: the Icebox Nav title menu (Play Game, Settings, Quit; Quit is hidden in a web build). Set GODOT=/path/to/godot to use a different binary.

Godot runs as a background child without terminal input. The launcher stays attached for logs; Ctrl+C stops the game (or editor) and returns to the shell.

Probe runs (a window that never takes focus)

Godot’s headless mode draws no pixels, so looking at a real frame needs a window. A probe run keeps that window harmless: -- --probe (or FRACTAL_PROBE=1) makes ProbeMode (src/session/probe_mode.gd) skip every focus grab, keep the mouse visible instead of captured, keep the console embedded in the main window, and flag the main window unfocusable. Add --quit-after=SECONDS and the run ends itself. Launched through open -g the window appears behind whatever is in front and stays there:

tests/probe_main.sh          # Main in probe mode for 10 seconds, no focus taken
tests/probe_main.sh 30       # for 30 seconds
./run.sh -- --probe --play --quit-after=5   # the same through the launcher (a tty launch
                                            # lets Godot activate itself once; see tools/launch.sh)

--background-window (see SessionLaunch) still parks the window off screen and combines with --probe.

The console opens alongside the title menu, with the selected build’s confirmed save loaded. Edit shapes or load a different level before pressing Play Game. Play keeps those edits, including unsaved changes. Movement, time bindings and the phone agent’s flight session start only after Play; ` (backtick) can reopen a closed console.

Trailer capture

./trailer.sh                    # the last saved level, fullscreen on the screen the mouse is on
./trailer.sh saves/final.json   # that save (also: final.json, final, res://saves/final.json)
./trailer.sh final --display 1  # on screen 1 (./trailer.sh --list-displays shows the indices)
./trailer.sh --dry-run final    # print the Godot command instead

Spellgraft’s trailer capture mode, ported. The game opens fullscreen at the screen’s native resolution and goes straight into the level, ready to fly. The console stays shut and the pilot chat is hidden; Tab shows the console and, during a connected call, the pilot chat (and hides them again). **** opens just the console, a Ctrl tap shows the hotkey card, **[** / **]** cut between the level's shapes, and **H** hides every overlay and HUD for a clean frame. **L** fixes the frame rate for a take: the game caps itself at 30 fps and renders every frame at full resolution and full quality, with no load shedding (no lower render scale, performance fog, cheaper detail), so footage runs slower but evenly instead of changing look as the scene gets expensive. L again releases the frame-rate cap; automatic load shedding remains paused. It works in any run, not only a trailer capture. Nothing is saved: Cmd+S does nothing, and the window's place is not remembered. In game this is – –play –trailer –level=PATH (TrailerMode, src/session/trailer_mode.gd); –level=PATH` opens that level in any run. Capture the whole display in OBS.

Controls

Tap Ctrl for the hotkey card, a reference to every key the game listens for, drawn over the view (tap again or press Escape to hide it). Its key names come from the InputMap and the level’s axis keys, so rebinding keeps it right.

Press ` (backtick) to open the console, a separate OS window: the Shape tab (one collapsible panel per shape, with a row for every fractal and placement knob), the Light tab (the expanded shape’s list of lights) and the Material tab, with a row for every light and material knob, the Gameplay tab (Save / Load level, exit sphere, spawn and movement), and the View tab (the Camera mode, the mouse sensitivity, the Noise editor button, the Window buttons, the Renderer options and Back to Menu), stacked above the Movement pane of virtual axes. Everything the console does not own is driven directly by the mouse and keyboard. Escape only frees the mouse: nothing pauses, and it never opens or closes the console. Click the view to take the mouse back.

F3 toggles an opaque performance overlay above the menus, including while a console or noise-editor field has focus. It shows current FPS and expensive CPU operations as milliseconds and percentages of the 25 ms (40 FPS) diagnostic frame budget, averaged per game frame over the last quarter second. CPU scopes include nested work, so their percentages overlap. The fractal viewport’s latest available CPU submission and GPU ray-march/shading times are shown separately; an idle cached image is marked idle, and unavailable timer results are labelled rather than reported as zero cost. CPU/GPU work can overlap. Closing F3 stops its timers, sampling callbacks, and viewport timing queries.

While F3 is open, every frame is captured in memory. Closing it (including temporary suppression) or exiting the game writes a versioned JSON Lines log to user://performance/; the full path is printed to the game log. Each game process gets one file, and reopening F3 appends another capture segment. No JSON formatting, disk writes, periodic flushes, or background writer run during capture. Long captures use progressively more RAM; an abnormal exit can lose the open segment. Saving a long capture may pause the game after F3 closes.

Logs contain actual elapsed frame times, per-operation CPU milliseconds and percentages of that frame’s wall time, frame_budget_exceeded markers above 25 ms, and still_generated markers when the automatic high-quality cached still is requested. Screenshots and exports do not generate these markers. The 40 FPS target is a diagnostic threshold, not a change to the game’s frame cap or quality governor. GPU timings are broken down by viewport pass (including lens, painting, phosphor, beacon, and cache passes when present). These are asynchronous observations, not exact per-source-frame GPU timings; individual calculations inside the fractal shader cannot be timed separately this way. See the profile log format and analysis guide before analyzing captures. Each file also embeds its timing semantics in its header.

F4 toggles a matching gameplay overlay with the current shape/level number and distance from the run origin (spawn). One gameplay world unit is the distance travelled in one second at the player speed when the run starts, including the legacy starting-speed calibration, measured by the single ray straight ahead rather than the six-ray Scale, so levels saved before scale awareness keep their stage radii. The scale stays fixed through movement, speed changes and stage transitions, and is shared by the readout and level advancement. Restarting the run captures a new starting scale. The overlay works from child windows and text fields and does no readout work while hidden. Both overlays stay in the game window; their shortcuts also work from child windows.

The console opens beside the main window — or on another monitor when there is no room beside it — and can be dragged anywhere. Keep it off the main window: on macOS (Godot 4.6.1, GL Compatibility) a separate window overlapping the main one corrupts its picture (the fractal squeezed into the uncovered strip, or black) until it is moved away. On a single laptop screen with the main window at its default size there is no room beside it, so shrink the main window, drag the console aside, or use Fullscreen (below). (On the web it is a window inside the page.)

The main window first opens over half of its screen’s pixels (each side about 71 % of the screen’s), centred. After that the game remembers where you left the main window and the console, and reopens them there (kept in user://settings.cfg; a monitor that has since been unplugged falls back to the default). The Rendering tab’s Window row has:

The mouse sensitivity (the title menu’s Settings, or the console’s View tab) is the player’s preference, not part of a level: it is kept in user://settings.cfg, survives restarts, and loading a saved level does not change it.

The Renderer options on the console’s Rendering tab tune how the open view is rendered, and are saved with the view.

Renderer option what it does
Detail scales the precision everywhere (higher = finer structure, more steps)
Full-detail range how far full detail reaches, in multiples of the reference distance below, so it means the same at any zoom depth
Detail falloff how quickly detail coarsens past that range; 0 (the default) keeps detail matched to the pixel at every distance
Full-detail range follows Scale measure the full-detail range in the player’s six-ray Scale instead of the camera’s distance to the nearest surface d₀ (off by default, so d₀ as before). The fog wall’s reference now lives on the Environment tab (Scattering → Measured in)
Shed depth of field under load let the load shedder drop depth of field first on a GPU-bound frame (the blur runs at display resolution, so lowering the render scale does not cheapen it); on by default
Shed by CPU or GPU load classify each moving frame as CPU- or GPU-bound and shed from that stream (a CPU-bound frame loses scale rays and readouts, not resolution); on by default. Off reads no render timers and sheds resolution and the GPU ladder exactly as before the split
Detail vs scale how Detail follows the surroundings’ Scale: 0 (the default) ignores it; a positive value sharpens detail as you dive, negative coarsens it, never finer than Detail’s own maximum
Max march steps the step budget per ray (default 128: 96 coarse + 32 fine)
KIFS bailout margin multiplies the orbit escape radius of 1000; 1×–16×, default 1×
Mandelbulb bailout margin multiplies the power- and Julia-dependent orbit escape radius; 1×–16×, default 1×
Min resolution the lowest render scale Fast Controls may drop to while moving (default 10%)
Fast Controls target 60 fps while anything moves, then restore resolution up to the painting ceiling when enabled
Painterly brushstrokes opt into gradient-oriented brush dabs over a smaller ray-marched image
Painting ray resolution persistent resolution ceiling, including at rest (default 30% per dimension, about 9% of the primary rays)
Brush size stroke size in logical screen pixels, independent of ray resolution (default 12 px)

Higher bailout margins delay escape checks for more refined distance estimates, at the cost of additional orbit iterations. They still obey Gameplay’s Geometry iterations, apply to all shapes of that family (including CPU camera collision and picking), and do not enlarge the shape’s spatial bounds. Both reset to 1× with renderer defaults or when loading older levels that do not contain these settings. They do not guarantee a mathematically conservative distance estimate.

Painting is off by default. Enable Painterly brushstrokes in the console’s Rendering → Renderer options. The low-resolution pass does the actual camera ray marching; a separate full-resolution pass paints deterministic overlapping dabs along sampled colour gradients. Colour-aware weights limit bleeding across strong edges. Fast Controls can reduce the ray resolution further under load, but settling never removes the painting ceiling or the brush marks. Settings save with the level, and clipboard/MCP captures preserve the painted image. Older levels load with painting disabled. Screen-anchored marks are stable at rest; they can slide over surfaces during camera motion. Very thin silhouettes may disappear below the chosen ray resolution, and equal-colour depth edges cannot be distinguished by this colour-guided presentation pass.

A ray stops once the distance estimate falls below precision / detail × t × max(1, t / (range × ref))^falloff, where t is the distance along the ray and ref the reference distance chosen by Full-detail range follows Scale: the camera’s distance to the nearest surface d₀ (the default) or the player’s six-ray Scale. With Detail vs scale set, detail here is the view’s Detail times Scale^(−factor), clamped to the Detail range.

Scanning phosphor

Enable Scanning phosphor in Console → Rendering → Renderer for a progressively reconstructed hologram with cyan phosphor, glow, scan lines and a moving scan band. Scan bands selects the ray budget: the default 8 marches roughly 1/8 of the full target’s camera rays per frame and rebuilds the image over 8 rendered frames. More bands save rays but reconstruct more slowly. Strip heights round up to whole pixels with a two-row minimum, so tiny targets use a larger fraction.

Phosphor persistence controls how long old rows remain visible (default 0.6 seconds to decay to 37% brightness); Phosphor glow controls the halo (default 0.5). Movement and parameter edits deliberately leave trails until the scan revisits each row. Scanning continues with a stationary camera, even after Fast Controls settles to full resolution. Resize, render-scale changes, enabling the mode, setup and shader replacement restart reconstruction from black. Switching the mode off restores normal rendering. These options save with the level; old levels and Reset renderer defaults leave the mode off.

The march runs in a short viewport; other rows do no camera-ray marching that frame. A retained canvas target decays old pixels through alpha blending and replaces the current strip. A separate presentation pass adds the style and is also used by image capture. Neither pass samples its own render target. These full-screen compositing passes have a cost, so the ray reduction is not a promise of an equivalent frame-rate increase. Unlike normal idle rendering, this effect keeps drawing while stationary.

Camera effects

The Camera effects section of Console → Rendering → Renderer, beside Painting and Scanning phosphor, holds three groups that behave as a real camera would at a fixed physical size, so you feel your scale without grid lines. They are per-shape look rows (saved under the shape, bindable where scalar, resolved like every other look row), edited for the shape you expand on the Shape tab; a label names it. All default to off, so an old save renders exactly as before. Each reads one of two smooth, pop-free anchors — the decades below the shape’s Reference scale (Environment → Scale cues) or the decades above the nearest shape’s zoom floor — and the CPU pushes the result to a single display-resolution lens pass, created only while an effect is live.

Under the scanning phosphor the Camera effects section shows a hint that depth of field and the beacon are paused; colour fringing and grain still apply.

Scale cue recipes

Every cue defaults off, so a level shows none of them until you set some rows. These are starting points; the rows are on the Environment tab (Scale cues, Lighting rig, Scattering) and the Rendering tab’s Renderer Camera effects (Depth of field, Lens, Beacon), per shape. First set Reference scale to the Scale of the shape’s opening view: open the Gameplay tab at spawn and read Current scale; the same line shows the decades you are below the reference and above the zoom floor, which is what every ramp below counts in. Flip the Scale cues switch off at any time to compare with the plain render; the rows keep their values.

Every FLOAT row is bindable, so a cue can also follow a virtual axis or Time: bind Coaxial lamp to an axis to dim the ring light by hand, or Aperture per decade to Time for a slow focus breathe. On the Renderer pane, Shed by CPU or GPU load chooses whether the governor removes CPU work on a CPU-bound frame or behaves as the single GPU ladder did; switch it off to compare.

Deep zoom

The default renderer uses 32 iterations and 128 march steps, the site’s own budget, with the Mandelbox precision at the site’s 0.000025. Lower Fractal iterations (8 is the floor) or raise precision for a faster, smoother picture; raise Max march steps above 128 for structure that exhausts the march. The following adjustments operate within that selected work budget:

Fast controls methods

At the top of Rendering, Fast controls method offers combinable Detailed, Vignette, Blur, and Low-rez checkboxes. Each simple method shows its configuration when selected. Existing levels start in Detailed. Turning Detailed off collapses and disables the renderer editor and suspends its overrides; turning it back on restores them.

Default asks for confirmation, clears the rendering profiles, shared renderer settings and fast-method configuration, and selects Low-rez alone. It preserves shapes, materials, lighting, camera position and gameplay. Cancel changes nothing. The Default check clears when the method settings are customized.

Low-rez uses 50% of native width and height while the view changes (25% of the full-screen primary rays), then captures a native-resolution still after 150 ms of quiet. Blur uses the same reduced resolution and blends a nine-sample pixel blur in and out; its radius is measured in source pixels. These methods also respond to mouse-look, zoom, and scene edits. Combined with Detailed, their resolution is a ceiling on the Moving profile; detailed adaptation and Still quality remain available.

Vignette closes a soft black oval over the previous still in 120 ms, then fades to the live center over 150 ms. Its default opening is 25% of the screen’s area, without zooming or changing field of view. The primary shader skips pixels outside the opening before marching or shading; diagnostic probes and geometry scans retain their coverage. Stopping captures the full still before reopening its edges over 300 ms. Closing, crossfade, opening, feather width, and opening area are configurable. With a phosphor Still profile, reopening waits for a complete scan so the next movement never starts from a partial band capture. The transitions blend cached pixels without additional fractal captures; a full-quality still can still pause while rendering.

Moving and still quality

Rendering → Renderer starts with Moving and Still profile tabs. Each profile has its own resolution target, detail and distance falloff, march budget, bailout margins, shadow budget, surface detail, painting, phosphor, atmosphere, scale cues and camera effects. Scale probe ray count and console-readout cadence are independently configurable too. Scale-ray steps always use the Still march budget after its multiplier and cap; iterations and hit precision (Detail, range and falloff) also follow Still rendering. Older probe_steps values are ignored. The geometry iteration count lives in Gameplay and stays shared by rendering and collision across both profiles and load shedding.

Every row starts on Use scene: it follows the existing shared renderer setting or the active shape’s bound value. Editing the row creates an override for just that profile; selecting Use scene again removes it. Copy Moving → Still (or the reverse) copies effective values so later edits are independent. Use scene for all clears one profile. Reset renderer defaults clears both profiles and restores the shared defaults. Profiles are saved with the level; older saves keep their appearance and clear overrides from the previous level. Shared shape precision and camera-effect binding editors remain below the profiles in the collapsed shared-default sections.

For a detailed still, raise the Still profile’s Detail and Max march steps, and check the Quality limits section for inherited caps. Gameplay’s Geometry iterations changes the shared surface for both rendering and collision. Set Resolution target to 100% and disable still Painting/Phosphor for a sharp frozen image. A moving painting or phosphor can transition to an ordinary still. Shadows, atmosphere and lens effects remain optional and configurable. The still renders once after the settle delay and fades over the previous image; raising its quality does not add intermediate ray-marched frames. A still with Phosphor enabled intentionally keeps scanning.

Moving Resolution target is a ceiling; Moving adaptation → Min resolution is the floor while Fast Controls adapts. If the ceiling is below the floor, the ceiling wins. A still is a single capture at its target resolution, subject to that profile’s Painting ceiling, so it has no adaptive minimum. Settle delay, fade duration and the inherited still resolution are under Still capture timing & default resolution.

Under Moving load protection, turn off Allow shedding for fog to keep distant landmarks, or for probe/readouts to retain your chosen scale measurements and readout cadence. Detail, steps, shadows and surface detail can also be protected separately. Depth-of-field shedding and the CPU/GPU split remain under Moving adaptation. Protected work may lower frame rate instead. The still profile ignores moving load levels 1–4. Level 0 supplies inherited quality limits; explicit profile limits replace those defaults.

Quality costs can compound: 100% versus 50% resolution traces roughly four times the primary rays, 512 versus 32 march steps permits sixteen times the steps per ray, and 32 versus 8 iterations permits four times the work per distance estimate. Shadows and camera effects add work. A demanding still may visibly pause capture; actual time depends on the scene and GPU. Render overrides do not change the saved collision/picking budgets or per-shape binding values. All scale consumers share at most one ray batch per scene per process frame, including catch-up physics ticks, camera cues, collision-related scale queries and route timing. The first request selects the sample position and directions; the batch covers both configured profile ray counts. Each profile takes its geometric mean from its prefix of those shared distances, even after movement or parameter edits. Changes take effect on the next frame’s first request. Unchanged scenes keep their cached measurement. Local collision distances and clearance checks still evaluate each position; route timing uses the shared forward sample with fresh local distance caps. The independent centre-ray marker and fixed world-unit calibration retain their legacy picking budget, keeping saved level distances stable on load.

Load shedding

Currently paused: automatic resolution reductions, GPU quality shedding, CPU probe/readout shedding and depth-of-field shedding are disabled, including when loading existing saves. Moving and Still quality profiles and their selected resolution targets still apply. Saved shedding preferences and levels are retained for later; the related automatic controls are disabled. The description below documents the retained system.

With Fast Controls on, the governor targets 60 fps (16.67 ms per frame). Resolution drops first, down to Min resolution (default 10% per dimension, which draws 1% as many pixels). At that floor, sustained rates below 58 fps shed quality a level at a time. The opening level uses these same defaults; other saved levels retain their explicitly saved renderer settings. Older levels without an iteration setting inherit the current iteration budget.

Every limit only lowers the user’s budget. Displacement remains active because it defines the geometry. Rendering, CPU collision and picking retain Gameplay’s shared geometry iteration budget. Lower march budgets may omit fine structure; raise march steps when inspecting deep detail.

Those levels are the GPU ladder. The governor classifies each moving frame as GPU-, CPU- or unknown-bound from the ray and lens viewports’ render timers and the main thread’s time, and sheds from the bound stream. The GPU stream’s levers, in order, are depth of field (first, since the blur runs at display resolution so a lower render scale does not cheapen it), then the render scale, then the GPU ladder above; each moves only on a GPU- or unknown-bound frame. A separate CPU ladder sheds a CPU-bound frame’s own work instead of lowering resolution and shading to no effect: level 1 drops the six-ray Scale to a forward/back mean and thins the console readouts, level 2 to a single forward value. These means use the shared batch, which reserves both configured profile counts, so reducing the moving mean alone no longer saves marches. March steps and precision always follow Still rendering. The centre ray, collision, picking, the depth scan and the agent’s scans keep their budgets. The CPU ladder sheds on a CPU-bound frame, or under unknown timing once the GPU ladder has maxed out. On the web export, where the GPU timer is unavailable, a slow frame whose CPU is near idle reads as GPU-bound, so the export drops depth of field and then GPU work at the first slow dwells. Depth of field eases back over 18 frames after the scale has climbed to its moving target and the GPU ladder is at 0, and is full quality in the still frame.

Rays stop at the performance fog distance. Shedding needs 0.5 s below 58 fps, with 1 s between changes; restoration needs 2 s above 59.5 fps, so a display paced to 60 Hz can recover quality. A quick relapse doubles the restoration wait, up to 16 s. Fog, detail and march multipliers ease back over 18 frames; absolute march caps and surface effects change with the level. After 150 ms without changes, one final frame (capped at the painting resolution when enabled) uses the Still profile quality and resolution. The previous image stays visible until the new capture is ready, then the two images blend over 0.3 seconds. These intermediate fade frames only blend cached pixels; they do not re-march the fractal or rerun the painting shader. Scale-aware detail also finishes settling before that capture. Both rendering passes then freeze until something changes. Phosphor scanning and escape animations remain live. Movement or edits cancel the fade immediately. The moving scale and shed level are remembered for the next move, and the expensive still capture is excluded from moving performance measurements. The console’s Movement pane shows the applied load shed GPU n/4 · CPU m/2 (each part hidden at 0, · DOF off while depth of field is shed), clearing it when the still frame restores full quality; the F3 overlay adds a Bound: line with the GPU and CPU times and the verdict. L disables adaptation for a trailer take: the Moving profile target and shed level 0 on every frame, with the game capped at 30 fps (Main.FIXED_FPS) so the pacing stays even. A scene that cannot hold 30 fps at full quality still drops below it.

These controls target 60 fps; headless tests verify their behavior, not GPU frame time. Hardware, screen size, shape parameters and noise graphs still need a rendered benchmark to establish an actual frame-rate guarantee.

Rendering cost by shape

Frame cost is roughly pixels × march steps × distance-estimator work, plus surface shading. A scene estimate tries each shape that cannot be rejected by its cheap bounding cube, so nested and overlapping bounds can multiply work.

Lower resolution is the broadest saving: half the width and height means a quarter of the pixel work. Shorter view/fog distances and coarser precision can end rays sooner. Lower iteration limits reduce work per estimate but change fine geometry; lower march limits cap work but can leave dark unresolved rays. Normals add six distance estimates per lit hit; Ice Fractal shading uses four field samples, or both sets during its crossfade. Grayscale skips normals. Shadows add another march (up to 48 steps at level 0), bump adds six noise samples, and displacement adds noise to every distance estimate and may force smaller march steps. Simple colour arithmetic and post-hit haze are generally cheaper than those repeated geometric queries. Player speed limits are independent of rendering quality and do not reduce per-frame shader work.

Fly camera (default)

key / action what it does
Ctrl (tap) show / hide the hotkey card: every key, over the view. Fires on release, so Ctrl+S and Ctrl+P still work
` (backtick) open the console and free the mouse; press again to close it and recapture
[ / ] cut to the previous / next shape of the level (wrapping), back at spawn with no fade; works from the end screen too
Q / E the default virtual Axis A (negative / positive). Bind a Shape row to Axis A to give Q and E that knob
N toggle the noise-field editor (also releases/recaptures the mouse)
Escape free the mouse (click the view to recapture). Nothing else: no pause, and the console stays as it is. While the noise editor has the keyboard, Escape closes it
. close the game
W / A / S / D move forward / left / back / right
Space / Backspace move up / down (world-relative to the camera’s own up)
Shift (hold) free flight only: raycast distance speed — the farther the surface under the crosshair, the faster you fly (never slower than walking)
H hide / show all UI over the view: the chat, the noise editor, the hotkey card, the F3 / F4 overlays, the call centre’s HUD, the agent’s drawings, the probe dot, the Julia marker and the escape boundary sphere. The console stays as it is, so you can edit shapes and levels against a clean view, and the mouse is left alone. Pressed again, brings back exactly what was showing
Tab hide / show the UI: the console and, during a connected call, the pilot chat. Hiding recaptures the mouse to fly; showing frees it (a trailer capture starts hidden)
R show / hide the centre-ray probe: a white dot painted on the surface straight ahead, where the speed is measured to (remembered between runs)
mouse move mouse-look while captured (yaw about world +Z, pitch about the camera’s right; no roll)
click with the mouse free, click the view to recapture the mouse (and close the noise editor); the console stays open so its readouts stay in sight. A click on the console itself stays in the console

Free flight (the editor camera) moves at Walking speed x Player scale from Gameplay. Holding Shift switches to the raycast distance speed: the distance the centre ray travels before it meets a surface (open sky counts as the 20-unit horizon, max_distance_speed), capped at 10 times the nearest surface in any direction (near_cap) so hugging a wall while looking down a corridor cannot carry you through it, times the Sprint multiplier. Shift never flies slower than walking speed. Walking mode has no sprint. The wheel does not adjust flight speed.

Manual flight sweeps the entire requested path in conservative steps and stops at the last verified safe point. It never skips a remainder when the check budget is exhausted. Precision safety margin defaults to twice the local float32 rendering-floor estimate, checking each visible shape’s own clearance as well as the surrounding Scale. Tiny shapes retain proportionally tiny floors. Below a floor, positive-clearance positions can back out along a verified non-worsening path; nonpositive clearance stops flight until an explicit spawn/jump recovery. Looking around remains available. Gameplay displays the movement limit, with a duplicate indicator in Movement.

Geometry iterations in Gameplay defines the same surface for CPU scale, collision, and rendering. Moving/still profiles and load shedding cannot change that geometry. Legacy files migrate to their highest authored iteration count. The precision-floor estimate is conservative guidance, not a guarantee of visual quality for every fractal. Noise displacement remains GPU-only and is not covered by CPU collision. Orbit editing and agent movement retain their existing policies.

Orbit camera

Switch to Orbit from the Camera option on the console’s Rendering tab. The mouse is never captured in this mode.

action what it does
left-drag orbit around the centre point
Shift + left-drag pan (moves both the camera and the centre)
right-drag, or Alt + left-drag dolly in and out
mouse wheel zoom toward the point under the cursor
click (no drag) re-centre on the surface under the cursor (or the origin if nothing is hit nearby)

Console (`)

The backtick key opens the console, a separate OS window: the Shape, Light, Material and View tabs on top, taking the full width and most of the height, and the Movement pane below them at its natural height (drag the bar between them to share the height differently). ⌘= / ⌘− (Ctrl elsewhere) zoom everything in the console, text and controls, a step at a time from 50% to 200%, and ⌘0 resets; the window keeps its size and the panes reflow. The zoom is remembered between runs.

Shape tab. An Add shape button and a read-only Self-similar: yes / no (reason) line head the tab, over a list with one entry per shape in the scene. Each entry is a header line — a ☰ handle, a ▾ / ▸ arrow that expands or collapses its panel, the shape’s editable name, and a ✕ that removes it (the last shape cannot be removed) — above that shape’s full control panel. Drag a shape’s ☰ handle up or down to change its place in the play order: a blue line shows where it will land, the list scrolls near its edges, and a right click cancels. The order is saved with the level, and a shape being played keeps playing at its new place. Only one panel can be expanded at a time. The Light and Material tabs edit that expanded shape and show its list number at the top. Closing every panel hides both editors. Inactive shapes show their defaults in binding readouts and gradient previews; only the gameplay-active shape is resolved live. The optional material stage preview follows the running view’s centre pixel and keeps its last sample when the shape being edited changes. Clicking a header expands the shape and makes it the active one (its name turns blue): the self-similarity line and the Julia marker follow it, and Add shape makes a copy of its knobs (bindings aside) at a tenth of its size, centred in front of the camera, and selects it. The self-similarity line says whether the active shape is exactly self-similar, following the shape and the noise graph live. On the Mandelbox that is Julia mode with the constant fixed in x, y and z, an iteration that magnifies, no scale drift and no noise displacement; the KIFS qualifies without a twist, the Kleinian always (both without noise), the Mandelbulb never (its power map stretches some directions more than others, so its deep copies are distorted). It is information only; docs/reference/self-similar-substitution.md explains what it means for deep zoom. A panel has one row per knob of the fractal that shape draws, grouped Placement, Fractal, that fractal’s own groups, then Render. Rows and headings of the fractals not chosen are hidden.

Light tab. Lighting system switches between the material’s own lamp (off, the default) and the expanded shape’s lights (on). While off, light entries, Add light, Ambient and rig weights are disabled. Up to four lights are stored per shape, followed by the scene rows. Add light offers three types and puts the new light in the first free slot of four (it is disabled, with a tooltip saying so, once all four are in use). Each light is an entry with a header — a ▾ / ▸ arrow that expands or collapses it, Light N · Type, Place here for a Point light and a ✕ that removes it (its slot goes back to Off and every value and binding in it to the defaults; slots do not renumber) — above its rows, the first of which is its Type, so a light can be re-typed in place:

Every type has a Colour, an Intensity and a Drop-off distance (half strength at that distance, 1 / (1 + (d / drop-off)²); 0 is none). For a sun, which has no position, the distance is from your eye: the sun fades with depth. Lights add up; their colour is blended by how much each lights the pixel, and the first light in the list tints and aims the scattering haze. A frame draws one shape’s lights — the shape being played, or the selected one in the editor — so in the editor the selected shape’s lights light every shape on screen. Below the lights: Ambient, the scene’s fill, added to every lit pixel whatever the lights do. Then Picture — Exposure and Saturation on the final surface colour. (Scattering — the atmosphere — moved to the Environment tab below.)

Material tab. One editable material replaces the numbered colour modes and cosine palette. The default Ice Fractal preset reconstructs the site’s lamp, normal tints, blue floor and inverse-step ramp. Its ramp uses a dense linear gradient approximation; the normal blends to the true surface normal up close. The groups are Preset, Inverse Step Count Recolor, Normal, Material lamp, Surface, and Texture.

In a gradient editor, click the strip to add a sampled point, select a handle to edit its colour, drag it between its neighbours, and right-click a handle to remove it (the last point stays). Tint pickers have no alpha; the ramp has an alpha strip beneath its RGB strip. Gradients are saved as arrays of {pos, color: [r, g, b, a]} and cannot have bindings.

Reset Material defaults resets its groups without switching the lighting system; Reset Light defaults resets that switch. Old color_mode and palette_* save keys are ignored with the loader’s unknown-id warning. A save without lighting_system loads it on when any saved light slot is not Off, and off otherwise. An explicit saved switch always wins. The other site modes are no longer available because their formulas cannot be expressed by these material controls.

Environment tab. A fourth look tab, like Light and Material, for the shape you expand on the Shape tab. Its groups:

The camera’s own effects — Depth of field, Lens and Beacon — are not on this tab: they are on the Rendering tab’s Renderer Camera effects section (above), so “what the camera does” stays on Rendering and “what the world is like” on Environment.

Each bindable row carries Fractacular’s binding widgets: a Source (None, Time, or a virtual axis), a Gain, a Waveform (Linear / Sine / Triangle) and a Period. The value in use each frame is default + gain × waveform(source), shown as a live readout while a binding is active.

Movement pane. One line per virtual axis — an editable label, its id, the live value, a 0 button, the push speed, two key-capture buttons (click, then press a key) and a remove button — plus Add axis, the clock readout and the collision status. The keys and axes have no file of their own: they are saved inside the level (the Gameplay tab’s Save level…) and restored when a level is loaded. Key pairs push an axis at its speed; a Shape row bound to that axis reads the value, so the row’s gain is the key’s sensitivity.

⌘S (Ctrl+S elsewhere) saves the level over the current file without a panel; with no current file it opens the Save panel.

⌘P (Ctrl+P elsewhere) copies a screenshot of the fractal, without any UI, to the clipboard as a PNG. On Linux this needs xclip.

Rendering tab (console)

The Rendering tab holds Camera mode (Fly / Orbit), mouse sensitivity, the Noise editor…, window controls, Renderer options and Back to Menu.

Gameplay tab (console)

Build type selects one project-wide mode: Published Web Build (the default), Level Developer, or Published Desktop. Source/editor runs always keep the console available so you can change this selection. Exported builds only show the console in Level Developer mode; startup, backtick, Tab and fullscreen cannot reopen it in either published mode. Radio selections made in a source run are saved to build_profile.tres for the next export, independently of level saves and Reset Gameplay defaults. These modes control console availability; choose the matching platform export preset separately.

Each row has a Save file dropdown and Confirm button. Choosing a file stages the assignment; Confirm saves it to the build profile. Confirming the active build also loads that file; confirming another build keeps the current level open. Switching radio buttons loads the newly selected build’s confirmed save, discarding unsaved edits. Published Web starts with 10-26Publish.json; Level Developer and Published Desktop start with defaults.json. Source runs offer the JSON saves directly in saves/, so assignments travel with exports. The OS window titles for the console and game show the current filename—the Cmd/Ctrl+S target—or Unsaved level when Save will ask for a filename. The filename is not drawn inside either window.

New file / Save level… / Load level… live here, with the current file and status. New file discards unsaved edits and starts an unsaved level with one default Kleinian shape, resetting all level settings, keys, noise and the camera. Personal preferences such as mouse sensitivity are kept. The next Save asks for a filename; the previous file and last-saved startup pointer stay intact. Each shape’s Level settings offers two radio-selected escape modes:

Centers use scene coordinates with full precision. Radii use the run’s fixed speed-based world units: one unit is one second of travel at the starting speed. At a starting speed of 2 scene units/second, radius 20 means 40 scene units. Later movement-speed changes do not resize the sphere. An exit radius of 0 retains the legacy default of 20. These settings are saved per shape; switching modes keeps the other mode’s values. Old saves retain spawn-centered exit spheres using next_level_distance. Gameplay spheres are independent of shader bailout distances and the rendering-only clipping sphere.

Show escape boundary draws the active mode’s trigger sphere as a blue wireframe, using the same center and radius as the escape check. It is a flat CPU-projected overlay without occlusion, hides with H and during the escape animation, and is saved with the level. Enable escaping in Escape Animation to activate either condition; scripted commands are optional. The screen fades out, the player returns to the run origin with their view direction preserved while the next shape loads under fully opaque fog, then the screen fades back in. Meeting the last shape’s escape condition starts the escape animation: the sky and wind-blown grass fade in beneath the player, distance fog clears, and the fractal dissolves with ordered dithering. Escape Animation sets each transition’s duration independently (default 3 seconds; 0 is instant). Controls hide and movement locks while mouse look remains available, including after the fades finish. The flashing support panel is the only remaining HUD and reads “Dimensional void traversed, Have a nice day!” Diagnostics, chat, navigation guidance, and the F3 performance overlay stay hidden until the flight resets. Escape releases the mouse and clicking the view recaptures it. Cmd/Ctrl+S still saves on the end screen; confirmation appears in the terminal. Returning inside, including a return to spawn, resets the effects and controls. params.gameplay.exit_skybox accepts an optional equirectangular Texture2D for the sphere’s background; no sky picker is included yet. With no texture it uses the material background.

Spawn X/Y/Z default to the safe starting position (0.5, 0.5, 0.5). Return to spawn restarts the run at shape 1’s spawn and preserves the view direction. Saves keep the authored spawn, not the live camera. Loading starts shape 1 at spawn with default facing, fly mode, including when loading older saves.

A shape can have its own spawn: fly to the spot, face the way the player should start, and press Set spawn here in that shape’s Level settings. It stores the live camera position (full precision) and camera angle. Whenever that shape starts (a new run, Return to spawn, an advance from the previous shape, or [ / ]) the player is put there facing that way, and an exit center set to follow spawn moves there too. Use level spawn forgets it. Shapes without their own spawn use Spawn X/Y/Z and keep the view direction.

Flight tuning and safety controls are grouped in Gameplay; see the flight policy above. The old distance-scaling toggle, response power, pre-multiplier caps, and Rendering speed multipliers no longer control player flight. Virtual-axis speeds in Movement still control shape-parameter animation.

Scale

The player’s Scale is the geometric mean of six rays marched on the CPU from the 64-bit eye in the camera frame — forward (the same ray the centre-ray probe dot uses), back, right, left, up and down — each the distance to the surface it meets, or a fixed 20 scene units on a miss, clamped away from zero. It is one number per frame, cached and re-marched only when the camera or a shape changes, shared by flight speed, scaled bindings and the grid. Because it reads the whole surroundings, turning away from a nearby wall toward open space no longer makes the speed jump the way the single forward ray did. One thing still reads the forward ray alone: the gameplay world unit captured when a run starts (see F4 above), so every level’s Next level distance means the same scene distance it did before.

The two scale-cue anchors. The camera’s scale effects (Environment → Scale cues and Lighting rig, and the Rendering tab’s Renderer Camera effects) each read one of two smooth, pop-free numbers derived from Scale: u, the decades the smoothed Scale sits below the shape’s Reference scale (0 at and above it), and v, the decades above the nearest shape’s zoom floor (0 at and below it). Both follow a rate-limited log-space follower with a 0.12 s half-life, so they never jump when the six-ray Scale jumps an order between frames, and each snaps on a level load. A cue has a start and a span in decades and ramps between with a smoothstep.

The SCALE section of the Gameplay tab shows the live Current scale (scientific notation and its order of magnitude, plus · u below reference · v above floor, while the tab is open) and holds:

Scaled bindings. Every Shape or look binding has a fifth field, a scale factor (the [scale] box after Period), so a drive can grow or shrink with the surroundings. The effective gain is gain × scale ^ (scale_factor × scale_response): a factor of 0 ignores Scale (the default, so old levels are unchanged), 1 multiplies the gain by Scale, 0.5 by its square root in orders of magnitude, and a negative factor grows the drive as you dive. For example, a wobble of gain 0.1 at a scale factor of 0.5, seen at Scale 0.1, varies by 0.1 × 0.1^0.5 ≈ 0.0316 instead of 0.1 — so an animation stays proportionate as the player zooms through orders of magnitude.

The grid. With Show scale grid on, a world-fixed translucent lattice of lines is drawn along the world axes at every multiple of 10^k for three orders of magnitude — k-1, k and k+1, where k = floor(log10(scale)) — so you can read your current scale at a glance. The lattice sits at fixed world coordinates and does not move with you; lines are occluded by the surface and fade out with distance so fine cells never alias. Each order has one colour, a hue running the long way from red at Grid red at 10^ (grid_max_exponent, default 1) through yellow, green and cyan to blue at Grid blue at 10^ (grid_min_exponent, default −12); orders outside the pair clamp to the end colour. The blue exponent must stay below the red one. Grid opacity (default 0.5) sets the line strength at the nearest cells. The grid is computed from CPU-supplied 64-bit phases, so it stays exact at deep zoom where world coordinates cannot be formed in the shader’s float32.

Scale-aware rendering. The Rendering tab’s Renderer options can let the view distance and the level of detail follow Scale instead of the camera’s distance to the nearest surface d₀. Full-detail range follows Scale switches the reference for the full-detail range from d₀ (the default, as before) to Scale — with Scale, turning away from one near wall no longer pulls the full-detail band inward, and it tracks the surroundings as you zoom. (The fog wall’s reference is now its own per-shape row, Environment → Scattering → Measured in.) Detail vs scale lets Detail follow Scale by the same power law the bindings use, Scale^(−factor), with a factor of 0 changing nothing. A positive factor sharpens Detail as you dive; a negative one coarsens it, within its control range. Geometry iterations stay fixed at the Gameplay setting so collision sees the same geometry as the renderer. The Scale driving the visual response is rate-limited, so the order-size jumps inside the Kleinian do not pop the fog or the detail. Flight’s precision floor reserves the maximum Detail when this response is enabled.

Keys and axes

The virtual axes and their key pairs have no file of their own: they are saved inside the level (the axes list below) and restored when a level is loaded. Until a level is loaded the keymap is the shipped default — one Axis A on E (positive) / Q (negative) at one unit per second. Keys are stored by name (layout-independent), "" meaning unbound, and each axis registers two input actions, axis_<id>_pos and axis_<id>_neg.

Shapes and nesting

The scene is a list of up to eight shapes, each a full fractal (Mandelbox, KIFS, Kleinian or Mandelbulb) with its own knobs plus a placement: a world point p is evaluated in a shape as q = Rᵀ (p − position) / size and the shape’s distance comes back as d_local × size. The picture is the union: at every march step each shape’s distance is taken in its own frame and the smallest wins (a shape whose bounding cube is further away than the best so far is skipped, so far-off shapes cost nothing). The hit shape’s own Precision stops the ray and its frame shades the pixel; the lights and material are one shape’s look (the Light and Material tabs): the shape being played, or in the editor the selected one, lights and colours every shape drawn. Flight speed is the distance to the nearest surface of any shape, so inside a small shape you crawl at its scale and speed up as you escape; the zoom floor, the deep-zoom coarse pass and the level of detail are likewise the nearest shape’s, at its scale.

Nesting. A shape an order of magnitude smaller than its parent fits in one of the parent’s cavities. Fly into a cavity, Add shape (a tenth-size copy appears in front of you), tune its Position and Size, repeat. To start a run inside the innermost shape, set Spawn X/Y/Z to its starting position before saving; loading uses that spawn. saves/nestedEscape.json is a ready-made three-level nest (sizes 1, 0.04, 0.0016, each turned so no straight line leads out) with the camera in the innermost pocket.

Why it stays sharp at any size. Everything the fold sees is O(1): the shader never forms eye + dir·t in world space and subtracts a position (a float32 cancellation that would lose a shape a thousandth the size), because the CPU hands it each shape’s frame already applied to the eye — local_eye, computed in 64-bit, and the frame’s axes — so a ray samples local_eye + Rᵀdir · t / size. The camera’s eye is itself kept in 64-bit (CameraState.set_eye / move_by; transform.origin is its float32 mirror) so a frame’s travel inside a tiny shape is not rounded away, and a save writes the eye at full precision. Within each shape, the direct march still has a 32-bit zoom floor, scaled by its size (see Deep zoom). The ray parameter t is float32, so a surface further than about 1e7 × its own detail from the eye is below resolution (never the case for a shape you can see), and the orbit camera moves the eye through 32-bit transforms, so switch to Orbit only at ordinary scales.

The level save

There is one save file — the level — a small JSON file tagged "kind": "level", version 7. ("kind": "sequence" is reserved for the level-sequence editor to come; a sequence file handed to the level loader is refused by name, and so is an old stand-alone keymap file.)

Live camera position and orientation are not saved. Older camera snapshots and camera modes are ignored on load; the player starts at spawn on shape 1 with default facing (or shape 1’s own saved spawn and angle), fly mode and normal flight controls.

Loading skips anything it does not recognise or cannot read, with a warning, and leaves values a file does not mention as they are. Older levels still load: a version-4 file copies its shared look table into each shape, preserving defaults and bindings as independent settings. A look from version 6 or earlier had one lamp: its light_color, light_yaw, light_pitch, light_size and shadows (defaults and bindings) become light 1’s, a Player light, and the look loads with Two-sided on, so it draws exactly as it did. A version-3 file (one shape table over the whole catalogue) loads as a one-shape scene at the identity placement, its light and material rows going to the look table; a version-2 file (whose axes is a dict of values by id) applies those values onto the axes in hand and leaves the keys alone; a version-1 file (the old flat fractal section) has its keys mapped (scale→box_scale, inner_radius→min_radius, outer_radius→fixed_radius, julia_enabled→julia_all, julia_point→the three Constants); all without a warning. Saving always writes version 7. An exported build cannot write into the project, so it saves to user://saves/ instead.

Play Game opens the selected build’s confirmed save from build_profile.tres. An explicit --level=PATH overrides it. A missing or invalid assigned save leaves the built-in scene available and reports the load failure in Gameplay. Every successful save (including quick-save) still updates last_saved.txt for trailer capture; ordinary saves and loads do not change build assignments. Paths inside the project use res:// so they stay portable. Custom launches that explicitly set application/run/last_saved_pointer retain pointer-based startup; normal source runs and exports use the build profile.

file what it is
saves/default.json the original default preset
saves/juliaIceField.json Julia mode at (-0.23, 1.512, 1.892), Scale −2.29, Min 0, Fold 0.72, Fixed 0.29, Ice Fractal, precision 0.00002
saves/juliaIceTerraces.json Julia mode at (-0.23, 1.512, 1.892), Scale −1.88, Min 0.49, Fold 0.81, Fixed 0.53, Ice Fractal, precision 0.0001
saves/noiseRidges.json the default Ice Fractal level with a noise field wired: value-noise ridges displacing the surface and tinting it blue (see Noise fields)
saves/nestedEscape.json three default Mandelboxes nested through the same cavity (Outer 1 ⊃ Middle 0.04 ⊃ Inner 0.0016), the camera inside the innermost: escape all three (see Shapes and nesting)

Noise fields

Press N (or the Noise editor… button on the console’s Rendering tab) to open the noise-field editor: an in-app node graph, ported from Fractacular’s Isolation window, that builds a 3D scalar field and overlays it on the Mandelbox. Wiring it to the Output node does two things to the picture, live as you edit:

The editor is a window inside the main window (unlike the console): drag it by its title bar; its ✕, or N or Escape while it has the keyboard, closes it. Opening the editor frees the mouse; a click in the view in Fly mode closes it and recaptures. Orbit mode (Camera dropdown) is the comfortable way to author, since the mouse is never captured. Each node with an output carries a live preview — a flat slice of the field — and the toolbar’s Extent and Slice Z choose which slice every preview shows. Right-click the canvas for the add menu; drag between the coloured ports to wire (green is a Float scalar field, amber a Vec3 vector field); Delete, Backspace or right-click a wire to remove it.

group nodes
Source Position — the sample point, in fractal coordinates (one per graph)
Noise Value noise, Gradient noise (both with Octaves/Lacunarity/Gain FBM), Cellular (Worley F1)
Vector Transform (offset, scale, rotate), Warp (domain warp), Combine XYZ, Split XYZ
Math Constant, Math (Add…Fract), Remap, Clamp, Mix, Length
Output Output — Displace, Tint, Texture and Bump inputs, Amplitude, Step scale, Tint colour and strength (one per graph). Texture multiplies the surface colour; Bump tilts the normal for a fake relief (the Material tab’s Texture and Bump rows scale them).

Saving. The graph is saved inside the level, as an additive "noise" key in the same JSON file — a level without the key leaves the current field alone. The graph has no file of its own; the editor’s New button clears it back to the default (Position and Output). Load saves/noiseRidges.json for a one-click demo: value-noise ridges displacing and blue-tinting the default Ice Fractal level.

A FLOAT parameter edit only re-pushes a shader uniform; an Octaves/op/toggle edit (baked into the generated GLSL) recompiles the shader. An unwired graph compiles to nothing, so the picture and its cost are exactly as if there were no editor.

Default view

The viewer opens on the site’s initial framing exactly.

quantity value
eye (8.175847, 3.812460, 3.283393)
looks at the origin
vertical field of view 40°
world up +Z

All positions are in fractal coordinates — the space the distance estimator is evaluated in. The site’s own camera coordinates are half of ours: a point the site reports at (x, y, z) is (2x, 2y, 2z) here.

Testing

tests/run_all.sh      # every headless SceneTree test
python3 tests/fog_render_test.py  # fog cutoff/shadows/pixels in headless Chrome
python3 tests/material_shader_render_test.py  # float material stages in headless Chrome
python3 tests/material_scene_render_test.py   # real ray-march parity against the old Ice Fractal shader
python3 tests/phosphor_render_test.py  # phosphor shader and retained blending in headless Chrome
tests/screenshots.sh  # windowed render check (writes PNGs to screenshots/)
tests/probe_main.sh   # the real game, windowed, in probe mode (never takes focus; quits itself)

tests/run_all.sh imports the project (refreshing the class cache) and then runs each tests/*_test.gd headlessly; it exits non-zero if any test fails. The CPU distance estimators (DistanceEstimator, KifsEstimator, KleinianEstimator, MandelbulbEstimator) are pinned by captured fixtures, and march_safety_test.gd checks every fractal and fold option is safe to sphere-trace: a full-step march never jumps a surface that a quarter-step march finds. Because the headless renderer produces no pixels, the actual render is verified by tests/screenshots.sh, which launches Godot windowed and requires permission when run by an agent, renders the default view, the material presets, a console_bound_axis frame (Axis A bound to the scale, required to differ from the default), a lighting_raked frame (the light swung and broadened, also required to differ) and the nested_escape level (required to differ from its outer shape alone), and writes the results plus a PASS/FAIL log to tests/out/screenshots.txt.

The scale grid’s pixels cannot be checked headless. Its CPU plumbing — the per-level spacings, the 64-bit phases and the colour ramp — is pinned by scale_grid_test.gd, and shader_test.gd compiles the shader with the grid code in and pins its uniforms. The lattice itself is rendered by the windowed check tests/screenshots.sh (windowed, without stealing focus) as the grid_*.png frames: the default view, the same view moved a third of a cell along world x, and 1e-3 and 1e-5 short of the default view’s surface, facing it and looking along it, each beside the same view without the grid (*_plain.png). For each frame the red exponent is set to the order above the current Scale and the blue to the order below, so the three levels must come out red (coarsest), green and blue (finest); the pass counts pixels pushed toward each of the three and fails if the current order’s level is missing, or both neighbours are (the moved frame needs only the current order, since a third of a cell can shift the Scale a whole order inside the Kleinian). Check by eye that the lines sit still in the world between the default and moved frames and are hidden where the surface is in front. This step opens a window, so run it yourself; a Claude session only launches it with your explicit yes.

The phosphor tests check scan coverage and budgets, stationary animation setup, resize/toggle resets, persistence, UI controls and styled capture routing. The headless Chrome test renders the production phosphor shader and checks retained alpha decay, strip replacement, tint, band and glow. Godot’s headless renderer has no pixels: the final appearance and retained-viewport scheduling still need a windowed Godot visual check, which must be explicitly authorized before launch.

The UI suites drive the real widgets the way a player does, still headless. tests/ui_driver.gd (UiDriver) turns the 64x64 headless root into a 1280x800 logical surface and pushes synthetic mouse, wheel, trackpad-pan and key events through the root at window-aware global coordinates, so clicks land in the console and noise windows (embedded, headless), their dropdown popups and file panels through Godot’s own hit-testing. ui_driver_test.gd checks each primitive; ui_console_test.gd scrolls the Shape list, picks dropdown items, clicks and drags sliders, types values and edits axes; ui_noise_test.gd adds a node from the right-click menu, wires it, edits it live and saves and loads the graph; ui_journey_test.gd plays one session from the title menu to Back to Menu.

deep_zoom_test.gd replays the shader’s two-phase march on the CPU from a camera 1e-3 and 1e-5 short of the default view’s surface: with the old fixed 16-iteration coarse pass the ray spends its whole budget or misses, with the distance-following count it hits in under half.

render_budget_test.gd checks the fractal iteration budget, confirms the shader has no perturbation uniforms, and loads an older save with retired perturbation settings to verify the direct zoom floor and clean re-saving.

The reference capture from the site lives at docs/reference/icefractal-default.jpg. Put screenshots/default_view.png next to it to eyeball the default view against the original — float32 and no HiDPI mean the pixels differ, but the shape and the blue colour family should match.

Systems viz

docs/viz/ is an in-browser, pannable, zoomable explainer of how the viewer works: the architecture, one input to one frame, the distance estimator fold by fold, every dial, the two-phase ray march, the material pipeline, the two normals, the cameras and the resolution governor, and the scene of shapes with the tough calls behind nesting (./viz.sh shape-nesting, its second tab the calls). Its live figures run JavaScript ports of the shader and the GDScript (checked at load against the 24 distance-estimator fixtures), and every tooltip links the file:line it describes at a pinned commit (the panes written before the scene of shapes are pinned at the single-shape commit, where their lines are right).

./viz.sh                  # open it in your default browser
./viz.sh material         # current material pipeline, stages, presets and cost
./viz.sh mandelbox/5      # straight to a tab (1-based)
node tools/viz/check.mjs --print --pane material   # every cited line exists at the pinned commit

It is a dependency-free static page that works from file://; the engine is ported from the sibling project spellfactory. Extend or refresh it with the /visualize skill (.claude/skills/visualize/). The links 404 until the pinned commit is pushed.

MCP server (agent control)

mcp/ is an MCP server that lets an agent fly through the fractal, look at it and draw on the screen. It speaks MCP over stdio and relays each tool call to the McpBridge autoload (src/mcp/mcp_bridge.gd), which listens on 127.0.0.1:9077 (FRACTAL_MCP_PORT to change it) for newline-delimited JSON while the game runs. The server is off in a web build and in headless runs.

cd mcp && npm install     # once

The repo’s .mcp.json registers it as fractal for Claude Code in this folder. Elsewhere: claude mcp add fractal -- node /path/to/repo/mcp/server.mjs.

tool what it does
launch_game start the game straight into the explorer and wait for the bridge (see below)
status, start_game where things stand; leave the title menu
get_camera, set_camera eye / look_at / forward / up, optionally animated (duration); camera reads include facing direction and a tunable multidirectional geometry estimate
move move in the camera’s frame, in surface units by default (multiples of the distance to the nearest surface, so forward: 0.5 always halves the gap at any zoom depth); movement ignores geometry by default, with safe: true for raycast-limited moves
rotate yaw (about +Z) / pitch / roll in degrees
dive fly part of the way to the surface under a pixel and face it: repeat to zoom into a feature
scan_ship_depth expensive, 3D search for empty volumes around the player, including behind geometry; 768 positions by default, configurable 24–2048; raw results with include_samples: true, shared five-second cooldown; use the ship tool jump_to_gap to reach a scanned volume through geometry
raycast, probe, project surface point + normal under a pixel; distance estimate at a point; pixel of a point
get_shape, set_shape every Mandelbox knob with ranges and descriptions; set any of them by id
list_views, load_view, save_view the JSON views in saves/
screenshot the window as an image, after the full-quality still frame (overlay: false for the bare fractal)
draw line, polygon, circle, text or labelled point, in screen pixels or world (fractal) coordinates that stay attached to the fractal as you fly
clear_drawings, list_drawings remove one drawing by id, or all
trail record the camera’s path as a world-space line
wait_for_message, say, chat_history the pilot chat (below)

Traversal tools take look: true to return a screenshot of the new view with the result. World drawings are drawn on top of the fractal (not occluded) and clipped where they pass behind the camera.

Pilot chat. A translucent column down the right of the view holds the conversation between you and the agent (src/chat/). Enter focuses its input (WASD and the axis keys fall silent while you type), Enter sends, Escape or a click on the view leaves it; clicks elsewhere on the column go through to the view. The agent long-polls wait_for_message, acts, and answers with say; the header shows whether an agent is listening, working, or absent. The MCP server’s pilot prompt starts that loop. The log lives in McpBridge, so it survives Back to Menu.

It is built for voice input: a speech-to-text process talks straight to the bridge socket with chat_post {text, source: "voice"} for a finished utterance (shown tagged “(voice)”) and chat_draft {text} for the live transcript, which fills the input box as you speak.

Phone. server/ takes this to a phone call: a Twilio number and a server on Render (configured by render.yaml). The game dials out to the server itself when it starts (ServerLink, src/mcp/server_link.gd), given FRACTAL_SERVER_URL and GAME_LINK_TOKEN in the environment or in server/.env; -- --no-link keeps it offline. The call is answered either by the call centre’s OpenAI Realtime voice agent (VOICE_AGENT=openai, which flies the ship with the call centre’s tools) or by ConversationRelay into the pilot chat (the link posts the caller’s words with chat_post and speaks the agent’s replies). See server/README.md.

The call centre. call_center/ is the help line’s in-game half, apart from the game: the crash report and self-resolving diagnostics log, the support panel, brackets on whatever the agent names, and high-level ship tools (ship_tools, ship_tool, ship_events) that McpBridge serves. Run the game with it via ./run.sh or call_center/run.sh (--demo to enter the cockpit directly); see call_center/README.md.

Launching from a Claude session. launch_game runs tools/launch.sh -- --play --background-window, the same way Spellgraft launches: the window is parked off screen bar a 4 px always-on-top corner (a fully hidden macOS window draws no frames; screenshots read the viewport, so they are still full size), and the launcher hands focus back to whatever app had it every time Godot takes it. Any game started from a Claude Code session (CLAUDECODE=1) parks itself the same way (src/session/session_launch.gd); -- --foreground-window or FRACTAL_PLAY=1 gives a normal window, and launch_game {foreground: true} opens one for a person to watch. ./run.sh stays the launcher for people.

node mcp/smoke.mjs drives the real server end to end (launch, raycast, draw, dive, trail, screenshots into mcp/out/).

Web export

A single Web export preset (threads off) targets the git-ignored build/web/:

mkdir -p build/web
godot4 --headless --path . --export-release Web build/web/index.html

Use Godot 4.6.1 stable and its matching Web export templates, as pinned in the deployment workflow. Without the templates, the export reports missing templates rather than writing files. The export must run from a standard (non-mono) editor: a .NET build of Godot 4 refuses the Web preset even for a GDScript-only project like this one.

To try the build locally, serve build/web/ over HTTP (it will not run from file://), e.g. python3 -m http.server --directory build/web.

Click the view to capture the mouse; Escape releases it. Capture requires a browser input gesture, so a rejected startup request must not prevent that first click (or a later click after the browser releases the lock). With Playwright and its Chromium installed, check these paths headlessly against the served export:

node tests/web_mouse_capture_test.cjs http://127.0.0.1:8000

Set CHROME_PATH to use an existing Chrome executable instead.

On Windows, also install pngjs alongside Playwright and run the hardware render regression against the same export:

node tests/web_windows_render_test.cjs http://127.0.0.1:8000

This forces Chrome’s ANGLE/Direct3D 11 backend with the shader disk cache disabled. It checks startup within 60 seconds, visible fractal pixels, cycling through the bundled Mandelbox, Mandelbulb and Kleinian stages, and shader or context errors. Screenshots go to tests/out/web-windows/. It requires a real Windows GPU; software rendering does not reproduce this compiler regression. Keep the expensive normal and fold functions at shared call sites: duplicating them across branches can make D3D shader linking take minutes even when native OpenGL and software WebGL tests pass.

Deploying

.github/workflows/deploy-web.yml exports the Web preset on every push to main and publishes it to GitHub Pages, served at https://www.adamgklein.com (the repository’s Pages custom domain; the GitHub-hosted address still works). It installs Godot and the web templates itself; the repository only needs Pages enabled with GitHub Actions as the source (Settings > Pages), which the workflow also attempts on its own. The published page links the browser game to the phone server at support.adamgklein.com through Cloudflare Turnstile; the page’s config and site key are the preset’s html/head_include, and the deployed values are listed in server/README.md under “The live configuration”.

The Web preset opens on the title artwork and menu. Play Game activates the prepared level assigned to Published Web in build_profile.tres. The preset includes the profile and saves/*.json, so a fresh browser and a returning browser both reach the title screen before playing the bundled save. Keep Build type set to Published Web Build in build_profile.tres to keep the console hidden. Web/deploy builds also start the call-centre HUD automatically: the escape/ship stats at the top left and Sub-Dimensional Support at the bottom left appear with their frosted-glass backgrounds after Play Game and the normal 1.5-second intro delay. They do not require desktop launcher arguments or an active phone connection. Confirm the chosen save in Gameplay and include it and build_profile.tres in your commit before publishing; the next export follows that assignment.

Check the exported package headlessly from outside the project directory, so source files cannot mask missing export files:

cd /tmp
godot4 --headless --main-pack /absolute/path/to/build/web/index.pck \
  -s res://tests/web_deploy_smoke.gd

KorokOrb collectibles and recording paths

Open a shape’s Level settings → Collectibles and use + Add collectible. Each collapsible entry has a name and type, up/down reorder buttons and a remove button. KorokOrbs move along smooth closed loops; intercept their route to catch them. Only the active shape’s collectibles participate.

Use Record and fly the intended route. The flashing red RECORDING indicator appears at the top right. Positions are sampled at Recording resolution distance intervals, including multiple samples when one frame crosses several intervals. Stop ends sampling. Recording over an existing route asks for confirmation before replacing any points. Changing shapes or deleting the recording orb stops the recording and restores normal settings.

Points used selects a spatially distributed subset for a periodic cubic B-spline; it does not delete source points. Lowering it removes members of the closest spatial pairs, while keeping their original traversal order. Raising it restores points. At least four distinct-enough controls and a nonzero curve length are required. Source points provides paged, full-precision XYZ editing and Used / Not used labels. The wireframe checkbox shows every source marker and the actual movement spline, including its closed seam. Paths, speed, recording resolution and orb radius use the shape’s local units: placement changes carry and scale them with the shape. Animated geometry can intersect a route.

Recording Settings… opens shared Gameplay and Rendering tabs for every orb in the level. Their controls apply only while recording. Each tab can copy the current normal settings; rendering copy includes the selected shape’s precision and camera effects. File/build/spawn actions remain in the normal console. Saving while recording keeps normal settings and the recording profile separate. Collection and automatic advancement pause during recording; animation continues. Stopping starts a fresh collectible attempt at the current camera position.

On a shape with KorokOrbs, the old escape boundary cannot complete the shape. After the last orb is caught, a white sphere appears at spawn and the HUD points back toward it. With Escaping enabled, touching it fades to white, moves to the next shape, then reveals it. The final shape uses the sky/grass ending. Return sphere radius is measured in the run’s fixed gameplay world units. Unrecorded or invalid orbs block completion until repaired or removed. Levels without KorokOrbs retain their existing escape conditions; ordinary target-entry transitions now use white fades too.

Performance: each active orb adds CPU curve/collision work and analytic sphere intersection work per rendered pixel, outside the fractal march. Moving orbs keep the renderer updating even with a stationary camera. Wireframe previews and point simplification add authoring-only CPU work; more expensive recording settings can increase ray, march or post-processing cost while recording. Extreme settings that exceed the sampling/collision work limits report a visible error instead of silently dropping samples or catches.

Progressive full-quality stills

When movement settles, photographic stills refine outward from the centre in small native-quality tiles. A rolling frame-time controller targets 40 ms per frame, adjusts the number of submitted tiles, and cancels unfinished work when the view changes. Moving and still render targets are retained separately. Final resolution, ray budgets and effects still follow the Still profile. Animated escape/phosphor views keep their live rendering path.

The 40 ms target is best effort, not a guaranteed maximum. See progressive still notes for the budgeting rules, additional costs and how to interpret tiled captures.

World text and fractal artwork

Open a shape’s Level settings → World text to add centered multiline 3D lettering. Each entry supports font import, position/rotation/uniform scale, reordering, and Place in front of player. Text has thick sides, intersects fractal geometry, and mirrors through a short fuzz transition when viewed from behind. Imported fonts travel with the save; Source Sans 3 is bundled by default.

The same entry exposes Distortion (em) for actual DE-slope geometry warping, Field tint sampling scale/blend using the shape’s Material axis tint controls, and Dissolve escape threshold for volume carving. Shape to sample from defaults to the owner; another saved shape can supply the field. Resample offset, rotation and scale transform that field virtually without moving its geometry. All effects start disabled. Thresholds use log2(1 + orbit radius) after 16 fixed iterations, capped at 40; Kleinian uses terminal radius. Lower thresholds remove more volume.

Text fields target 48 texels/em and a maximum 2048×512 texture per block; up to 64 visible objects share a 64 MiB atlas budget. Overflow and invalid resources are reported on their entries. Fine features below the declared field resolution or warped/carved ray-sampling stride can be unresolved. Distortion and carving add substantial GPU orbit evaluations; new fonts/text add CPU baking and memory. See the design and implementation limits. Run python3 tests/run_world_text.py for the strict headless acceptance suite; its real-font shader preview is written to build/tests/world-text/preview.png.

Walking collision topology

When walking cannot find support near the player’s feet, it searches for nearby geometry in all directions, including walls and ceilings. Once the falling path passes body-clearance checks, the player drifts toward the nearest discovered surface at 0.5 player units per second, gradually turning their feet toward it. Mouse look remains available; movement keys do not steer the fall. Ground contact starts the normal walking topology checks, and walking resumes only if those pass. Teleports, geometry edits, and leaving walking mode cancel the old falling path.

The landing search uses a GPU field gradient, a bounded spherical ray search, and normal refinement; it cannot guarantee the globally nearest point of arbitrary fractal geometry. A missing target or an unresolved body sweep leaves the player stationary. Searching and validating a fall adds GPU queries once per acquisition; the drift itself reuses the verified route with per-frame pose interpolation.

In the console’s Gameplay tab, enable Walk on fractals and Show collision topology. Collision lookahead (player units) controls the surrounding query radius. Support rays per cell defaults to 1 · center; choose a denser grid up to 441 · 21 × 21 without changing cell size. Cyan dots mark actual ray hits (including recent route/acquisition rays). Look down to inspect nearby cells. The Walking tab exposes the same overlay switch and Route segment length separately.

Body sweep fidelity defaults to 1 · low, shared. One continuous clearance region covers a 3×3 group of cells; neighboring cells reuse the result while each keeps its own support ray. Choose 4, 9, …, 64 initial regions for finer subdivision. Every setting covers the full character volume. Uncertain regions refine across frames; a shared obstruction falls back to individual-cell checks. Both fidelity settings persist with levels, and changing either invalidates old coverage.

The persistent grid shows green sampled support with verified body clearance, yellow queries in flight, violet dashed targets not projected yet but queued to be queried, and gray unresolved areas. Red crosses mark confirmed missing support or blocked body poses; a red mark beside an obstacle does not mean all the nearby ground is missing. Verified routes remain visible alongside the patch. W/A/S/D use cached clearance where possible and wait for required tests elsewhere. Looking around does not invalidate coverage. Prefetch runs while walking is enabled, even with the overlay off. Large lookahead values report coverage limited at the bounded cache extent.

Load saves/walking_mandelbox.json, saves/walking_mandelbox_julia.json, or saves/walking_kleinian.json for authored surface spawns. See walking validation and capture instructions.

One center ray can miss small holes between samples. Increasing support fidelity revalidates coverage and movement certificates; body-clearance checks stay intact. Hit-dot history/display are capped at 4,096 points and report clipping.

Prefetch adds GPU support rays and swept-body checks plus bounded CPU cache work. Ambiguous distance-estimator sweeps use small GPU volume refinements queued across frames; unfinished checks never grant movement. Dense or difficult geometry can still take time to certify. This is numerical collision testing, not a proof for all fractal parameters or sub-sample features.