-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Environment Map Filtering GPU pipeline #19076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@@ -197,6 +197,22 @@ impl Image { | |||
}) | |||
.map(DynamicImage::ImageRgba8) | |||
} | |||
TextureFormat::Rgba16Float => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this needed for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be a little big, maybe shrink the resolution a bit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like for bevy to include some spatiotemporal blue noise from https://github.com/electronicarts/fastnoise.
We shouldn't use the temporal aspect for this feature, but I'd like to use a STBN texture now so we don't have multiple copies of blue noise.
@@ -37,6 +44,8 @@ enum ReflectionMode { | |||
// Both a world environment map and a reflection probe are present. The | |||
// reflection probe is shown in the sphere. | |||
ReflectionProbe = 2, | |||
// A prefiltered environment map is shown. | |||
PrefilteredEnvironmentMap = 3, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like calling this a prefilter envmap. The "EnvironmentMap" option is also prefiltered, just offline :P
TextureDescriptor { | ||
label: Some("prefilter_environment_map"), | ||
size: Extent3d { | ||
width: 512, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to hardcode to 512x512? 256x256 might be cheaper and not worse quality? Or maybe we should let the user configure.
}; | ||
|
||
for (entity, bind_groups, env_map_light) in self.query.iter_manual(world) { | ||
// Copy original environment map to mip 0 of the intermediate environment map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the original cubemap is not 512x512?
|
||
// Tonemapping functions to reduce fireflies | ||
|
||
const white_point: f32 = 1.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the white point calculation here? It's different from what I used in TAA, where was the math derived from? And is it meant to be user-configurable?
fn get_uniform_direction(index: u32) -> vec3f { | ||
var dir = vec3f(0.0); | ||
|
||
switch(index % 64u) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Array indexing or even indexing into a buffer might make more sense here? Idk.
} | ||
|
||
// Calculate tangent space for the given normal | ||
fn calculate_tangent_frame(normal: vec3f) -> mat3x3f { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming this is what I think it is, I suggest using https://github.com/JMS55/bevy/blob/solari6/crates/bevy_solari/src/scene/sampling.wgsl#L89-L97.
If you want to be cool you can move it into some shared file I can later use in bevy_solari :)
irradiance = irradiance / total_weight * PI; | ||
} | ||
|
||
// Add some low-frequency ambient term to avoid completely dark areas |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this matter?
|
||
@compute | ||
@workgroup_size(8, 8, 1) | ||
fn generate_irradiance_map(@builtin(global_invocation_id) global_id: vec3u) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI this uses the lambert BRDF, which is not what Bevy uses for StandardMaterial.
Generally I don't think it matters, we probably already assume lambert elsewhere, but you could technically importance sample the other BRDF if you did some more math.
Some thoughts on the user API:
|
// Importance sample GGX normal distribution function for a given roughness | ||
fn importance_sample_ggx(xi: vec2f, roughness: f32, normal: vec3f) -> vec3f { | ||
// Use roughness^2 to ensure correct specular highlights | ||
let a = roughness * roughness; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget which is which, but we need to be careful of using roughness vs perceptual_roughness in this code.
Ran it through NSight.
Generally those buffer to image copies are way too slow, need to figure out what those are and remove them. Radiance pass looks good, and irradiance pass isn't that slow but has very suspiciously low active threads per warp. |
Based on the shader profiler, the reason irradiance map is performing poorly is the giant switch statement absolutely kills perf as every thread gets masked out. Should 100% index into an array for that one. |
Oh, I see that you're also doing a separate dispatch per mip for the radiance pass. Does each level depend on the previous? |
Objective
This PR implements a robust GPU-based pipeline for dynamically generating environment maps in Bevy. It builds upon PR #19037, allowing these changes to be evaluated independently from the atmosphere implementation.
While existing offline tools can process environment maps, generate mip levels, and calculate specular lighting with importance sampling, they're limited to static file-based workflows. This PR introduces a real-time GPU pipeline that dynamically generates complete environment maps from a single cubemap texture on each frame.
Closes #9380
Solution
Implemented a Single Pass Downsampling (SPD) pipeline that processes textures without pre-existing mip levels or pre-filtered lighting data.
Single Pass downsampling (SPD) pipeline:
Pre-filtering pipeline: composed of multiple Radiance Map (specular mips) generation passes, followed by the irradiance map pass (diffuse).
The pre-filtering pipeline is largely based on these articles:
Previous work: #9414
Testing
The
reflection_probes.rs
example has been enhanced with:First test case and use-case, we load a KTX texture with the RGBA 16-bit floating point format. I obtained this texture from
polyhaven.com
and used the./convert.sh goegap_road_2k.exr
command to convert it to a cubemap.I had to install 4 different command line tools to get this to work (not ideal): OpenEXR CLI, ImageMagick, OpenImageIO, and finally the KTX command line tool. And I lose half the precision in this process going from 32-bit input to 16-bit output image.
convert.sh
Showcase
User facing API:
Computed Environment Maps
To use fully dynamic environment maps, create a new placeholder image handle with
Image::new_fill
, extract it to the render world. Then dispatch a compute shader, bind the image as a 2d array storage texture. Anything can be rendered to the custom dynamic environment map.This is already demonstrated in PR #19037 with the
atmosphere.rs
example.We can extend this idea further and run the entire PBR pipeline from the perspective of the light probe, and it is possible to have some form of global illumination or baked lighting information this way, especially if we make use of irradiance volumes for the realtime aspect. This method could very well be extended to bake indirect lighting in the scene.
#13840 should make this possible!
Notes for reviewers
This PR includes a 7.2 MB KTX file for testing, this can of course be removed if it adds too much weight in the repo. We could include this "magic script" I came up with somewhere in the codebase as well. Robswain@ was suggesting that we implement the cubemap generation in a compute pipeline as well, so we could use equirectangular env maps directly instead (future work).