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..213a02b4a0619 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,39 @@ 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`] 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 { + /// 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, + /// 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..e4e3ef8cbed82 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -0,0 +1,332 @@ +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 tracing::warn; + +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 init_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 queue_atmosphere_probe_pipelines( + 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 }); +} + +// 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, + mut images: ResMut>, +) { + 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: new_size.x, + height: new_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: new_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..099aabb84771c --- /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 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); + 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) { @@ -133,12 +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_probe_textures + .in_set(RenderSystems::PrepareResources) + .after(prepare_atmosphere_textures), + prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), + 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), ), @@ -160,6 +178,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..2ad87bb6a8edc 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, + // Enables the atmosphere to drive reflections and ambient lighting (IBL) for this view + AtmosphereEnvironmentMapLight::default(), )); }