Skip to content

Commit ae143d4

Browse files
authored
Atmosphere generated environment map lighting (#20529)
# Objective - Add generated environment lighting to the atmosphere for reflections and diffuse - Tracking issue: #20374 ## Solution - created a new atmosphere node type called environment - up-sampling the existing sky-view lookup texture and tied to the current view only. this atmosphere cube map pass has negligible performance impact, however the filtering pipeline does have a small performance impact to the example. - using the new filtering pipeline, creates mip chain for different roughness levels as well as diffuse ## Testing - ran the atmosphere example --- ## Showcase <img width="1281" height="752" alt="Screenshot 2025-08-12 at 12 19 08 PM" src="https://github.com/user-attachments/assets/8f00ff25-5f48-4c51-b67e-abcbf421abc4" /> ```rs commands.spawn(( Camera3d::default(), // ... // Renders the atmosphere to the environment map from the perspective of the camera AtmosphereEnvironmentMapLight::default(), )); ``` ## Limitations, out of scope - The generation does not support light probes (yet). This allows to render the atmosphere as a light probe from any "location" within the scene and within the overall atmosphere. - therefore we assume that the relative scale of the scene to the atmosphere are orders of magnitude different , this is a safe assumption now with the current impl not officially supporting space views, yet - the PBR directional light is still unaffected by the atmosphere (not tinted by it) out of scope for this PR - unrelated issue to this PR: small black pixels around the fully reflective sphere. narrowed it down, this is a problem in the inscattering calculation based on the depth value in the render sky shader. however the addition of the env map light makes this problem more "apparent". to be addressed separately. - past work staged for further PRs, up next: mate-h#10
1 parent 452b187 commit ae143d4

File tree

9 files changed

+438
-7
lines changed

9 files changed

+438
-7
lines changed

crates/bevy_light/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use cluster::{
2727
mod ambient_light;
2828
pub use ambient_light::AmbientLight;
2929
mod probe;
30-
pub use probe::{EnvironmentMapLight, GeneratedEnvironmentMapLight, IrradianceVolume, LightProbe};
30+
pub use probe::{
31+
AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight,
32+
IrradianceVolume, LightProbe,
33+
};
3134
mod volumetric;
3235
pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight};
3336
pub mod cascade;

crates/bevy_light/src/probe.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bevy_asset::Handle;
22
use bevy_camera::visibility::Visibility;
33
use bevy_ecs::prelude::*;
44
use bevy_image::Image;
5-
use bevy_math::Quat;
5+
use bevy_math::{Quat, UVec2};
66
use bevy_reflect::prelude::*;
77
use bevy_transform::components::Transform;
88

@@ -140,6 +140,39 @@ impl Default for GeneratedEnvironmentMapLight {
140140
}
141141
}
142142

