Fixed cluster sampling issues for volumetric point and spot lights#23387
Fixed cluster sampling issues for volumetric point and spot lights#23387JeroenHoogers wants to merge 1 commit intobevyengine:mainfrom
Conversation
…ixed raymarching sampling issue when camera is outside the fogvolume
| // Calculate where we are in the ray. | ||
| let P_world = Ro_world + Rd_world * f32(step) * step_size_world; | ||
| let P_view = Rd_view * f32(step) * step_size_world; | ||
| let P_view = view_start_pos + Rd_view * f32(step) * step_size_world; |
There was a problem hiding this comment.
This fix has a similar theme to this PR #23406. Upon cross checking they are separate fixes, both needed. the other one is for fog AABB traversal / far-plane intersection.
this one fixes per-sample view position which affects both clustering and the shadow cascades sampled. so worth testing the shadows as well to see.
|
|
||
| let cluster_index = clustering::view_fragment_cluster_index(frag_coord.xy, P_view.z, is_orthographic); | ||
| var clusterable_object_index_ranges = clustering::unpack_clusterable_object_index_ranges(cluster_index); | ||
| for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; |
There was a problem hiding this comment.
I wonder if flipping these loops around between for each cluster and for each step introduces any performance regression. while it's definitely the right approach, this requires recomputing the cluster each step meaning more work per fragment. I wonder how/if we can save this. Optimization could be left for later anyway in a later PR.
There was a problem hiding this comment.
Thank you for looking at this! You are right, there is likely some overhead related to finding the right cluster slice for every sample. I think a relatively easy optimization would be to cache the cluster-able object indices, computing them only once per cluster instead of for every sample. Then I re-compute the cluster index + object indices as soon as the ray exits the current cluster's z-boundary. I can look into it and try to add it to this PR.
There was a problem hiding this comment.
I looked into this and came up with a solution that avoids the (expensive) view -> cluster lookups altogether, but it requires some pre-computation:
- One time: pre-compute a
view_z_planes[]array storing the minview_z(entry plane) for every cluster z_slice. This buffer only needs to be updated if the projection matrix changes. - Bind the new
view_z_planes[]array as a uniform / storage buffer.
Inside the volumetric_fog fragment shader:
- Compute
xandycluster indices once per fragment (trivial). - Compute initial
z_slice(usually 0, except when the camera is outside of the volume). - For every sample, check if we intersected one or more z_plane(s)
if (P_view.z > view_z_planes[z_slice + 1])(exit plane)? If yes:- Increase
z_slice - Re-compute cluster index from x,y and z indices using existing function:
fragment_cluster_index(p: vec3<u32>, cluster_dimensions: vec4<u32>) -> u32 - Get the new light indices using updated
cluster_index.
- Increase
This way we only increase the z_slice index as we march, avoiding the expensive logarithmic view_z -> z_slice calculations completely.
Let me know what you think. Also if we want to go ahead with it, should it be part of this PR or a new one?
There was a problem hiding this comment.
Thanks for looking into this - I think it's best if we keep this change in a separate PR and keep the scope of this one small. Since it's a visual bugfix first I'm going to approve this one knowing theres a way forward for performance.
There was a problem hiding this comment.
on the volumetric fog example on mac I see these numbers (doesn't have many lights)
this branch: 7.21ms
main: 7.37ms
for a proper performance comparison I suggest a stress test volumetric fog with many lights in this case 32:
I see a clear performance regression, took 2 measurements per branch
this branch: 192.77ms 186.10ms
main: 160ms 161.77ms
There was a problem hiding this comment.
Posted a branch might help with your testing https://github.com/mate-h/bevy/tree/m/volume-many-lights
|
|
||
| // Calculate absorption (amount of light absorbed by the fog) and | ||
| // out-scattering (amount of light the fog scattered away). | ||
| let sample_attenuation = exp(-step_size_world * density * (absorption + scattering)); |
There was a problem hiding this comment.
This looks like the correct fix compared to the directional path above. Same discrete Beer–Lambert extinction per step as before, the atmosphere-side is more analytic along the actual path, which could be a separate improvement.
| @@ -338,33 +338,35 @@ fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> { | |||
| // Point lights and Spot lights | |||
| let view_z = view_start_pos.z; | |||
There was a problem hiding this comment.
looks unused, can remove this
|
Oh and also thanks for posting this PR, must have been a tough one to track down! |
Objective
Solution
FogVolume. This is now fixed in both directional light and the point / spotlight implementations.Testing
I tested using the code given in the bug report and adapted it to the latest version of bevy:
Showcase
After changes (using code from: #18371):
volumetric_spotlights.mp4
Note: The flickering of the light is due to the regular sampling interval, adding more samples or jittering the ray origin minimizes this problem.
Ray marching when camera is outside of

FogVolume(Before):After:
