xfy 334e65fc76 test(physics): mirror Kerr RK45 loop in CPU, cover spin=0 degeneracy + spin>0 capture
Phase 2 shipped the Kerr deriv() and the adaptive RK45 integration loop in
the shader (black_hole.wgsl:260, 320-390), but the CPU mirror in physics.rs
only covered the single-step Kerr derivative (kerr_bending_accel), not the
loop. That left the AGENTS.md 'CPU <-> shader mirror' contract broken for
Phase 2: the spin-dependent capture radius, the adaptive step control, and
the budget = accepted-steps-only / dt_min forced-accept semantics existed
only on the GPU, with nothing testable on the CPU side.

Add a faithful CPU mirror and the loop-level tests that close the gap:

- rk45_step(): Dormand-Prince step mirroring black_hole.wgsl:260-285, using
  kerr_bending_accel so it is also the Phase 1 step at chi=0.
- is_captured_rk45(): the full adaptive loop mirroring black_hole.wgsl:320-390
  — same seeding (total_path / steps), same reject/retry at dt_min (the
  forced-accept floor that prevents infinite retry), same accept-then-refine,
  and the spin-dependent capture radius r+(chi) via kerr_horizon.

Tests (tests/physics_test.rs):
- spin=0 RK45 loop captures below bcrit and escapes above (degeneracy).
- spin=0.9 still captures a b~2.0 ray (horizon shrinks but not past b<bcrit).
- capture set does not grow as spin increases across a bcrit-straddling sweep.
- rk45_step error shrinks monotonically with dt (the property the adaptive
  loop's reject/accept decision depends on).

Note: probing the error scaling showed it falls between 2nd and 4th order in
dt, not a clean 5th, because of the per-stage normalize() projection the
shader (and now the mirror) applies. The loop only relies on monotonicity, so
this is correct-as-shipped; documented in a doc-comment on rk45_step so a
future reader is not misled by the '45' in the name.

cargo test: 17 passed (3 Phase 1 inline + 14 integration, up from 12).
2026-07-14 15:51:53 +08:00

singularity-rs

A real-time, physically-motivated Schwarzschild black-hole renderer in Bevy 0.19. Each pixel geodesic-ray-traces curved spacetime, producing gravitational lensing, the Einstein ring, a Doppler-beamed accretion disk, a lensed starfield, and optional lensed planets, a spacetime-curvature (Flamm) grid, and a cubemap skybox. Runs on desktop and the web (WebGPU).

The visual target is Gargantua from Interstellar: a black shadow surrounded by a tilted, glowing accretion disk whose back side is lensed up and over the hole (and down underneath), with one side brighter from relativistic Doppler beaming.

Run

Desktop (Vulkan / Metal / D3D12):

cargo run --release

Web (WebGPU — Chrome, Edge, recent Firefox/Safari):

cargo install --locked trunk
rustup target add wasm32-unknown-unknown
trunk serve
# open http://127.0.0.1:8080

Release web build: trunk build --release. On a browser without WebGPU, the page shows a fallback message instead of a blank canvas.

Controls

  • Drag (mouse) — orbit the camera around the hole.
  • Scroll — zoom (changes distance / impact parameter).
  • Controls panel (top-left) — live-tune every parameter:
    • Camera — distance, yaw, pitch, FOV.
    • Accretion Disk — inner/outer radius, tilt, brightness, rotation speed.
    • Doppler — enable + strength of the relativistic beaming asymmetry.
    • Renderer — integrator step count (the main quality/perf lever).
    • Background — procedural star intensity, optional cubemap skybox intensity.
    • Grid — toggle the lensed Flamm-paraboloid curvature grid + density.

Orbit input is automatically disabled while the cursor is over the panel.

How it works

A single full-screen quad carries a custom Material2d whose fragment shader, for every pixel:

  1. Generates a primary ray from the camera basis + FOV.
  2. Integrates the ray through Schwarzschild spacetime with RK4, applying the discretized bending acceleration a = -1.5·Rs·h²/r⁵ · pos (h = angular momentum).
  3. At each step tests the bent segment against the accretion disk (equatorial plane), lensed planets (storage buffer), and the Flamm paraboloid grid surface — compositing hits front-to-back.
  4. Terminates on capture (r < Rs, the shadow emerges naturally at the critical impact parameter b_crit = 3√3/2·Rs ≈ 2.598) or escape (samples the procedural starfield / cubemap along the bent final direction).

The disk is tilted relative to the camera, so the back of the disk is lensed over the top and under the bottom of the shadow — the characteristic "halo." Doppler beaming brightens the approaching side.

The CPU-side physics (src/physics.rs) mirrors the integrator and is unit-tested: b < b_crit rays are captured, b > b_crit rays escape.

Performance

Defaults target ~60 FPS at full resolution on a discrete/integrated GPU (developed on Apple M4). On web, defaults drop to steps=200 for interactivity. If you need more FPS, lower Steps in the Controls panel.

Note: the render_scale parameter exists but is not wired to a real sub-resolution render target in Phase 1 (the full-screen quad always renders at window resolution). It is reserved for future work; Steps is the real performance lever today.

Project layout

src/
  main.rs            app entry, plugin wiring
  camera.rs          orbit controller (yaw/pitch/zoom) + WantsPointer
  params.rs          BlackHoleParams (tunable, mirrored to GPU each frame)
  physics.rs         CPU mirror of the geodesic integrator (unit-tested)
  ui.rs              egui Controls panel (collapsible sections)
  web.rs             wasm glue: WebGPU detection + fallback message
  scene/planets.rs   Planet component + storage-buffer upload
  render/            BlackHolePlugin, material, fullscreen quad
assets/shaders/      WGSL: ray gen, Schwarzschild RK4, disk, stars, planets, grid, skybox
docs/superpowers/    design spec + implementation plan

Status

Phase 1 (Schwarzschild, this codebase) — complete: shadow, tilted Doppler accretion disk with lensed Einstein halo, lensed starfield, lensed planets, lensed Flamm grid, optional cubemap skybox, live egui controls, desktop + web/WebGPU.

Phase 2 (Kerr / true Gargatua) — future work. Replace the Schwarzschild integrator with the Kerr metric (Boyer-Lindquist, adaptive RK4, spin parameter) for frame-dragging and ergosphere asymmetry. Same scene elements, camera, params, and UI carry over; only the integrator core swaps. See docs/superpowers/specs/2026-07-09-interstellar-blackhole-design.md.

Description
No description provided
Readme 649 KiB
Languages
Rust 71.7%
WGSL 27%
Shell 0.7%
HTML 0.6%