143+
/// Lets the atmosphere contribute environment lighting (reflections and ambient diffuse) to your scene.
144+
///
145+
/// Attach this to a [`Camera3d`](bevy_camera::Camera3d) to light the entire view, or to a
146+
/// [`LightProbe`] to light only a specific region.
147+
/// Behind the scenes, this generates an environment map from the atmosphere for image-based lighting
148+
/// and inserts a corresponding [`GeneratedEnvironmentMapLight`].
149+
///
150+
/// For HDRI-based lighting, use a preauthored [`EnvironmentMapLight`] or filter one at runtime with
151+
/// [`GeneratedEnvironmentMapLight`].
152+
#[derive(Component, Clone)]
153+
pub struct AtmosphereEnvironmentMapLight {
154+
/// Controls how bright the atmosphere's environment lighting is.
155+
/// Increase this value to brighten reflections and ambient diffuse lighting.
156+
///
157+
/// The default is `1.0` so that the generated environment lighting matches
158+
/// the light intensity of the atmosphere in the scene.
159+
pub intensity: f32,
160+
/// Whether the diffuse contribution should affect meshes that already have lightmaps.
161+
pub affects_lightmapped_mesh_diffuse: bool,
162+
/// Cubemap resolution in pixels (must be a power-of-two).
163+
pub size: UVec2,
164+
}
165+
166+
impl Default for AtmosphereEnvironmentMapLight {
167+
fn default() -> Self {
168+
Self {
169+
intensity: 1.0,
170+
affects_lightmapped_mesh_diffuse: true,
171+
size: UVec2::new(512, 512),
172+
}
173+
}
174+
}
175+
143176
/// The component that defines an irradiance volume.
144177
///
145178
/// See `bevy_pbr::irradiance_volume` for detailed information.
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
use crate::{
2+
resources::{
3+
AtmosphereSamplers, AtmosphereTextures, AtmosphereTransform, AtmosphereTransforms,
4+
AtmosphereTransformsOffset,
5+
},
6+
AtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset,
7+
};
8+
use bevy_asset::{load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages};
9+
use bevy_ecs::{
10+
component::Component,
11+
entity::Entity,
12+
query::{QueryState, With, Without},
13+
resource::Resource,
14+
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
15+
world::{FromWorld, World},
16+
};
17+
use bevy_image::Image;
18+
use bevy_light::{AtmosphereEnvironmentMapLight, GeneratedEnvironmentMapLight};
19+
use bevy_math::{Quat, UVec2};
20+
use bevy_render::{
21+
extract_component::{ComponentUniforms, DynamicUniformIndex, ExtractComponent},
22+
render_asset::RenderAssets,
23+
render_graph::{Node, NodeRunError, RenderGraphContext},
24+
render_resource::{binding_types::*, *},
25+
renderer::{RenderContext, RenderDevice},
26+
texture::{CachedTexture, GpuImage},
27+
view::{ViewUniform, ViewUniformOffset, ViewUniforms},
28+
};
29+
use bevy_utils::default;
30+
use tracing::warn;
31+
32+
use super::Atmosphere;
33+
34+
// Render world representation of an environment map light for the atmosphere
35+
#[derive(Component, ExtractComponent, Clone)]
36+
pub struct AtmosphereEnvironmentMap {
37+
pub environment_map: Handle<Image>,
38+
pub size: UVec2,
39+
}
40+
41+
#[derive(Component)]
42+
pub struct AtmosphereProbeTextures {
43+
pub environment: TextureView,
44+
pub transmittance_lut: CachedTexture,
45+
pub multiscattering_lut: CachedTexture,
46+
pub sky_view_lut: CachedTexture,
47+
pub aerial_view_lut: CachedTexture,
48+
}
49+
50+
#[derive(Component)]
51+
pub(crate) struct AtmosphereProbeBindGroups {
52+
pub environment: BindGroup,
53+
}
54+
55+
#[derive(Resource)]
56+
pub struct AtmosphereProbeLayouts {
57+
pub environment: BindGroupLayout,
58+
}
59+
60+
#[derive(Resource)]
61+
pub struct AtmosphereProbePipelines {
62+
pub environment: CachedComputePipelineId,
63+
}
64+
65+
pub fn init_atmosphere_probe_layout(mut commands: Commands, render_device: Res<RenderDevice>) {
66+
let environment = render_device.create_bind_group_layout(
67+
"environment_bind_group_layout",
68+
&BindGroupLayoutEntries::sequential(
69+
ShaderStages::COMPUTE,
70+
(
71+
uniform_buffer::<Atmosphere>(true),
72+
uniform_buffer::<AtmosphereSettings>(true),
73+
uniform_buffer::<AtmosphereTransform>(true),
74+
uniform_buffer::<ViewUniform>(true),
75+
uniform_buffer::<GpuLights>(true),
76+
texture_2d(TextureSampleType::Float { filterable: true }), //transmittance lut and sampler
77+
sampler(SamplerBindingType::Filtering),
78+
texture_2d(TextureSampleType::Float { filterable: true }), //multiscattering lut and sampler
79+
sampler(SamplerBindingType::Filtering),
80+
texture_2d(TextureSampleType::Float { filterable: true }), //sky view lut and sampler
81+
sampler(SamplerBindingType::Filtering),
82+
texture_3d(TextureSampleType::Float { filterable: true }), //aerial view lut ans sampler
83+
sampler(SamplerBindingType::Filtering),
84+
texture_storage_2d_array(
85+
// output 2D array texture
86+
TextureFormat::Rgba16Float,
87+
StorageTextureAccess::WriteOnly,
88+
),
89+
),
90+
),
91+
);
92+
93+
commands.insert_resource(AtmosphereProbeLayouts { environment });
94+
}
95+
96+
pub(super) fn prepare_atmosphere_probe_bind_groups(
97+
probes: Query<(Entity, &AtmosphereProbeTextures), With<AtmosphereEnvironmentMap>>,
98+
render_device: Res<RenderDevice>,
99+
layouts: Res<AtmosphereProbeLayouts>,
100+
samplers: Res<AtmosphereSamplers>,
101+
view_uniforms: Res<ViewUniforms>,
102+
lights_uniforms: Res<LightMeta>,
103+
atmosphere_transforms: Res<AtmosphereTransforms>,
104+
atmosphere_uniforms: Res<ComponentUniforms<Atmosphere>>,
105+
settings_uniforms: Res<ComponentUniforms<AtmosphereSettings>>,
106+
mut commands: Commands,
107+
) {
108+
for (entity, textures) in &probes {
109+
let environment = render_device.create_bind_group(
110+
"environment_bind_group",
111+
&layouts.environment,
112+
&BindGroupEntries::sequential((
113+
atmosphere_uniforms.binding().unwrap(),
114+
settings_uniforms.binding().unwrap(),
115+
atmosphere_transforms.uniforms().binding().unwrap(),
116+
view_uniforms.uniforms.binding().unwrap(),
117+
lights_uniforms.view_gpu_lights.binding().unwrap(),
118+
&textures.transmittance_lut.default_view,
119+
&samplers.transmittance_lut,
120+
&textures.multiscattering_lut.default_view,
121+
&samplers.multiscattering_lut,
122+
&textures.sky_view_lut.default_view,
123+
&samplers.sky_view_lut,
124+
&textures.aerial_view_lut.default_view,
125+
&samplers.aerial_view_lut,
126+
&textures.environment,
127+
)),
128+
);
129+
130+
commands
131+
.entity(entity)
132+
.insert(AtmosphereProbeBindGroups { environment });
133+
}
134+
}
135+
136+
pub(super) fn prepare_probe_textures(
137+
view_textures: Query<&AtmosphereTextures, With<Atmosphere>>,
138+
probes: Query<
139+
(Entity, &AtmosphereEnvironmentMap),
140+
(
141+
With<AtmosphereEnvironmentMap>,
142+
Without<AtmosphereProbeTextures>,
143+
),
144+
>,
145+
gpu_images: Res<RenderAssets<GpuImage>>,
146+
mut commands: Commands,
147+
) {
148+
for (probe, render_env_map) in &probes {
149+
let environment = gpu_images.get(&render_env_map.environment_map).unwrap();
150+
// create a cube view
151+
let environment_view = environment.texture.create_view(&TextureViewDescriptor {
152+
dimension: Some(TextureViewDimension::D2Array),
153+
..Default::default()
154+
});
155+
// Get the first view entity's textures to borrow
156+
if let Some(view_textures) = view_textures.iter().next() {
157+
commands.entity(probe).insert(AtmosphereProbeTextures {
158+
environment: environment_view,
159+
transmittance_lut: view_textures.transmittance_lut.clone(),
160+
multiscattering_lut: view_textures.multiscattering_lut.clone(),
161+
sky_view_lut: view_textures.sky_view_lut.clone(),
162+
aerial_view_lut: view_textures.aerial_view_lut.clone(),
163+
});
164+
}
165+
}
166+
}
167+
168+
pub fn queue_atmosphere_probe_pipelines(
169+
pipeline_cache: Res<PipelineCache>,
170+
layouts: Res<AtmosphereProbeLayouts>,
171+
asset_server: Res<AssetServer>,
172+
mut commands: Commands,
173+
) {
174+
let environment = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
175+
label: Some("environment_pipeline".into()),
176+
layout: vec![layouts.environment.clone()],
177+
shader: load_embedded_asset!(asset_server.as_ref(), "environment.wgsl"),
178+
..default()
179+
});
180+
commands.insert_resource(AtmosphereProbePipelines { environment });
181+
}
182+
183+
// Ensure power-of-two dimensions to avoid edge update issues on cubemap faces
184+
pub fn validate_environment_map_size(size: UVec2) -> UVec2 {
185+
let new_size = UVec2::new(
186+
size.x.max(1).next_power_of_two(),
187+
size.y.max(1).next_power_of_two(),
188+
);
189+
if new_size != size {
190+
warn!(
191+
"Non-power-of-two AtmosphereEnvironmentMapLight size {}, correcting to {new_size}",
192+
size
193+
);
194+
}
195+
new_size
196+
}
197+
198+
pub fn prepare_atmosphere_probe_components(
199+
probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without<AtmosphereEnvironmentMap>,)>,
200+
mut commands: Commands,
201+
mut images: ResMut<Assets<Image>>,
202+
) {
203+
for (entity, env_map_light) in &probes {
204+
// Create a cubemap image in the main world that we can reference
205+
let new_size = validate_environment_map_size(env_map_light.size);
206+
let mut environment_image = Image::new_fill(
207+
Extent3d {
208+
width: new_size.x,
209+
height: new_size.y,
210+
depth_or_array_layers: 6,
211+
},
212+
TextureDimension::D2,
213+
&[0; 8],
214+
TextureFormat::Rgba16Float,
215+
RenderAssetUsages::all(),
216+
);
217+
218+
environment_image.texture_view_descriptor = Some(TextureViewDescriptor {
219+
dimension: Some(TextureViewDimension::Cube),
220+
..Default::default()
221+
});
222+
223+
environment_image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING
224+
| TextureUsages::STORAGE_BINDING
225+
| TextureUsages::COPY_SRC;
226+
227+
// Add the image to assets to get a handle
228+
let environment_handle = images.add(environment_image);
229+
230+
commands.entity(entity).insert(AtmosphereEnvironmentMap {
231+
environment_map: environment_handle.clone(),
232+
size: new_size,
233+
});
234+
235+
commands
236+
.entity(entity)
237+
.insert(GeneratedEnvironmentMapLight {
238+
environment_map: environment_handle,
239+
intensity: env_map_light.intensity,
240+
rotation: Quat::IDENTITY,
241+
affects_lightmapped_mesh_diffuse: env_map_light.affects_lightmapped_mesh_diffuse,
242+
});
243+
}
244+
}
245+
246+
pub(super) struct EnvironmentNode {
247+
main_view_query: QueryState<(
248+
Read<DynamicUniformIndex<Atmosphere>>,
249+
Read<DynamicUniformIndex<AtmosphereSettings>>,
250+
Read<AtmosphereTransformsOffset>,
251+
Read<ViewUniformOffset>,
252+
Read<ViewLightsUniformOffset>,
253+
)>,
254+
probe_query: QueryState<(
255+
Read<AtmosphereProbeBindGroups>,
256+
Read<AtmosphereEnvironmentMap>,
257+
)>,
258+
}
259+
260+
impl FromWorld for EnvironmentNode {
261+
fn from_world(world: &mut World) -> Self {
262+
Self {
263+
main_view_query: QueryState::new(world),
264+
probe_query: QueryState::new(world),
265+
}
266+
}
267+
}
268+
269+
impl Node for EnvironmentNode {
270+
fn update(&mut self, world: &mut World) {
271+
self.main_view_query.update_archetypes(world);
272+
self.probe_query.update_archetypes(world);
273+
}
274+
275+
fn run(
276+
&self,
277+
graph: &mut RenderGraphContext,
278+
render_context: &mut RenderContext,
279+
world: &World,
280+
) -> Result<(), NodeRunError> {
281+
let pipeline_cache = world.resource::<PipelineCache>();
282+
let pipelines = world.resource::<AtmosphereProbePipelines>();
283+
let view_entity = graph.view_entity();
284+
285+
let Some(environment_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.environment)
286+
else {
287+
return Ok(());
288+
};
289+
290+
let (Ok((
291+
atmosphere_uniforms_offset,
292+
settings_uniforms_offset,
293+
atmosphere_transforms_offset,
294+
view_uniforms_offset,
295+
lights_uniforms_offset,
296+
)),) = (self.main_view_query.get_manual(world, view_entity),)
297+
else {
298+
return Ok(());
299+
};
300+
301+
for (bind_groups, env_map_light) in self.probe_query.iter_manual(world) {
302+
let mut pass =
303+
render_context
304+
.command_encoder()
305+
.begin_compute_pass(&ComputePassDescriptor {
306+
label: Some("environment_pass"),
307+
timestamp_writes: None,
308+
});
309+
310+
pass.set_pipeline(environment_pipeline);
311+
pass.set_bind_group(
312+
0,
313+
&bind_groups.environment,
314+
&[
315+
atmosphere_uniforms_offset.index(),
316+
settings_uniforms_offset.index(),
317+
atmosphere_transforms_offset.index(),
318+
view_uniforms_offset.offset,
319+
lights_uniforms_offset.offset,
320+
],
321+
);
322+
323+
pass.dispatch_workgroups(
324+
env_map_light.size.x / 8,
325+
env_map_light.size.y / 8,
326+
6, // 6 cubemap faces
327+
);
328+
}
329+
330+
Ok(())
331+
}
332+
}

0 commit comments

Comments
 (0)