From f7fdf437b8105b163c3cf13c0544ada5a66dd3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Tue, 12 Aug 2025 12:14:17 +0200 Subject: [PATCH 1/6] Atmosphere generated environment map lighting --- crates/bevy_light/src/lib.rs | 5 +- crates/bevy_light/src/probe.rs | 25 +- crates/bevy_pbr/src/atmosphere/environment.rs | 315 ++++++++++++++++++ .../bevy_pbr/src/atmosphere/environment.wgsl | 37 ++ crates/bevy_pbr/src/atmosphere/functions.wgsl | 2 +- crates/bevy_pbr/src/atmosphere/mod.rs | 22 +- crates/bevy_pbr/src/atmosphere/node.rs | 1 + .../bevy_pbr/src/atmosphere/render_sky.wgsl | 6 +- examples/3d/atmosphere.rs | 4 +- 9 files changed, 410 insertions(+), 7 deletions(-) create mode 100644 crates/bevy_pbr/src/atmosphere/environment.rs create mode 100644 crates/bevy_pbr/src/atmosphere/environment.wgsl diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index 324de212fab72..a2843acfe6f3f 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -27,7 +27,10 @@ use cluster::{ mod ambient_light; pub use ambient_light::AmbientLight; mod probe; -pub use probe::{EnvironmentMapLight, GeneratedEnvironmentMapLight, IrradianceVolume, LightProbe}; +pub use probe::{ + AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight, + IrradianceVolume, LightProbe, +}; mod volumetric; pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight}; pub mod cascade; diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index 11d316c8751c1..a3be4958d011a 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -2,7 +2,7 @@ use bevy_asset::Handle; use bevy_camera::visibility::Visibility; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::Quat; +use bevy_math::{Quat, UVec2}; use bevy_reflect::prelude::*; use bevy_transform::components::Transform; @@ -140,6 +140,29 @@ impl Default for GeneratedEnvironmentMapLight { } } +/// Generates a cubemap of the sky produced by the atmosphere shader. +/// +/// Attach to a `LightProbe` to capture reflections inside its volume. +#[derive(Component, Clone)] +pub struct AtmosphereEnvironmentMapLight { + /// Luminance multiplier in cd/m². + pub intensity: f32, + /// Whether the diffuse contribution should affect meshes that already have lightmaps. + pub affects_lightmapped_mesh_diffuse: bool, + /// Cubemap resolution in pixels (must be a power-of-two). + pub size: UVec2, +} + +impl Default for AtmosphereEnvironmentMapLight { + fn default() -> Self { + Self { + intensity: 1.0, + affects_lightmapped_mesh_diffuse: true, + size: UVec2::new(512, 512), + } + } +} + /// The component that defines an irradiance volume. /// /// See `bevy_pbr::irradiance_volume` for detailed information. diff --git a/crates/bevy_pbr/src/atmosphere/environment.rs b/crates/bevy_pbr/src/atmosphere/environment.rs new file mode 100644 index 0000000000000..29c13a24f6ea0 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -0,0 +1,315 @@ +use crate::{ + resources::{ + AtmosphereSamplers, AtmosphereTextures, AtmosphereTransform, AtmosphereTransforms, + AtmosphereTransformsOffset, + }, + AtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset, +}; +use bevy_asset::{load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryState, With, Without}, + resource::Resource, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::Image; +use bevy_light::{AtmosphereEnvironmentMapLight, GeneratedEnvironmentMapLight}; +use bevy_math::{Quat, UVec2}; +use bevy_render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex, ExtractComponent}, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::{binding_types::*, *}, + renderer::{RenderContext, RenderDevice}, + texture::{CachedTexture, GpuImage}, + view::{ViewUniform, ViewUniformOffset, ViewUniforms}, +}; +use bevy_utils::default; + +use super::Atmosphere; + +// Render world representation of an environment map light for the atmosphere +#[derive(Component, ExtractComponent, Clone)] +pub struct AtmosphereEnvironmentMap { + pub environment_map: Handle, + pub size: UVec2, +} + +#[derive(Component)] +pub struct AtmosphereProbeTextures { + pub environment: TextureView, + pub transmittance_lut: CachedTexture, + pub multiscattering_lut: CachedTexture, + pub sky_view_lut: CachedTexture, + pub aerial_view_lut: CachedTexture, +} + +#[derive(Component)] +pub(crate) struct AtmosphereProbeBindGroups { + pub environment: BindGroup, +} + +#[derive(Resource)] +pub struct AtmosphereProbeLayouts { + pub environment: BindGroupLayout, +} + +#[derive(Resource)] +pub struct AtmosphereProbePipelines { + pub environment: CachedComputePipelineId, +} + +pub fn prepare_atmosphere_probe_layout(mut commands: Commands, render_device: Res) { + let environment = render_device.create_bind_group_layout( + "environment_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + uniform_buffer::(true), + uniform_buffer::(true), + uniform_buffer::(true), + uniform_buffer::(true), + uniform_buffer::(true), + texture_2d(TextureSampleType::Float { filterable: true }), //transmittance lut and sampler + sampler(SamplerBindingType::Filtering), + texture_2d(TextureSampleType::Float { filterable: true }), //multiscattering lut and sampler + sampler(SamplerBindingType::Filtering), + texture_2d(TextureSampleType::Float { filterable: true }), //sky view lut and sampler + sampler(SamplerBindingType::Filtering), + texture_3d(TextureSampleType::Float { filterable: true }), //aerial view lut ans sampler + sampler(SamplerBindingType::Filtering), + texture_storage_2d_array( + // output 2D array texture + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ); + + commands.insert_resource(AtmosphereProbeLayouts { environment }); +} + +pub(super) fn prepare_atmosphere_probe_bind_groups( + probes: Query<(Entity, &AtmosphereProbeTextures), With>, + render_device: Res, + layouts: Res, + samplers: Res, + view_uniforms: Res, + lights_uniforms: Res, + atmosphere_transforms: Res, + atmosphere_uniforms: Res>, + settings_uniforms: Res>, + mut commands: Commands, +) { + for (entity, textures) in &probes { + let environment = render_device.create_bind_group( + "environment_bind_group", + &layouts.environment, + &BindGroupEntries::sequential(( + atmosphere_uniforms.binding().unwrap(), + settings_uniforms.binding().unwrap(), + atmosphere_transforms.uniforms().binding().unwrap(), + view_uniforms.uniforms.binding().unwrap(), + lights_uniforms.view_gpu_lights.binding().unwrap(), + &textures.transmittance_lut.default_view, + &samplers.transmittance_lut, + &textures.multiscattering_lut.default_view, + &samplers.multiscattering_lut, + &textures.sky_view_lut.default_view, + &samplers.sky_view_lut, + &textures.aerial_view_lut.default_view, + &samplers.aerial_view_lut, + &textures.environment, + )), + ); + + commands + .entity(entity) + .insert(AtmosphereProbeBindGroups { environment }); + } +} + +pub(super) fn prepare_probe_textures( + view_textures: Query<&AtmosphereTextures, With>, + probes: Query< + (Entity, &AtmosphereEnvironmentMap), + ( + With, + Without, + ), + >, + gpu_images: Res>, + mut commands: Commands, +) { + for (probe, render_env_map) in &probes { + let environment = gpu_images.get(&render_env_map.environment_map).unwrap(); + // create a cube view + let environment_view = environment.texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + // Get the first view entity's textures to borrow + if let Some(view_textures) = view_textures.iter().next() { + commands.entity(probe).insert(AtmosphereProbeTextures { + environment: environment_view, + transmittance_lut: view_textures.transmittance_lut.clone(), + multiscattering_lut: view_textures.multiscattering_lut.clone(), + sky_view_lut: view_textures.sky_view_lut.clone(), + aerial_view_lut: view_textures.aerial_view_lut.clone(), + }); + } + } +} + +pub fn prepare_atmosphere_probe_pipeline( + pipeline_cache: Res, + layouts: Res, + asset_server: Res, + mut commands: Commands, +) { + let environment = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("environment_pipeline".into()), + layout: vec![layouts.environment.clone()], + shader: load_embedded_asset!(asset_server.as_ref(), "environment.wgsl"), + ..default() + }); + commands.insert_resource(AtmosphereProbePipelines { environment }); +} + +pub fn prepare_atmosphere_probe_components( + probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without,)>, + mut commands: Commands, + mut images: ResMut>, +) { + for (entity, env_map_light) in &probes { + // Create a cubemap image in the main world that we can reference + let mut environment_image = Image::new_fill( + Extent3d { + width: env_map_light.size.x, + height: env_map_light.size.y, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + environment_image.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + ..Default::default() + }); + + environment_image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING + | TextureUsages::STORAGE_BINDING + | TextureUsages::COPY_SRC; + + // Add the image to assets to get a handle + let environment_handle = images.add(environment_image); + + commands.entity(entity).insert(AtmosphereEnvironmentMap { + environment_map: environment_handle.clone(), + size: env_map_light.size, + }); + + commands + .entity(entity) + .insert(GeneratedEnvironmentMapLight { + environment_map: environment_handle, + intensity: env_map_light.intensity, + rotation: Quat::IDENTITY, + affects_lightmapped_mesh_diffuse: env_map_light.affects_lightmapped_mesh_diffuse, + }); + } +} + +pub(super) struct EnvironmentNode { + main_view_query: QueryState<( + Read>, + Read>, + Read, + Read, + Read, + )>, + probe_query: QueryState<( + Read, + Read, + )>, +} + +impl FromWorld for EnvironmentNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + probe_query: QueryState::new(world), + } + } +} + +impl Node for EnvironmentNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.probe_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + let view_entity = graph.view_entity(); + + let Some(environment_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.environment) + else { + return Ok(()); + }; + + let (Ok(( + atmosphere_uniforms_offset, + settings_uniforms_offset, + atmosphere_transforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + )),) = (self.main_view_query.get_manual(world, view_entity),) + else { + return Ok(()); + }; + + for (bind_groups, env_map_light) in self.probe_query.iter_manual(world) { + let mut pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("environment_pass"), + timestamp_writes: None, + }); + + pass.set_pipeline(environment_pipeline); + pass.set_bind_group( + 0, + &bind_groups.environment, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + atmosphere_transforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + + pass.dispatch_workgroups( + env_map_light.size.x / 8, + env_map_light.size.y / 8, + 6, // 6 cubemap faces + ); + } + + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/atmosphere/environment.wgsl b/crates/bevy_pbr/src/atmosphere/environment.wgsl new file mode 100644 index 0000000000000..0ab056b64f1b2 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/environment.wgsl @@ -0,0 +1,37 @@ +#import bevy_pbr::{ + atmosphere::{ + functions::{direction_world_to_atmosphere, sample_sky_view_lut, view_radius}, + }, + utils::sample_cube_dir +} + +@group(0) @binding(13) var output: texture_storage_2d_array; + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let dimensions = textureDimensions(output); + let slice_index = global_id.z; + + if (global_id.x >= dimensions.x || global_id.y >= dimensions.y || slice_index >= 6u) { + return; + } + + // Calculate normalized UV coordinates for this pixel + let uv = vec2( + (f32(global_id.x) + 0.5) / f32(dimensions.x), + (f32(global_id.y) + 0.5) / f32(dimensions.y) + ); + + let r = view_radius(); + + var ray_dir_ws = sample_cube_dir(uv, slice_index); + + // invert the z direction to match + ray_dir_ws = vec3(ray_dir_ws.x, ray_dir_ws.y, ray_dir_ws.z * -1.0); + + let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz); + let inscattering = sample_sky_view_lut(r, ray_dir_as); + let color = vec4(inscattering, 1.0); + + textureStore(output, vec2(global_id.xy), i32(slice_index), color); +} \ No newline at end of file diff --git a/crates/bevy_pbr/src/atmosphere/functions.wgsl b/crates/bevy_pbr/src/atmosphere/functions.wgsl index c1f02fc921c88..86b1a94b7e8d0 100644 --- a/crates/bevy_pbr/src/atmosphere/functions.wgsl +++ b/crates/bevy_pbr/src/atmosphere/functions.wgsl @@ -272,7 +272,7 @@ fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3::default(), ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), UniformComponentPlugin::::default(), UniformComponentPlugin::::default(), - )); + )) + .add_systems(Update, prepare_atmosphere_probe_components); } fn finish(&self, app: &mut App) { @@ -139,6 +148,14 @@ impl Plugin for AtmospherePlugin { configure_camera_depth_usages.in_set(RenderSystems::ManageViews), queue_render_sky_pipelines.in_set(RenderSystems::Queue), prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources), + prepare_atmosphere_probe_layout.in_set(RenderSystems::PrepareResources), + prepare_probe_textures + .in_set(RenderSystems::PrepareResources) + .after(prepare_atmosphere_textures), + prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_atmosphere_probe_pipeline + .in_set(RenderSystems::PrepareResources) + .after(prepare_atmosphere_probe_layout), prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), @@ -160,6 +177,7 @@ impl Plugin for AtmospherePlugin { Core3d, AtmosphereNode::RenderSky, ) + .add_render_graph_node::(Core3d, AtmosphereNode::Environment) .add_render_graph_edges( Core3d, ( diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 93c1a33ae9db2..eb4ba81666f66 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -23,6 +23,7 @@ use super::{ pub enum AtmosphereNode { RenderLuts, RenderSky, + Environment, } #[derive(Default)] diff --git a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl index f8298272caa69..182415fbee32d 100644 --- a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl +++ b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -46,12 +46,16 @@ fn main(in: FullscreenVertexOutput) -> RenderSkyOutput { let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz); transmittance = sample_transmittance_lut(r, mu); inscattering += sample_sky_view_lut(r, ray_dir_as); - inscattering += sun_radiance * transmittance * view.exposure; + inscattering += sun_radiance * transmittance; } else { let t = ndc_to_camera_dist(vec3(uv_to_ndc(in.uv), depth)); inscattering = sample_aerial_view_lut(in.uv, t); transmittance = sample_transmittance_lut_segment(r, mu, t); } + + // exposure compensation + inscattering *= view.exposure; + #ifdef DUAL_SOURCE_BLENDING return RenderSkyOutput(vec4(inscattering, 0.0), vec4(transmittance, 1.0)); #else diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index 1ba2d8e9f908f..12f6af608e557 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -5,7 +5,7 @@ use std::f32::consts::PI; use bevy::{ camera::Exposure, core_pipeline::{bloom::Bloom, tonemapping::Tonemapping}, - light::{light_consts::lux, CascadeShadowConfigBuilder}, + light::{light_consts::lux, AtmosphereEnvironmentMapLight, CascadeShadowConfigBuilder}, pbr::{Atmosphere, AtmosphereSettings}, prelude::*, }; @@ -42,6 +42,8 @@ fn setup_camera_fog(mut commands: Commands) { Tonemapping::AcesFitted, // Bloom gives the sun a much more natural look. Bloom::NATURAL, + // Renders the atmosphere to the environment map from the perspective of the camera + AtmosphereEnvironmentMapLight::default(), )); } From daa0d57178c5e50b4964252e98122c6f409d6d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Wed, 13 Aug 2025 11:40:06 +0200 Subject: [PATCH 2/6] Rework user facing docs --- crates/bevy_light/src/probe.rs | 16 +++++++++++++--- examples/3d/atmosphere.rs | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index a3be4958d011a..e98ccce7117d3 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -140,12 +140,22 @@ impl Default for GeneratedEnvironmentMapLight { } } -/// Generates a cubemap of the sky produced by the atmosphere shader. +/// Lets the atmosphere contribute environment lighting (reflections and ambient diffuse) to your scene. /// -/// Attach to a `LightProbe` to capture reflections inside its volume. +/// Attach this to a [`Camera3d`](bevy_camera::Camera3d) to light the entire view, or to a +/// [`LightProbe`](crate::LightProbe) to light only a specific region. +/// Behind the scenes, this generates an environment map from the atmosphere for image-based lighting +/// and inserts a corresponding [`GeneratedEnvironmentMapLight`]. +/// +/// For HDRI-based lighting, use a preauthored [`EnvironmentMapLight`] or filter one at runtime with +/// [`GeneratedEnvironmentMapLight`]. #[derive(Component, Clone)] pub struct AtmosphereEnvironmentMapLight { - /// Luminance multiplier in cd/m². + /// Controls how bright the atmosphere's environment lighting is. + /// Increase this value to brighten reflections and ambient diffuse lighting. + /// + /// The default is `1.0` so that the generated environment lighting matches + /// the light intensity of the atmosphere in the scene. pub intensity: f32, /// Whether the diffuse contribution should affect meshes that already have lightmaps. pub affects_lightmapped_mesh_diffuse: bool, diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index 12f6af608e557..2ad87bb6a8edc 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -42,7 +42,7 @@ fn setup_camera_fog(mut commands: Commands) { Tonemapping::AcesFitted, // Bloom gives the sun a much more natural look. Bloom::NATURAL, - // Renders the atmosphere to the environment map from the perspective of the camera + // Enables the atmosphere to drive reflections and ambient lighting (IBL) for this view AtmosphereEnvironmentMapLight::default(), )); } From 95044e541676375102c62958b729a216a8c425be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Wed, 13 Aug 2025 11:40:37 +0200 Subject: [PATCH 3/6] Cubemap left handed clarity --- crates/bevy_pbr/src/atmosphere/environment.wgsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/atmosphere/environment.wgsl b/crates/bevy_pbr/src/atmosphere/environment.wgsl index 0ab056b64f1b2..099aabb84771c 100644 --- a/crates/bevy_pbr/src/atmosphere/environment.wgsl +++ b/crates/bevy_pbr/src/atmosphere/environment.wgsl @@ -26,8 +26,8 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { var ray_dir_ws = sample_cube_dir(uv, slice_index); - // invert the z direction to match - ray_dir_ws = vec3(ray_dir_ws.x, ray_dir_ws.y, ray_dir_ws.z * -1.0); + // invert the z direction to account for cubemaps being lefthanded + ray_dir_ws.z = -ray_dir_ws.z; let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz); let inscattering = sample_sky_view_lut(r, ray_dir_as); From 01cf341f4bef2608791a22b7554c294e19ead6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Wed, 13 Aug 2025 11:41:02 +0200 Subject: [PATCH 4/6] Validate power of two envmap size --- crates/bevy_pbr/src/atmosphere/environment.rs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/atmosphere/environment.rs b/crates/bevy_pbr/src/atmosphere/environment.rs index 29c13a24f6ea0..2a81db49636ce 100644 --- a/crates/bevy_pbr/src/atmosphere/environment.rs +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -27,6 +27,7 @@ use bevy_render::{ view::{ViewUniform, ViewUniformOffset, ViewUniforms}, }; use bevy_utils::default; +use tracing::warn; use super::Atmosphere; @@ -179,6 +180,21 @@ pub fn prepare_atmosphere_probe_pipeline( commands.insert_resource(AtmosphereProbePipelines { environment }); } +// Ensure power-of-two dimensions to avoid edge update issues on cubemap faces +pub fn validate_environment_map_size(size: UVec2) -> UVec2 { + let new_size = UVec2::new( + size.x.max(1).next_power_of_two(), + size.y.max(1).next_power_of_two(), + ); + if new_size != size { + warn!( + "Non-power-of-two AtmosphereEnvironmentMapLight size {}, correcting to {new_size}", + size + ); + } + new_size +} + pub fn prepare_atmosphere_probe_components( probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without,)>, mut commands: Commands, @@ -186,10 +202,11 @@ pub fn prepare_atmosphere_probe_components( ) { for (entity, env_map_light) in &probes { // Create a cubemap image in the main world that we can reference + let new_size = validate_environment_map_size(env_map_light.size); let mut environment_image = Image::new_fill( Extent3d { - width: env_map_light.size.x, - height: env_map_light.size.y, + width: new_size.x, + height: new_size.y, depth_or_array_layers: 6, }, TextureDimension::D2, @@ -212,7 +229,7 @@ pub fn prepare_atmosphere_probe_components( commands.entity(entity).insert(AtmosphereEnvironmentMap { environment_map: environment_handle.clone(), - size: env_map_light.size, + size: new_size, }); commands From ddf4737d0a37e1230e5ecd0838180d8b83e3b8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Wed, 13 Aug 2025 11:51:29 +0200 Subject: [PATCH 5/6] Systems naming and scheduling adjustments --- crates/bevy_pbr/src/atmosphere/environment.rs | 4 ++-- crates/bevy_pbr/src/atmosphere/mod.rs | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/bevy_pbr/src/atmosphere/environment.rs b/crates/bevy_pbr/src/atmosphere/environment.rs index 2a81db49636ce..e4e3ef8cbed82 100644 --- a/crates/bevy_pbr/src/atmosphere/environment.rs +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -62,7 +62,7 @@ pub struct AtmosphereProbePipelines { pub environment: CachedComputePipelineId, } -pub fn prepare_atmosphere_probe_layout(mut commands: Commands, render_device: Res) { +pub fn init_atmosphere_probe_layout(mut commands: Commands, render_device: Res) { let environment = render_device.create_bind_group_layout( "environment_bind_group_layout", &BindGroupLayoutEntries::sequential( @@ -165,7 +165,7 @@ pub(super) fn prepare_probe_textures( } } -pub fn prepare_atmosphere_probe_pipeline( +pub fn queue_atmosphere_probe_pipelines( pipeline_cache: Res, layouts: Res, asset_server: Res, diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index aabbb9c8ddf84..12aceab2feef1 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -53,6 +53,7 @@ use bevy_render::{ extract_component::UniformComponentPlugin, render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines}, view::Hdr, + RenderStartup, }; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, @@ -65,8 +66,8 @@ use bevy_render::{ use bevy_core_pipeline::core_3d::graph::Core3d; use bevy_shader::load_shader_library; use environment::{ - prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components, - prepare_atmosphere_probe_layout, prepare_atmosphere_probe_pipeline, prepare_probe_textures, + init_atmosphere_probe_layout, prepare_atmosphere_probe_bind_groups, + prepare_atmosphere_probe_components, prepare_probe_textures, queue_atmosphere_probe_pipelines, AtmosphereEnvironmentMap, EnvironmentNode, }; use resources::{ @@ -142,20 +143,20 @@ impl Plugin for AtmospherePlugin { .init_resource::() .init_resource::() .init_resource::>() + .add_systems(RenderStartup, init_atmosphere_probe_layout) .add_systems( Render, ( configure_camera_depth_usages.in_set(RenderSystems::ManageViews), queue_render_sky_pipelines.in_set(RenderSystems::Queue), prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources), - prepare_atmosphere_probe_layout.in_set(RenderSystems::PrepareResources), prepare_probe_textures .in_set(RenderSystems::PrepareResources) .after(prepare_atmosphere_textures), prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), - prepare_atmosphere_probe_pipeline - .in_set(RenderSystems::PrepareResources) - .after(prepare_atmosphere_probe_layout), + queue_atmosphere_probe_pipelines + .in_set(RenderSystems::Queue) + .after(init_atmosphere_probe_layout), prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), From f82a86ac922b17010260c4ac22814dae2028be39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Wed, 13 Aug 2025 12:06:37 +0200 Subject: [PATCH 6/6] Fix CI --- crates/bevy_light/src/probe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index e98ccce7117d3..213a02b4a0619 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -143,7 +143,7 @@ impl Default for GeneratedEnvironmentMapLight { /// Lets the atmosphere contribute environment lighting (reflections and ambient diffuse) to your scene. /// /// Attach this to a [`Camera3d`](bevy_camera::Camera3d) to light the entire view, or to a -/// [`LightProbe`](crate::LightProbe) to light only a specific region. +/// [`LightProbe`] to light only a specific region. /// Behind the scenes, this generates an environment map from the atmosphere for image-based lighting /// and inserts a corresponding [`GeneratedEnvironmentMapLight`]. ///