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.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.
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.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.
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.
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.
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.
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.
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:
d₀ to the nearest surface, to
ceil(log(2 / (2 × eps × d₀)) / log|scale|) kept between 16 and 32
(eps = precision / detail): 24 at d₀ = 0.001, 32 by about 3e-6. The
KIFS runs the same rule on its own Scale and the Mandelbulb on its Power (an
iteration near its surface stretches by about the power); the Kleinian,
which has no steady magnification per iteration, always marches its coarse
pass at the selected iteration budget.
Coarse pass iterations (Rendering → Renderer, and a row in each quality
profile) overrides this: Auto (0) is the rule above; 8 to 32 pins the count,
never above Gameplay’s Geometry iterations. 16 is the site’s fixed coarse pass: a
smoother first surface that the fine pass lands on in a step or two, so up
close fewer rays run out of steps and go dark, at the cost of the fine
structure the 16-iteration surface cannot see.d₀ = 2 and d₀ = 0.1 it fades smoothly
(a smoothstep, an S-shaped blend with no kink at either end) into the true
surface normal. Choose plain Ice field to retain the site normal up close, or
Surface for the true distance-field normal everywhere. Julia and other fractal
types always use Surface.1e-14.x, those floats land on a grid
ulp32(x) = 2^(floor(log2 |x|) − 23) apart (about 1.2e-7 near the box).
Once the hit threshold is much finer than that grid, rays reach the surface
only by luck. The nearest shape’s recommended rendering floor is
size × ulp32(largest |local eye coordinate|) / (50 × eps), where
eps = precision / detail: about 1e-4 on the default box. Higher Detail
needs a finer threshold, so this rendering floor is further out. Player
flight uses this floor with Gameplay’s Precision margin (default 2),
computed separately for every visible shape. The most demanding Detail across
moving/still profiles determines the floor; a scale-dependent Detail response
reserves its maximum so a quality transition cannot invalidate the stop.
A swept path check keeps every normal segment above the floor and stops at
the last safe point, with a 1% clearance reserve for backing away. A positive-clearance
saved position below the floor may back out along a checked path that does not
worsen its clearance or Scale at the verified samples. Nonpositive or invalid distance estimates stop
movement. Gameplay shows why flight stopped. Older saves’ retired perturbation
settings are ignored when loaded.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.
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.
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.
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.
| 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.
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) |
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.
box_scale, including in Julia mode,
so the attribute, bindings and saved value stay in sync. The original slider
covers −5 to −0.5; ours retains the wider scale range. It does not change the
Julia constants, radii, fold limit or fourth-coordinate W slice.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.
1 − u) or Linear (u), where u is the step at which
the ray first came within the Detail 1 hit threshold, as a fraction of the
active profile’s step budget, capped at 1. At Detail 1 that is exactly the
steps the ray used, as the site counts them. Above it the ray still marches
on to the real threshold, so the picture is unchanged, but the count it
reads no longer depends on Rendering’s Detail or on load shedding (which
lowers the effective Detail): a sharper Still does not read darker (a pixel
can still differ by a step or two where the coarse and fine passes
disagree). Below Detail 1 the ray stops before reaching that threshold, so
the missing steps are estimated from the hit angle, using the normal the
material already shades with (no extra samples, so a rough estimate under
the Ice field normal): approaching a flat face at angle θ to its normal,
each step closes a fraction cos θ of the remaining gap, so a
threshold k times coarser saves ln k / −ln(1 − cos θ) steps, which are
added back. The budget, not a fixed length, spans the ramp, so the Moving
and Still profiles read alike. A ray that runs out reads the step at which
it came within the Detail 1 threshold if it did, otherwise the whole budget.
The Still profile’s effective budget, with its march multiplier and cap, is
shown beside Relationship.
Ramp points carry RGB and alpha: alpha is the blend amount.
Intensity multiplies RGB (default 5.5), then Blend chooses Multiply,
Screen or Mix. The selected-point readout and second strip show the colour
after intensity; these describe the selected point, not the centre pixel.(0, 0, 0.2) gives the
site’s crevices their blue floor; black removes it.0.5 × d + d^160 + 0.1. Facing selects Absolute (default, both sides),
Clamp to zero, or Snap to value (negative dot products only). These rows
disable while the lighting system is on. Specular and Roughness live in
Surface and are shared by both lighting paths.2 / roughness² − 2.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.
The Rendering tab holds Camera mode (Fly / Orbit), mouse sensitivity, the Noise editor…, window controls, Renderer options and Back to Menu.
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.
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.
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.
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.
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.)
shapes — one entry per shape, in list order: its label, a
defaults map (every per-shape catalogue id to its value, the fractal and
the Placement ids among them) and a bindings map (only active bindings,
each a source / gain / waveform / period). A file from before the fractal
could be chosen has no shape id and loads as a Mandelbox. Each entry also
carries its own look table (defaults and bindings) for light, colour,
material and gradient settings; its four lights are the ids
light_N_<field> (N 1 to 4: type, color, intensity, falloff, yaw,
pitch, size, shadows, pos_x/y/z, radius, off_x/y/z).
A shape with its own spawn also has spawn: {position: [x, y, z],
rotation: [x, y, z, w]} (the camera angle as a quaternion).active — always 0; loads select the first shape.axes — the keymap: a list of {id, label, speed, positive, negative,
value} entries. Loading a level replaces the keymap in hand with these,
rebinding the input actions.fractal — the non-shape preferences: fast_controls,
mouse_sensitivity and the renderer options. Camera mode is transient.gameplay — full-precision spawn coordinates, escape timing, movement tuning and an
optional sphere sky texture resource path. Older files use gameplay defaults.noise — the noise graph, when one is wired (additive; see below).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) |
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.
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.
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.
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/ 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/).
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.
.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
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.
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.
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.
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.