29 Commits

Author SHA1 Message Date
xfy
107fbd0a74 feat(shader): add disk_color_volumetric (ridged + density + arms)
Three multiplying signal layers: ridged_fbm filaments for brightness,
smoothstep-gated FBM for density clumping, logarithmic-spiral arm
modulation riding the Keplerian shear. Octave triplet selected from
disk_quality tier. Returns DiskSample; inert until Task 7 wires it in.
2026-07-15 13:37:52 +08:00
xfy
de4ceee8ff refactor(shader): extract disk helpers, add DiskSample + flat fallback
Splits disk_color into shared helpers (temperature_color, radial_falloff,
apply_doppler, r_of) reused by both the flat and volumetric paths.
disk_color_flat returns a DiskSample with the old fixed 0.85 alpha,
preserving the exact pre-volumetric appearance behind the Off tier. A
temporary disk_color shim keeps the main-loop call site compiling until
Task 7 restructures it.
2026-07-15 13:29:48 +08:00
xfy
c9cc2eec99 feat(shader): add ridged_fbm multifractal noise
Ridged noise (1 - |2n-1|, raised to sharpness) produces the sharp bright
filaments the spec calls for, replacing the smooth FBM blobs. Fixed
MAX_OCTAVES=6 with early break — conservative form for runtime-chosen
octave counts on older WebGPU drivers. Inert; no caller yet.
2026-07-15 13:24:16 +08:00
xfy
35ae3ce5cc feat(uniform): add 9 volumetric-disk fields to BlackHoleUniforms
Rust and WGSL structs kept in lockstep: 8 f32 tunables + disk_quality
u32 tier. Appended after _pad5; scalar fields pack contiguously so no
explicit padding needed. Defaults mirror BlackHoleParams::default.
2026-07-15 13:18:02 +08:00
xfy
52cf4e4a29 fix(stars): balance AA so it only softens edges, not size or count
After the round-star fix, toggling "Anti-aliased stars" made stars look
bigger and more numerous instead of just smoother. The AA path differed
from the hard-edge path in three ways that each inflated the footprint:

  - radius: AA used 0.25 + b*0.4 (up to 0.65); hard path used fixed 0.5
  - gain:   AA used 4.0; hard path used 3.0  (+33% peak brightness)
  - falloff: AA Gaussian decayed slowly (36.8% at one radius, never 0);
             hard path smoothstep hit 0 at the radius

The slow Gaussian tail was the main offender: it kept dim stars lit far
past the hard path's cutoff, lifting many sub-threshold stars above the
visibility floor — the apparent "more stars".

Balance the two paths so the toggle is purely an edge-softness control:
  - both use radius = 0.25 + b*0.4  (same size, brightness-driven)
  - both use gain 3.0               (same peak brightness)
  - AA steepens the Gaussian to exp(-4.6*d²/r²) so it converges to ~1%
    near the same radius where smoothstep cuts to 0 — matched footprint,
    soft vs hard edge.

Peak brightness is now identical across the toggle; effective lit area
differs by <30% (the Gaussian is a cone, smoothstep a flatter dome) and
the visibility radius matches, so dim stars no longer pop in and out as
AA is toggled.

Validated: naga type-checks; app runs 20s on Metal (M4) with no shader
errors; cargo test (14 physics tests) passes.
2026-07-15 10:23:29 +08:00
xfy
f4e937f35b fix(stars): round anti-aliased stars via 3x3x3 neighborhood scan
Background stars looked square even with anti-aliasing on, and toggling
AA just made them bigger rather than round. Two compounding causes, both
in star_color():

1. Square shape: the fast path measured cell distance with
   max(f.x, max(f.y, f.z)) — Chebyshev distance, which draws an
   axis-aligned cube, not a sphere. The visible star was literally a
   faded cube face.

2. AA "just makes it bigger": the Gaussian AA path used Euclidean
   distance, but only inside ONE cell. hash13(cell) returns a different
   value across a cell boundary, so a star's radial falloff could never
   reach zero — it was hard-clipped at the cell edge. The star's outline
   was therefore the cell's bounding box regardless of the radius
   parameter; bumping the gain/radius just filled more of that box.

Fix: scan the 3x3x3 neighborhood of cells so a star whose center lies in
an adjacent cell still contributes across the whole cell it lives in,
and take the brightest contributor. Distance is Euclidean in both paths,
so both are now genuinely round. star_aa selects soft Gaussian glow (on)
vs. a tight smoothstep disk (off) — a real shape choice instead of
square-vs-clipped-square.

