fix(disk/jets): suppress spin-0 jets, cap beaming, decouple from RK45 dt

Three issues in sample_jets produced the reported "jet column + bright
white ring over the hole's pole" on the default scene (spin = 0,
jets_enabled = true):

1. Jets rendered at χ = 0. Relativistic jets are spin-powered
   (Blandford-Znajek taps the ergosphere), which doesn't exist for a
   non-rotating hole. jets_enabled was a free toggle decoupled from
   spin, so the default scene showed polar columns with no physical
   cause. Gate sample_jets on uniforms.spin >= 0.05.

2. Beaming saturates to white. With β=0.92 the approaching jet's
   δ^3.5 reaches ~258×, blowing the blue base color past the bloom
   threshold so High-level bloom (3-pyramid) collapses it to white.
   Cap beaming at 8× — the approaching side is still visibly brighter,
   and the base color (0.4, 0.7, 1.0) survives the HDR + bloom path.

3. dt-modulated accumulation painted ring structures. `* dt` weighted
   the per-step contribution by the RK45 adaptive step size, which
   varies across a 16× range (dt_min..dt_max) and between neighboring
   pixels — the same class of bug the disk path already documented and
   fixed in integrate_disk_segment (radial spokes). The jet has no
   analytic slab to clip to, so use a fixed normalized weight (0.5);
   brightness then depends only on geometry and beaming, never on dt.
   Drops the now-unused dt parameter.
This commit is contained in:
xfy 2026-07-16 13:48:04 +08:00
parent 614bd1d88c
commit 38d4bd00f2

View File

@ -532,8 +532,15 @@ fn integrate_disk_segment(prev: vec3f, new_pos: vec3f, dir: vec3f,
// Gaussian radial falloff, exponential length decay, outward-flowing noise,
// δ^3.5 beaming (no floor needed β=0.92 keeps the denominator positive).
// Front-to-back composited into the same accumulators as the disk.
fn sample_jets(pos: vec3f, dir: vec3f, r_plus: f32, dt: f32,
fn sample_jets(pos: vec3f, dir: vec3f, r_plus: f32,
accum_color: ptr<function, vec3f>, accum_alpha: ptr<function, f32>) {
// Relativistic jets are spin-powered (Blandford-Znajek): the mechanism taps
// the ergosphere, which only exists for a rotating hole. At χ 0 there is
// nothing to drive an outflow, so suppress the jets regardless of the
// jets_enabled toggle the toggle expresses user intent, spin expresses
// physics. Without this gate the default scene (spin = 0, jets_enabled =
// true) shows blue/white columns over the poles that have no physical cause.
if (uniforms.spin < 0.05) { return; }
let jet_v = abs(pos.y);
let jet_max_h = 80.0;
if (jet_v <= r_plus * 1.8 || jet_v >= jet_max_h) {
@ -564,12 +571,26 @@ fn sample_jets(pos: vec3f, dir: vec3f, r_plus: f32, dt: f32,
let beta = abs(jet_vel);
let gamma = 1.0 / sqrt(1.0 - beta * beta);
let delta = 1.0 / (gamma * (1.0 - beta * cos_theta));
let beaming = pow(delta, 3.5);
// Cap the beaming: with β=0.92 the approaching jet's δ^3.5 reaches ~258×,
// which blows the blue jet past the bloom threshold and saturates it to
// white. Clamp to 8× still visibly brighter on the approaching side, but
// the base color (0.4, 0.7, 1.0) survives the HDR + bloom pipeline.
let beaming = min(pow(delta, 3.5), 8.0);
let base_color = vec3f(0.4, 0.7, 1.0);
let emission = base_color * jet_density * 0.05 * beaming * uniforms.jets_strength * dt;
// Decouple accumulation from the RK45 step size. Multiplying by `dt` (the
// adaptive step the caller passes in) injects a per-pixel brightness
// modulation along the jet axis: dt varies across a 16× range (dt_min..
// dt_max) and between neighbouring pixels, so `* dt` paints bright/dim
// bands and ring structures whose only cause is the integrator's step
// choice, not the physics. The disk path hit the identical bug (radial
// spokes) and fixed it by weighting on the analytic in-slab length instead
// of dt (see integrate_disk_segment). The jet has no comparable analytic
// segment to clip to, so use a fixed normalized weight (0.5) brightness
// then depends only on geometry and beaming, never on dt.
let emission = base_color * jet_density * 0.05 * beaming * uniforms.jets_strength * 0.5;
*accum_color += emission * (1.0 - *accum_alpha);
*accum_alpha += jet_density * 0.05 * uniforms.jets_strength * dt;
*accum_alpha += jet_density * 0.05 * uniforms.jets_strength * 0.5;
}
// --- planets ---
@ -785,7 +806,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// --- relativistic jets (along the spin axis) ---
if (uniforms.jets_enabled != 0u) {
sample_jets(new_pos, new_dir, r_plus, dt, &accum_color, &accum_alpha);
sample_jets(new_pos, new_dir, r_plus, &accum_color, &accum_alpha);
if (accum_alpha > 0.99) { break; }
}