Realtime Raytracing in Bevy 0.19 (Solari)
" data-check-icon="<br>">
Realtime Raytracing in Bevy 0.19 (Solari)
Apr 12, 2026
#bevy#raytracing
Introduction
Welcome back to the third installment of my series on the development of Solari, Bevy's realtime pathtraced renderer! Bevy 0.19 is fast approaching, so let's talk about what I've been working on.
Compared to 0.18, this cycle was lighter on big features. Most of my time went into polishing existing aspects of the renderer, rather than writing new techniques. I spent a lot of time fixing Solari's material BRDF, improving specular quality, and yet again improving the world cache.
I also spent a significant chunk of this cycle researching light sampling techniques to improve NEE, though that work is still ongoing, and hasn't produced any concrete PRs yet. More on that later.
A scene lit purely by 441 emissive sphere meshes, stress testing light sampling.
Here's a summary of what's changed in Solari for Bevy 0.19:
Specular improvements : dedicated mirror BRDF, primary surface replacement, better path termination
BRDF correctness : proper layering for non-metals, Fresnel-based lobe selection, invalid sample rejection
World cache : light leak reduction and stochastic updates
ReSTIR : better spatial sample finding, skip GI for smooth metals
Before getting into the details, let's recap where we left off.
Recap of 0.18
The major feature of Solari 0.18 was added support for specular materials. We added a separate specular GI pass that does 0-3 bounce pathtracing by sampling the GGX lobe of the BRDF via bounded VNDF sampling. We also fixed a major energy loss bug in light tile packing, reduced ReSTIR resampling bias, and made the world cache more reactive and much more performant on large scenes like Bistro.
However, the initial specular implementation was rough around the edges. During the 0.19 cycle I noticed that mirrors had artifacts, and our BRDF had correctness issues with non-metals. The world cache also still had light leak problems.
This cycle, I spent a lot of time fixing or mitigating these issues.
Specular Improvements
Mirror BRDF
In Bevy 0.18, perfect mirrors were handled by clamping roughness to 0.001, and then special-casing the GGX VNDF sampling to reflect perfectly at that threshold. This worked for sampling, but the microfacet BRDF evaluation was never designed to handle near-zero roughness — it produced black line artifacts on mirror surfaces.
Black line artifacts (bottom right) from the hacked microfacet BRDF
Fixed mirrors with the dedicated mirror BRDF
The fix was to stop trying to hack the microfacet BRDF, and instead introduce a proper mirror BRDF. Roughness is no longer clamped to a minimum — true zero-roughness materials are now allowed.
The mirror BRDF is simple in theory, but a little tricky to implement correctly: it evaluates to the Fresnel term when the half-vector is aligned with the normal, and zero otherwise. Its PDF is INF (a delta distribution) when aligned, and zero otherwise.
When sampling the mirror BRDF for importance sampling a direction for the next bounce, you need to avoid dividing by the INF PDF. But when doing MIS for NEE/emissive hits, you do want to use the INF PDF. I also had to change my balance heuristic code to handle infinite values correctly:
// Old balance heuristic<br>fn balance_heuristic(f: f32, g: f32) -> f32 {<br>let sum = f + g;<br>if sum == 0.0 {<br>return 0.0;<br>return max(0.0, f / sum);
// New balance heuristic - handles INF<br>fn balance_heuristic(f: f32, g: f32) -> f32 {<br>// ReSTIR reservoirs can have UCW=0, but you can remove this if you're not doing ReSTIR<br>if f == 0.0 {<br>return 0.0;<br>return max(0.0, 1.0 / (1.0 + (g / f)));
As part of this change, cos_theta (the NdotL term from the rendering equation) was also moved into the BRDF evaluation functions themselves. The mirror BRDF doesn't use cos_theta (it cancels out for a perfect reflection), so having it outside the BRDF was causing incorrect results. The diffuse and specular BRDF functions now each include their own cos_theta handling internally.
Primary Surface Replacement
One of the open issues from 0.18 was that mirror reflections had denoising artifacts — cross-hatching patterns when the camera was static, and ghosting when objects in reflections moved.
Cross-hatching artifacts without PSR
The root cause is that DLSS Ray Reconstruction uses guide buffers (normals, depth, motion vectors) from the primary surface to denoise. But for a mirror, the primary surface is just a flat reflective plane — the denoiser has no information about the geometry visible in the reflection.
To fix this, I implemented primary surface replacement (PSR), a technique described in NVIDIA's "Rendering Perfect Reflections and Refractions in Path-Traced Games".
The idea is to trace through mirror bounces until we hit a non-mirror surface, then replace the primary surface's guide buffer data with that surface's data. The denoiser then sees the actual reflected...