fix(disk): sample noise in polar coords to kill radial spokes

Root cause of the "zebra stripe" / radial spoke artifact: disk_color_volumetric
sampled noise using the raw Cartesian pos (x,y,z). On a disk, changing the
radius r moves x/z a lot (high-frequency oscillation → stripes), while a full
angular sweep revisits correlated lattice points (weak variation), so the
stripes elongate along radius into spokes. Verified numerically: the radial
sample line oscillated filament in [0.05, 0.77] while the angular line was
symmetric (phi=0 and phi=pi identical at 0.2475).

Fix: sample noise in polar space sp = (r/inner, phi*2.5 + rot, h/half_thickness).
This decouples radial and angular axes so turbulence flows tangentially — the
physically correct pattern for a rotating fluid. Post-fix radial/angular
variance ratio is 0.50 (radial smoother than angular), eliminating the spoke
bias.

Two secondary fixes in the same function:
- Density: replaced the hard smoothstep(0.3,0.7) cut (which made the slab
  patchy and over-transparent) with a soft 0.35+0.65*noise ramp.
- Brightness: raised the floor to 0.5+filament (was filament alone) so the
  disk reads as luminous plasma instead of dim brown.
- Guarded log(r) with max(r,0.1) for robustness near the inner edge.
This commit is contained in:
xfy 2026-07-15 14:17:09 +08:00
parent 19905ed86a
commit 376abbc87a

View File

@ -316,15 +316,17 @@ fn disk_color_flat(pos: vec3<f32>, dir: vec3<f32>) -> DiskSample {
return DiskSample(vec3<f32>(col), 0.85);
}
// Volumetric disk color: ridged filaments drive brightness, a smoothstep-
// gated FBM drives density clumping, and a logarithmic-spiral term (riding
// the Keplerian shear `rot`) drives large-scale arm structure. The three
// signals multiply density says where matter is, filaments say how bright,
// arms say how it's distributed.
// Volumetric disk color. Noise is sampled in POLAR coordinates, not the
// raw Cartesian pos, because sampling Cartesian (x,y,z) on a disk produces
// radial spokes: r changes move x/z a lot (high-frequency stripes) while a
// full angular sweep revisits correlated lattice points (weak variation),
// so stripes elongate along radius into spokes. Polar sampling (r_norm,
// phi·freq, height) decouples the two axes and lets turbulence flow
// tangentially the physically correct pattern for a rotating fluid.
fn disk_color_volumetric(pos: vec3<f32>, dir: vec3<f32>) -> DiskSample {
let r = r_of(pos);
let phi = atan2(pos.z, pos.x);
let rot = uniforms.time * uniforms.disk_rotation_speed / pow(r, 1.5);
let flow = vec3<f32>(0.0, 0.0, rot);
// Octave triplet from the quality tier.
let q = uniforms.disk_quality;
@ -333,25 +335,32 @@ fn disk_color_volumetric(pos: vec3<f32>, dir: vec3<f32>) -> DiskSample {
else if (q == 2u) { filament_octaves = 4u; density_octaves = 3u; warp_octaves = 3u; }
// q == 3u keeps the High defaults above; q == 0u is never passed here.
// Domain warp: distorts sample coords so filaments curve and bend.
let warp = fbm3(pos * 0.8 + flow * 0.1, warp_octaves);
// Polar sample coordinate: (r normalized, angle × freq + Keplerian flow,
// height within slab). The flow term advects the noise so inner radii
// (faster rotation) drift ahead of outer radii differential rotation.
let r_norm = r / uniforms.disk_inner;
let h = pos.y / max(uniforms.disk_half_thickness, 1e-3);
let sp = vec3<f32>(r_norm, phi * 2.5 + rot, h);
// Layer 1: ridged bright filaments.
let filament = ridged_fbm(pos * uniforms.filament_freq + warp * 1.5 + flow * 0.3,
// Domain warp in polar space: distorts sample coords so filaments bend.
let warp = fbm3(sp * 0.8, warp_octaves);
// Layer 1: ridged bright filaments (polar-sampled tangential streaks).
let filament = ridged_fbm(sp * uniforms.filament_freq + warp * 1.5,
filament_octaves, uniforms.filament_sharpness);
// Layer 2: density clumping (smoothstep makes a definite gas/void boundary).
let density_noise = fbm3(pos * uniforms.density_freq + warp, density_octaves);
let base_density = smoothstep(0.3, 0.7, density_noise) * uniforms.density_strength;
// Layer 2: density clumping (soft, no hard smoothstep cut avoids
// a patchy/over-transparent slab).
let density_noise = fbm3(sp * uniforms.density_freq + warp, density_octaves);
let base_density = (0.35 + 0.65 * density_noise) * uniforms.density_strength;
// Layer 3: logarithmic-spiral arm modulation, advected by Keplerian shear.
let phi = atan2(pos.z, pos.x);
let arm_phase = phi * uniforms.arm_count + log(r) * uniforms.arm_tightness - rot;
let arm_phase = phi * uniforms.arm_count + log(max(r, 0.1)) * uniforms.arm_tightness - rot;
let arm = 0.5 + 0.5 * cos(arm_phase);
let arm_mod = mix(1.0, pow(arm, 2.0), uniforms.arm_strength);
let total_density = base_density * arm_mod;
let brightness = filament;
let brightness = 0.5 + filament;
let t = (r - uniforms.disk_inner) / (uniforms.disk_outer - uniforms.disk_inner);
let tcol = temperature_color(t);