Skip to content

Commit 7b866ee

Browse files
mate-hatlv24
andauthored
Environment Map Filtering GPU pipeline (#19076)
# 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 protected]>
1 parent 6634300 commit 7b866ee

File tree

18 files changed

+2232
-117
lines changed

18 files changed

+2232
-117
lines changed

crates/bevy_light/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use cluster::{
2727
mod ambient_light;
2828
pub use ambient_light::AmbientLight;
2929
mod probe;
30-
pub use probe::{EnvironmentMapLight, IrradianceVolume, LightProbe};
30+
pub use probe::{EnvironmentMapLight, GeneratedEnvironmentMapLight, IrradianceVolume, LightProbe};
3131
mod volumetric;
3232
pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight};
3333
pub mod cascade;

crates/bevy_light/src/probe.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,38 @@ impl Default for EnvironmentMapLight {
108108
}
109109
}
110110

111+
/// A generated environment map that is filtered at runtime.
112+
///
113+
/// See `bevy_pbr::light_probe::generate` for detailed information.
114+
#[derive(Clone, Component, Reflect)]
115+
#[reflect(Component, Default, Clone)]
116+
pub struct GeneratedEnvironmentMapLight {
117+
/// Source cubemap to be filtered on the GPU, size must be a power of two.
118+
pub environment_map: Handle<Image>,
119+
120+
/// Scale factor applied to the diffuse and specular light generated by this
121+
/// component. Expressed in cd/m² (candela per square meter).
122+
pub intensity: f32,
123+
124+
/// World-space rotation applied to the cubemap.
125+
pub rotation: Quat,
126+
127+
/// Whether this light contributes diffuse lighting to meshes that already
128+
/// have baked lightmaps.
129+
pub affects_lightmapped_mesh_diffuse: bool,
130+
}
131+
132+
impl Default for GeneratedEnvironmentMapLight {
133+
fn default() -> Self {
134+
GeneratedEnvironmentMapLight {
135+
environment_map: Handle::default(),
136+
intensity: 0.0,
137+
rotation: Quat::IDENTITY,
138+
affects_lightmapped_mesh_diffuse: true,
139+
}
140+
}
141+
}
142+
111143
/// The component that defines an irradiance volume.
112144
///
113145
/// See `bevy_pbr::irradiance_volume` for detailed information.

crates/bevy_pbr/src/lib.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ pub mod prelude {
8888
};
8989
#[doc(hidden)]
9090
pub use bevy_light::{
91-
light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight, LightProbe, PointLight,
92-
SpotLight,
91+
light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight,
92+
GeneratedEnvironmentMapLight, LightProbe, PointLight, SpotLight,
9393
};
9494
}
9595

@@ -133,7 +133,7 @@ pub mod graph {
133133

134134
use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr};
135135
use bevy_app::prelude::*;
136-
use bevy_asset::{AssetApp, AssetPath, Assets, Handle};
136+
use bevy_asset::{embedded_asset, load_embedded_asset, AssetApp, AssetPath, Assets, Handle};
137137
use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
138138
use bevy_ecs::prelude::*;
139139
use bevy_image::Image;
@@ -185,6 +185,13 @@ impl Default for PbrPlugin {
185185
}
186186
}
187187

188+
/// A resource that stores the spatio-temporal blue noise texture.
189+
#[derive(Resource)]
190+
pub struct Bluenoise {
191+
/// Texture handle for spatio-temporal blue noise
192+
pub texture: Handle<Image>,
193+
}
194+
188195
impl Plugin for PbrPlugin {
189196
fn build(&self, app: &mut App) {
190197
load_shader_library!(app, "render/pbr_types.wgsl");
@@ -273,12 +280,19 @@ impl Plugin for PbrPlugin {
273280
},
274281
);
275282

283+
// Load the Spatio-temporal blue noise texture
284+
embedded_asset!(app, "stbn.ktx2");
285+
let bluenoise_texture = load_embedded_asset!(app, "stbn.ktx2");
286+
276287
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
277288
return;
278289
};
279290

280291
// Extract the required data from the main world
281292
render_app
293+
.insert_resource(Bluenoise {
294+
texture: bluenoise_texture,
295+
})
282296
.add_systems(
283297
RenderStartup,
284298
(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copy the base mip (level 0) from a source cubemap to a destination cubemap,
2+
// performing format conversion if needed (the destination is always rgba16float).
3+
// The alpha channel is filled with 1.0.
4+
5+
@group(0) @binding(0) var src_cubemap: texture_2d_array<f32>;
6+
@group(0) @binding(1) var dst_cubemap: texture_storage_2d_array<rgba16float, write>;
7+
8+
@compute
9+
@workgroup_size(8, 8, 1)
10+
fn copy(@builtin(global_invocation_id) global_id: vec3u) {
11+
let size = textureDimensions(src_cubemap).xy;
12+
13+
// Bounds check
14+
if (any(global_id.xy >= size)) {
15+
return;
16+
}
17+
18+
let color = textureLoad(src_cubemap, vec2u(global_id.xy), global_id.z, 0);
19+
20+
textureStore(dst_cubemap, vec2u(global_id.xy), global_id.z, vec4f(color.rgb, 1.0));
21+
}

0 commit comments

Comments
 (0)