Higher default turbulence frequency gives the volumetric disk more
trackable texture features at startup, making the Keplerian rotation
(rot ∝ 1/r^1.5) visually readable without needing to open the egui
panel. The old 0.8 default produced a near-featureless smooth slab at
default settings — the disk was rotating but the eye had nothing to lock
onto. 2.0 sits in the upper half of the slider range (0.2–3.0) while
still leaving room to push higher for debugging.
Two distinct sampling artifacts on the accretion disk, found with a
CPU pixel-mirror of the integrator:
1. Moiré on the disk. integrate_disk_segment placed its N sub-samples
on the deterministic (i+0.5)/N grid. That lattice aliases against
the 2-D pixel grid and the per-ray RK45 step lattice → a concentric
banding. Fix: jitter the sub-sample position within stratum i using
a hash of the pixel coordinate and the sample index. The jitter is
pixel-seeded only (no time), so the noise is spatially stable (no
per-frame flicker) and stays inside the stratum, preserving the
quadrature order. Verified by CPU mirror: banding transitions drop
sharply and the noise is unstructured.
2. Higher-order lensed-image rings read as an artifact. Rays near the
critical impact parameter wrap around the hole; each wrap crosses
the disk plane once, projecting to a ring (a physical Einstein-ring
structure, the same one Gargantua shows). The integrator renders
each wrap as a sharp discrete band, so at high `steps` the rings
look like a bug. Fix: per-pixel stochastic supersampling — fire
several jittered sub-rays per pixel and average them, which
antialiases the band edges into a smooth gradient while keeping the
underlying lensing physics.
The two fixes share a `pixel_seed` (var<private>, set from
@builtin(position)) so the jitter is deterministic per pixel.
Shader refactor: the single-ray march is extracted into march(dir);
fragment() now loops it MAX_AA_SAMPLES times (compile-time cap for
WGSL's static-loop-bound requirement, runtime count from a new
aa_samples uniform via early break — the fbm3/MAX_OCTAVES pattern).
Uniform layout: added aa_samples: u32 + two f32 pads to round the
struct to 16-byte alignment, mirrored byte-for-byte across the WGSL
struct and BlackHoleUniforms (ShaderType).
UI/params: new AaQuality enum (Off/Low/High = 1/2/4 samples) in the
Quality panel. Defaults are tiered per AGENTS.md: Off on web, Low (2×)
on desktop — the RK45 march is already ~10× Phase 1's cost, so desktop
stays at 2× with High (4×) selectable for a fully smooth look.
cargo test: 20 passed. cargo build --release: clean.
Defaults retuned to match the Interstellar look now that the density
model drives appearance:
- disk_outer 15 → 25: Gargantua-like extent.
- disk_half_thickness semantics → H/R ratio, default 0.15 (was an
absolute 0.3/0.2 world-space band). Standard thin-disk scale height.
- disk_color_mode default → Blackbody (was Gradient): the Novikov-Thorne
radial temperature gradient × Kerr Doppler gives the smooth white-hot
inner → deep-orange outer look the new density model is built around.
- disk_temp 10000 → 6500 K: tuned so the NT profile yields warm-white
inner fading to deep orange at the edge.
- density_strength 1.0 → 1.2: the new model has no floor and decays to 0
at the edges, so a slightly higher multiplier keeps the bulk opaque
after per-step integration.
UI: "Half thickness" → "Thickness (H/R)" with a 0.02..=0.3 range
(thin-disk regime); "Outer radius" range widened to 6..=50.
BlackHoleUniforms::Default (material.rs) mirrors the same values so the
GPU default matches params.rs before the first mirror_params tick. Field
order/types/layout unchanged; no uniform-struct migration needed.
Bring three accretion-disk strengths from others/blackhole-simulation into
the WGSL renderer, behind opt-in controls. The physics.rs CPU mirror is
untouched — these are all color/shading functions, outside the deriv/rk45/
integration-loop lockstep surface.
Color model (DiskColorMode uniform, default Gradient to preserve the prior
look):
- Gradient (mode 0): existing white-hot → orange ramp + Newtonian
apply_doppler, unchanged.
- Blackbody (mode 1): Novikov-Thorne radial temperature profile
(isco_r^0.75 · (1−√isco_r)^0.25) shifted by the Kerr equatorial
4-velocity Doppler factor δ, fed through a Tanner-Helland blackbody
color function, times δ^3.5 beaming. The approaching side therefore
turns hotter/bluer and the receding side cooler/redder — the physical
signature the gradient mode lacks. kerr_doppler() solves g_μν u^μ u^ν=-1
for circular orbits exactly; the max(0.01,…) floor guards pow-of-negative
NaN. A unified disk_emission() now assembles color for both the flat and
volumetric paths, replacing two duplicated tcol/falloff/doppler chains.
Relativistic jets (jets_enabled, default on): bipolar cones along the spin
axis with 0.92c outflow beaming (δ^3.5, no floor needed — β fixed keeps the
denominator positive), Gaussian radial falloff, exponential length decay,
and outward-flowing value-noise turbulence. Front-to-back composited into
the same accumulators as the disk. Ported to ptr<function,…> accumulators
(WGSL has no inout) and the existing value_noise3.
UI: Color model dropdown + temperature slider under Accretion Disk; new
Jets section with enable + strength. Four uniform fields added as one
16-byte row, Rust/WGSL order kept identical.
Verified: cargo build --release, cargo test (17 tests, mirror untouched),
naga WGSL validation.
Two runtime validation errors crashed the app on startup:
1. BrightPassMaterial used #[uniform(0)] threshold: f32 (bare f32 = 4-byte
binding), but the WGSL BrightPassUniform struct is 16 bytes (threshold +
3 pads). The pipeline-layout mismatch ("Buffer structure size 16...
greater than min_binding_size, which is 4") killed the opaque_mesh2d
pipeline. Fix: wrap threshold in a BrightPassUniform struct matching the
WGSL layout, same pattern as BlurUniform/CompositeUniform.
2. bevy_egui 0.41's auto-context assigns PrimaryEguiContext to the FIRST
Added<Camera> — with the 7-camera bloom pipeline, that's the offscreen
camera (renders to Rgba16Float Image). egui's pipeline expects the
window's Rgba8UnormSrgb surface → format mismatch crash. Fix: disable
auto_create_primary_context (PreStartup system) and explicitly tag the
composite (window) camera with PrimaryEguiContext.
Both fixes verified: the app now runs without validation errors for 8+
seconds. Physics tests (20/20) unaffected.
Adds CompositeMaterial + composite.wgsl: combines the HDR scene with the
bloom pyramid output, applies ACES (Narkowicz) tone mapping, and writes
LDR to the window. Removes UpscaleMaterial + upscale.wgsl.
mirror_params now updates composite (bloom_strength, exposure) and
brightpass (threshold) uniforms each frame. resize_offscreen rebuilds
the full bloom pyramid targets on window resize.
Adds BlurMaterial + blur.wgsl with a 13-tap weighted Gaussian kernel
(var<private> arrays — naga rejects dynamic indexing of const arrays),
run in downsample then upsample passes. For the 3-level pyramid:
bloom_0 (half, from brightpass) → bloom_1 (quarter) → bloom_2 (eighth),
then upsample to bloom_final (half). Camera orders -18/-17/-16/-15.
The bloom output is not yet composited (Task 6); this commit only wires
the passes and confirms they run in sequence without crashing.
Adds BrightPassMaterial + brightpass.wgsl: extracts luminance above a
soft-knee threshold from the HDR offscreen into a half-res Rgba16Float
target (bloom_0). Camera order -19 (after offscreen at -20). The brightpass
output is not yet composited (Task 6); this commit only adds the pass and
confirms it runs without crashing.
star_color's smoothstep(0.5,0.0,d) with scale=80 produced blocky rectangles
at low render_scale. Replace with a gaussian speck: distance to cell center
with exp(-d²/r²) falloff, producing a round 2-3 pixel anti-aliased disk.
star_aa uniform toggles between the gaussian path (desktop default) and the
old square-cell fast path (web default).
Also adds bloom_threshold/bloom_strength/exposure uniform fields to
BlackHoleUniforms (used by later bloom tasks) with safe defaults.
At 1.318 rad (~75.5 deg) the disk is nearly vertical: its normal points
almost along +Z while the camera yaw axis is world-Y, so the two axes
sit ~75 deg apart. Orbiting via yaw (a horizontal circle around world-Y)
therefore shears the disk through view -- full circle, squashed ellipse,
edge-on sliver, back circle -- which reads as the disk swinging or the
scene rolling.
0.45 rad (~25.8 deg) brings the disk near-horizontal (normal approx
(0, 0.90, 0.43)), shrinking the yaw-axis / disk-normal misalignment
from ~75 deg to ~26 deg. The swing is correspondingly muted; orbiting
feels like turning around the hole rather than shearing across a wall.
Both defaults are updated in lockstep: BlackHoleParams (the live runtime
default) and BlackHoleUniforms (the bootstrap uniform filled before the
first mirror_params frame).
Clippy warned that the manual Default implementation for BlackHoleMaterial
could be derived (clippy::derivable_impls). We now derive Default directly
on the struct and remove the manual implementation block.
The app showed only the camera clear color (grey srgb 43,44,47) — the
fullscreen quad's fragment shader produced no output. Two issues; the
second was the real blocker.
1. render/material.rs: skybox.wgsl declared binding 1 as texture_cube<f32>
but the material's #[texture(1)] defaulted to D2. The mismatched
bind-group layout vs. shader caused the pipeline to fail to specialize.
Fix: dimension = "cube" on the texture attribute so the layout matches.
2. SHADER COMPOSITION (the actual blocker): the shader was split across
naga_oil modules (ray_gen, geodesic, stars, disk, planets, grid, skybox)
pulled in via #import singularity::... This compiled and validated with
zero errors, but at runtime calling ANY cross-module-imported function
made the fragment output nothing (only the clear color showed). Local
functions were fine. Confirmed by bisecting a minimal repro with the
user watching the screen: local fn -> renders; imported fn (even pure
math) -> grey.
Rather than chase the naga_oil 0.22 / Bevy 0.19 composition bug, inline
every function into black_hole.wgsl and drop the #import singularity::*
lines. The 7 standalone module files are removed. Rendering now works.
Also fixes scene/planets.rs: upload_planets was allocating a fresh
ShaderBuffer (new handle) every frame, which re-triggered the
AsBindGroup RetryNextUpdate that the previous commit's startup pre-fill
was meant to eliminate. Now mutates the existing buffer asset in place
via set_data, keeping the handle stable.