-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
GenerateEnvironmentMapLight #9414
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
Conversation
Example |
Example |
Example |
Example |
Example |
1 similar comment
Example |
Is Also, |
The rust file is not needed, no. It's unused, but I left it as a reference. I can remove it if we're ok with an opaque bin file. Up to you/cart. Also damn, I thought I had double checked my spelling :( |
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 haven't fully digested the guts of the shader yet but here are a bunch of comments.
case 0u: { return vec3(1.0, v, -u); } | ||
case 1u: { return vec3(-1.0, v, u); } | ||
case 2u: { return vec3(u, 1.0, -v); } | ||
case 3u: { return vec3(u, -1.0, v); } |
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.
These two are flipped compared to https://github.com/KhronosGroup/glTF-IBL-Sampler/blob/master/lib/source/shaders/filter.frag, is that intentional?
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 used the same get_dir function from https://www.activision.com/cdn/research/filter_using_table_128.txt that I used in the other shaders
/// * The first frame this component is added to the skybox entity, an [`EnvironmentMapLight`] | ||
/// component will be generated and added to the skybox entity. |
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.
You could just say "this is generally a quick operation".
var color = vec4(0.0); | ||
for (var axis = 0u; axis < 3u; axis++) { | ||
let other_axis0 = 1u - (axis & 1u) - (axis >> 1u); | ||
let other_axis1 = 2u - (axis >> 1u); |
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 feel like if you're switching over axis
below anyway you might as well just set other_axis0
and other_axis1
explicitly in a switch statement instead of using these bit tricks.
var color = vec3(0.0); | ||
for (var sample_i = 0u; sample_i < 32u; sample_i++) { | ||
// R2 sequence - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences | ||
let r = fract(0.5 + f32(sample_i) * vec2<f32>(0.75487766624669276005, 0.5698402909980532659114)); |
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.
Factor this out into a rand
function :)
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.
It's 1 line, I'm inclined not to 😅
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.
LGTM
Diffuse filtering is suffering from not enough samples, and fireflys. I should look at https://www.shadertoy.com/view/4c2GRh. |
// R2 sequence - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences | ||
let r = fract(0.5 + f32(sample_i) * vec2<f32>(0.75487766624669276005, 0.5698402909980532659114)); | ||
|
||
let cos_theta = sqrt(1.0 - f32(r.y)); |
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.
let cos_theta = sqrt(1.0 - f32(r.y)); | |
// map uniformly distributed [0..1)^2 into hemisphere with cosine importance (Lambertian distribution) | |
let cos_theta = sqrt(1.0 - f32(r.y)); |
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) { | ||
var id = global_id; | ||
var level = 0u; | ||
if id.x < 128u * 128u { |
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 think it has to do the biggest mip first for other reasons, maybe because all the mips after it depend on it. but wgsl workgroups are not guaranteed to run in any particular order so idk
id.x -= id.y * res; | ||
|
||
let u = (f32(id.x) * 2.0 + 1.0) / f32(res) - 1.0; | ||
let v = -(f32(id.y) * 2.0 + 1.0) / f32(res) + 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.
it maps to the centers of every other 2x2 texel patch, because the lower mip level must represent those texels as one larger one that covers the area of all 4 original ones.
id.y = id.x / res; | ||
id.x -= id.y * res; | ||
|
||
let u = (f32(id.x) * 2.0 + 1.0) / f32(res) - 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.
let u = (f32(id.x) * 2.0 + 1.0) / f32(res) - 1.0; | |
// remap integers [0..res-1]^2 to the centers of every 2x2 texel patch we are mipping down to one texel, in (-1..1)^2 | |
let u = (f32(id.x) * 2.0 + 1.0) / f32(res) - 1.0; |
This PR has artifacts unfortunately. It can't be merged as-is. |
|
||
id.z = id.y; | ||
let res = 128u >> level; | ||
id.y = id.x / res; |
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.
could save an integer division here with
id.y = id.x / res; | |
id.y = id.x >> (7 - level); |
This commit introduces a new type of camera, the *omnidirectional* camera. These cameras render to a cubemap texture, and as such extract into six different cameras in the render world, one for each side. The cubemap texture can be attached to a reflection probe as usual, which allows reflections to contain moving objects. To use an omnidirectional camera, create an [`OmnidirectionalCameraBundle`]. Because omnidirectional cameras extract to six different sub-cameras in the render world, render world extraction code that targets components present on cameras now needs to be aware of this fact and extract components to the individual sub-cameras, not the root camera component. They also need to run after omnidirectional camera extraction, as only then will the sub-cameras be present in the render world. New plugins, `ExtractCameraComponentPlugin` and `ExtractCameraInstancesPlugin`, are available to assist with this. Each side of an omnidirectional camera can be individually marked as active via the `ActiveCubemapSides` bitfield. This allows for the common technique of rendering only one (or two, or three) sides of the cubemap per frame, to reduce rendering overhead. It also allows for on-demand rendering, so that an application that wishes to optimize further can choose sides to refresh. For example, an application might wish to only rerender sides whose frusta contain moving entities. In addition to real-time reflection probes, this patch introduces much of the infrastructure necessary to support baking reflection probes from within Bevy as opposed to in an external program such as Blender, which has been the status quo up to this point. Even with this patch, there are still missing pieces needed to make this truly convenient, however: 1. Baking a reflection probe requires more than just saving a cubemap: it requires pre-filtering the cubemap into diffuse and specular parts in the same way that the [glTF IBL Sampler] does. This is not yet implemented in Bevy; see bevyengine#9414 for a previous attempt. 2. The cubemap needs to be saved in `.ktx2` format, as that's the only format that Bevy presently knows how to load. There's no comprehensive Rust crate for this, though note that my [glTF IBL Sampler UI] has code to do it for the specific case of cubemaps. 3. An editor UI is necessary for convenience, as otherwise every application will have to create some sort of bespoke tool that arranges scenes and saves the reflection cubemaps. The `reflection_probes` example has been updated in order to add an option to enable dynamic reflection probes, as well as an option to spin the cubes so that the impact of the dynamic reflection probes is visible. Additionally, the static reflection probe, which was previously rendered in Blender, has been changed to one rendered in Bevy. This results in a change in appearance, as Blender and Bevy render somewhat differently. Partially addresses bevyengine#12233. [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler [glTF IBL Sampler UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui
This commit introduces a new type of camera, the *omnidirectional* camera. These cameras render to a cubemap texture, and as such extract into six different cameras in the render world, one for each side. The cubemap texture can be attached to a reflection probe as usual, which allows reflections to contain moving objects. To use an omnidirectional camera, create an [`OmnidirectionalCameraBundle`]. Because omnidirectional cameras extract to six different sub-cameras in the render world, render world extraction code that targets components present on cameras now needs to be aware of this fact and extract components to the individual sub-cameras, not the root camera component. They also need to run after omnidirectional camera extraction, as only then will the sub-cameras be present in the render world. New plugins, `ExtractCameraComponentPlugin` and `ExtractCameraInstancesPlugin`, are available to assist with this. Each side of an omnidirectional camera can be individually marked as active via the `ActiveCubemapSides` bitfield. This allows for the common technique of rendering only one (or two, or three) sides of the cubemap per frame, to reduce rendering overhead. It also allows for on-demand rendering, so that an application that wishes to optimize further can choose sides to refresh. For example, an application might wish to only rerender sides whose frusta contain moving entities. In addition to real-time reflection probes, this patch introduces much of the infrastructure necessary to support baking reflection probes from within Bevy as opposed to in an external program such as Blender, which has been the status quo up to this point. Even with this patch, there are still missing pieces needed to make this truly convenient, however: 1. Baking a reflection probe requires more than just saving a cubemap: it requires pre-filtering the cubemap into diffuse and specular parts in the same way that the [glTF IBL Sampler] does. This is not yet implemented in Bevy; see bevyengine#9414 for a previous attempt. 2. The cubemap needs to be saved in `.ktx2` format, as that's the only format that Bevy presently knows how to load. There's no comprehensive Rust crate for this, though note that my [glTF IBL Sampler UI] has code to do it for the specific case of cubemaps. 3. An editor UI is necessary for convenience, as otherwise every application will have to create some sort of bespoke tool that arranges scenes and saves the reflection cubemaps. The `reflection_probes` example has been updated in order to add an option to enable dynamic reflection probes, as well as an option to spin the cubes so that the impact of the dynamic reflection probes is visible. Additionally, the static reflection probe, which was previously rendered in Blender, has been changed to one rendered in Bevy. This results in a change in appearance, as Blender and Bevy render somewhat differently. Partially addresses bevyengine#12233. [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler [glTF IBL Sampler UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui
# 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: - accepts any square, power-of-two cubemap up to 8192 × 8192 per face and generates the complete mip chain in one frame; - copies the base mip (level 0) in a dedicated compute dispatch (`copy_mip0`) before the down-sampling pass; - performs the down-sampling itself in two compute dispatches to fit within subgroup limits; - heavily inspired by Jasmine's prototype code. Pre-filtering pipeline: - generates the specular Radiance Map using bounded-VNDF GGX importance sampling for higher quality highlights and fewer fireflies; - computes the diffuse Irradiance Map with cosine-weighted hemisphere sampling; - mirrors the forward-/reverse-tonemap workflow used by TAA instead of exposing a separate *white-point* parameter; - is based on the resources below together with the “Bounded VNDF Sampling for Smith-GGX Reflections” paper. The pre-filtering pipeline is largely based on these articles: - https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/ - https://bruop.github.io/ibl/ - https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf > The forward-/reverse-tonemap trick removes almost all fireflies without the need for a separate white-point parameter. Previous work: #9414 ## Testing The `reflection_probes.rs` example has been updated: - The camera starts closer to the spheres so the reflections are easier to see. - The GLTF scene is spawned only when the reflection probe mode is active (press Space). - The third display mode (toggled with Space) shows the generated cubemap chain. - You can change the roughness of the center sphere with the Up/Down keys. ## Render Graph Composed of two nodes and a graph edge: ``` Downsampling -> Filtering ``` Pass breakdown: ``` dowsampling_first_pass -> dowsampling_second_pass -> radiance_map_pass -> irradiance_map_pass ``` <img width="1601" height="2281" alt="render-graph" src="https://github.com/user-attachments/assets/3c240688-32f7-447a-9ede-6050b77c0bd1" /> --- ## Showcase <img width="2564" height="1500" alt="image" src="https://github.com/user-attachments/assets/56e68dd7-9488-4d35-9bba-7f713a3e2831" /> User facing API: ```rust commands.entity(camera) .insert(GeneratedEnvironmentMapLight { environment_map: world.load_asset("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), ..default() }); ``` ## 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 no longer bundles any large test textures. --------- Co-authored-by: atlas <email@atlasdostal.com>
Objective
Solution
GenerateEnvironmentMapLight
component.Changelog
GenerateEnvironmentMapLight
for automatically generating anEnvironmentMapLight
component from aSkybox
component. This can be used instead of KhronosGroup'sglTF-IBL-Sampler
.