Validated: naga type-checks the preprocessed shader; app runs 20s on
Metal (Apple M4) with no shader/compile/validation errors; cargo test
(14 physics tests) passes.
2026-07-15 10:00:57 +08:00
xfy
a7490c0c2f render: composite + ACES tone-map (bloom stage [5]), replaces upscale
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.
2026-07-15 00:14:03 +08:00
xfy
ed6639738f render: blur pyramid (bloom stages [3]/[4]) — down then up
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.
2026-07-15 00:07:09 +08:00
xfy
6ccd0a173b render: bright-pass material + camera (bloom stage [2])
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.
2026-07-15 00:01:31 +08:00
xfy
0f33d80fd3 shader: gaussian-speck stars + star_aa toggle + bloom/exposure uniforms
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.
2026-07-14 23:55:17 +08:00
xfy
7d3a8a81ba shader: fix FBM disk seam (atan2→Cartesian) + value_noise3 range
Two issues from code review: (1) the noise coordinate used phi=atan2
which has a branch-cut discontinuity at ±π, producing a visible radial
seam. Switch to Cartesian pos.x/pos.z (continuous, seam-free). (2)
value_noise3 summed three hash components per corner via dot(.,(1,1,1)),
giving a [-1.5,1.5) range instead of [0,1). Use a single scalar hash
(.x) per corner and drop the +0.5 recenter.
2026-07-14 23:51:29 +08:00
xfy
f31ef900d8 shader: domain-warped FBM disk texture replaces two-sine noise
The disk's noise was two sin() terms (phi*8, phi*23) producing geometric
bands. Replace with domain-warped FBM: a 3-octave value-noise field warps
the sampling coordinate of a 4-octave FBM, giving the feathered/smoky gas
texture of the Gargantua reference. The Keplerian shear (rot ∝ 1/r^1.5)
is retained as the flow time term so inner radii rotate faster.
2026-07-14 23:42:16 +08:00
xfy
aadd0c62e1 fix: make dt_min a forced-accept floor in RK45 to prevent GPU hang 2026-07-14 14:48:46 +08:00
xfy
c134661596 feat: adaptive RK45 integrator replacing fixed-step RK4 2026-07-14 14:33:13 +08:00
xfy
d8b77c00c8 feat: Kerr deriv() with frame-dragging + spin-dependent horizon 2026-07-14 14:22:37 +08:00
xfy
61edcfabd2 feat: plumb spin parameter into GPU uniform 2026-07-14 14:12:31 +08:00
xfy
e158c8c668 feat: wire render_scale via offscreen render-to-texture + upscale 2026-07-14 14:09:39 +08:00
xfy
4556376b4a fix: make the black hole actually render (was stuck on grey clear color)
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.
2026-07-13 16:43:01 +08:00
xfy
ee15ddecb7 fix: make the renderer actually draw (step size, storage buffer, quad fit)
The committed Phase 1 code rendered a black screen due to three
independent bugs, fixed here:

- black_hole.wgsl: dt was sized as |eye|/steps, so the full integration
  only traveled |eye| units total — rays never reached capture or escape
  and every pixel fell through to accum=black. Now dt covers
  eye_dist + escape_r across the configured step count, and the escape
  test uses the dynamic escape_r instead of a hardcoded 1000.0.
- render/plugin.rs: the planets binding was Handle::default(), causing
  AsBindGroup to return RetryNextUpdate every frame and silently skip
  the fullscreen quad's draw. Pre-fill a MAX_PLANETS-sized zeroed
  ShaderBuffer at startup so the binding resolves immediately.
- render/plugin.rs: scale the fullscreen quad by half the window size
  to match Camera2d's default WindowSize projection (1 unit = 1 px);
  the old aspect-based scaling letterboxed the image.
- render/plugin.rs: add nudge_camera to work around Bevy 0.19 #24448,
  where a static camera stops rendering after the first frame.
- planets.wgsl: rotate world-space planet centers into disk-local space
  before the ray-sphere test (ray and centers were in different spaces).
2026-07-13 11:47:31 +08:00
xfy
90e8c9b3a4 feat: cubemap skybox layer (optional, procedural stars default) 2026-07-13 09:54:21 +08:00
xfy
5fa2621e6a fix(shaders): use #define_import_path + namespaced imports
The whole-file #import "file.wgsl" form (without ::) does not reliably
bring functions into scope in naga_oil/Bevy 0.19 — the renderer was
failing at runtime with 'no definition in scope for identifier:
ray_direction' (and rot_x, deriv, etc.), producing a blank screen despite
'cargo build' passing (shaders aren't compile-checked at build time).

Fix: add #define_import_path to each module file and import symbols
explicitly via namespace::name (the canonical Bevy pattern from the
shader_material_2d example). Verified the full shader pipeline compiles
and runs at runtime with zero WGSL errors.

This was a pre-existing bug masked by incomplete runtime verification in
earlier tasks; it is NOT specific to Task 15's grid work. All earlier
visual features (shadow, Doppler disk, Einstein halo, stars, planets,
grid) now actually render.
2026-07-13 09:51:08 +08:00
xfy
661d28a6a1 feat: lensed Flamm paraboloid curvature grid 2026-07-13 09:37:25 +08:00
xfy
8931904495 feat: lensed planet ray-sphere intersection in geodesic tracer
Add planets as ray-traced spheres inside the RK4 geodesic loop. A new
planets.wgsl shader tests each integrator segment against a storage
buffer of SphereData (center/radius/color/emissive) and composites hits
front-to-back alongside the accretion disk. Lambert shading with a fixed
light direction; emissive flag bypasses shading.

- assets/shaders/planets.wgsl: segment-sphere intersection + shading
- src/scene/: Planet component, upload_planets system, default planet
- black_hole.wgsl: wire planet_hit into the compositing loop
- geodesic_schwarzschild.wgsl: replace tuple return with Deriv struct
2026-07-10 17:06:19 +08:00
xfy
1b1eda0772 feat: accretion disk with Doppler beaming + lensed Einstein halo 2026-07-10 10:48:31 +08:00
xfy
fbe1685a38 feat: procedural lensed starfield background 2026-07-10 10:45:24 +08:00
xfy
9d81df1c14 feat: Schwarzschild RK4 geodesic integrator + black shadow 2026-07-10 10:39:46 +08:00
xfy
9665d12348 feat: per-pixel camera ray generation 2026-07-10 10:36:18 +08:00
xfy
265ec9925d feat: BlackHoleUniforms + camera/params mirror system 2026-07-10 10:33:10 +08:00
xfy
a45d42c07a feat: full-screen quad + BlackHoleMaterial pipeline 2026-07-10 10:20:38 +08:00