Adds a dedicated Quality collapsible section: bloom quality dropdown
(Off/Low/Medium/High), bloom threshold/strength, exposure, resolution
scale, star AA toggle. Changing bloom quality at runtime triggers a
pipeline rebuild (despawn bloom entities + respawn via spawn_bloom_pipeline
helper). MSAA honestly labeled as decorative.
Note: the rebuild currently re-spawns the full 3-level pyramid whenever
bloom is enabled (Off vs On). Partial pyramids for Low/Medium are a
future enhancement.
BloomQuality { Off, Low, Medium, High } controls pyramid depth (0-3).
Web defaults to Low (1 level, minimal float textures), desktop to High
(3 levels, full cinematic). Off skips all bloom passes and composites
scene-only with ACES — the fallback path for WebGPU browsers without
float-filterable support. The composite samples a 1x1 black texture
when bloom is off.
Note: this task gates Off vs full-On (3-level pyramid always runs when
enabled). Partial pyramid for Low/Medium is deferred to the runtime
rebuild in Task 8.
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.
Switch the offscreen target from Bgra8UnormSrgb to Rgba16Float for HDR
headroom (disk brightness can exceed 1.0 without clamping). Set offscreen
camera order to -20 (the bloom pipeline, added next, needs cameras ordered
so offscreen renders first). Introduce three marker components:
- Nudgable: all render cameras carry it; nudge_camera now queries this
instead of a hardcoded Or<(OffscreenCamera, UpscaleCamera)>.
- QuadScaleFactor(fx, fy): fraction of offscreen res each quad fills;
resize_offscreen rescales each quad independently via this.
- CompositeQuad: the final quad renders to the window (rescaled against
window res, not offscreen).
The upscale path still works (textureSample reads float textures);
ACES tone-mapping is added in a later commit.
Fixes the following clippy warnings in plugin.rs:
1. clippy::too_many_arguments: Suppressed on spawn_fullscreen_quad since Bevy system functions naturally take many arguments as resources/queries.
2. clippy::field_reassign_with_default: Refactored instantiation of BlackHoleMaterial to set fields in the struct literal instead of post-creation assignment.
3. clippy::type_complexity: Suppressed on resize_offscreen and nudge_camera systems as Bevy queries with Filters/Disjunctions are inherently complex.
resize_offscreen declared two Query<&mut Transform> params (FullscreenQuad
and UpscaleQuad filters). Bevy's conflict checker does not treat With<T>
filters as disjoint access — both queries declare write access to the same
component, so the app panicked at startup with error[B0001].
Merge the two queries into a ParamSet (borrowed one at a time) and move
them to the end of the parameter list per the Params convention.
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).
The quad mesh was built once at startup with the initial aspect ratio, so
widening the window afterward left empty edges. Add a FullscreenQuad marker
component and a fit_quad_to_window system that rescales the quad's Transform
to the live aspect each frame (only mutating on change).
Also remove the misleading 'Render scale' slider — it isn't wired to a real
sub-resolution render target in Phase 1 (the full-screen quad always renders
at window resolution). Lowering Steps is the real perf lever. Documented in
a code comment for future work.
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