Skip to content

Commit fcd7c0f

Browse files
JMS55superdumpGitGhilliecorehatlv24
authored
Exposure settings (adopted) (#11347)
Rebased and finished version of #8407. Huge thanks to @GitGhillie for adjusting all the examples, and the many other people who helped write this PR (@superdump , @coreh , among others) :) Fixes #8369 --- ## Changelog - Added a `brightness` control to `Skybox`. - Added an `intensity` control to `EnvironmentMapLight`. - Added `ExposureSettings` and `PhysicalCameraParameters` for controlling exposure of 3D cameras. - Removed the baked-in `DirectionalLight` exposure Bevy previously hardcoded internally. ## Migration Guide - If using a `Skybox` or `EnvironmentMapLight`, use the new `brightness` and `intensity` controls to adjust their strength. - All 3D scene will now have different apparent brightnesses due to Bevy implementing proper exposure controls. You will have to adjust the intensity of your lights and/or your camera exposure via the new `ExposureSettings` component to compensate. --------- Co-authored-by: Robert Swain <[email protected]> Co-authored-by: GitGhillie <[email protected]> Co-authored-by: Marco Buono <[email protected]> Co-authored-by: vero <[email protected]> Co-authored-by: atlas dostal <[email protected]>
1 parent 184f233 commit fcd7c0f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+436
-131
lines changed

crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,17 @@ impl ViewNode for MainOpaquePass3dNode {
7676
}
7777

7878
// Draw the skybox using a fullscreen triangle
79-
if let (Some(skybox_pipeline), Some(skybox_bind_group)) =
79+
if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) =
8080
(skybox_pipeline, skybox_bind_group)
8181
{
8282
let pipeline_cache = world.resource::<PipelineCache>();
8383
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) {
8484
render_pass.set_render_pipeline(pipeline);
85-
render_pass.set_bind_group(0, &skybox_bind_group.0, &[view_uniform_offset.offset]);
85+
render_pass.set_bind_group(
86+
0,
87+
&skybox_bind_group.0,
88+
&[view_uniform_offset.offset, skybox_bind_group.1],
89+
);
8690
render_pass.draw(0..3, 0..1);
8791
}
8892
}

crates/bevy_core_pipeline/src/skybox/mod.rs

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@ use bevy_app::{App, Plugin};
22
use bevy_asset::{load_internal_asset, Handle};
33
use bevy_ecs::{
44
prelude::{Component, Entity},
5-
query::With,
5+
query::{QueryItem, With},
66
schedule::IntoSystemConfigs,
77
system::{Commands, Query, Res, ResMut, Resource},
88
};
99
use bevy_render::{
10-
extract_component::{ExtractComponent, ExtractComponentPlugin},
10+
camera::ExposureSettings,
11+
extract_component::{
12+
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
13+
UniformComponentPlugin,
14+
},
1115
render_asset::RenderAssets,
1216
render_resource::{
1317
binding_types::{sampler, texture_cube, uniform_buffer},
14-
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
15-
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
16-
DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState,
17-
RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages,
18-
SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState,
19-
TextureFormat, TextureSampleType, VertexState,
18+
*,
2019
},
2120
renderer::RenderDevice,
2221
texture::{BevyDefault, Image},
@@ -34,7 +33,10 @@ impl Plugin for SkyboxPlugin {
3433
fn build(&self, app: &mut App) {
3534
load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl);
3635

37-
app.add_plugins(ExtractComponentPlugin::<Skybox>::default());
36+
app.add_plugins((
37+
ExtractComponentPlugin::<Skybox>::default(),
38+
UniformComponentPlugin::<SkyboxUniforms>::default(),
39+
));
3840

3941
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
4042
return;
@@ -68,8 +70,41 @@ impl Plugin for SkyboxPlugin {
6870
/// To do so, use `EnvironmentMapLight` alongside this component.
6971
///
7072
/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
71-
#[derive(Component, ExtractComponent, Clone)]
72-
pub struct Skybox(pub Handle<Image>);
73+
#[derive(Component, Clone)]
74+
pub struct Skybox {
75+
pub image: Handle<Image>,
76+
/// Scale factor applied to the skybox image.
77+
/// After applying this multiplier to the image samples, the resulting values should
78+
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
79+
pub brightness: f32,
80+
}
81+
82+
impl ExtractComponent for Skybox {
83+
type Data = (&'static Self, Option<&'static ExposureSettings>);
84+
type Filter = ();
85+
type Out = (Self, SkyboxUniforms);
86+
87+
fn extract_component(
88+
(skybox, exposure_settings): QueryItem<'_, Self::Data>,
89+
) -> Option<Self::Out> {
90+
let exposure = exposure_settings
91+
.map(|e| e.exposure())
92+
.unwrap_or_else(|| ExposureSettings::default().exposure());
93+
94+
Some((
95+
skybox.clone(),
96+
SkyboxUniforms {
97+
brightness: skybox.brightness * exposure,
98+
},
99+
))
100+
}
101+
}
102+
103+
// TODO: Replace with a push constant once WebGPU gets support for that
104+
#[derive(Component, ShaderType, Clone)]
105+
pub struct SkyboxUniforms {
106+
brightness: f32,
107+
}
73108

74109
#[derive(Resource)]
75110
struct SkyboxPipeline {
@@ -88,6 +123,7 @@ impl SkyboxPipeline {
88123
sampler(SamplerBindingType::Filtering),
89124
uniform_buffer::<ViewUniform>(true)
90125
.visibility(ShaderStages::VERTEX_FRAGMENT),
126+
uniform_buffer::<SkyboxUniforms>(true),
91127
),
92128
),
93129
),
@@ -186,31 +222,37 @@ fn prepare_skybox_pipelines(
186222
}
187223

188224
#[derive(Component)]
189-
pub struct SkyboxBindGroup(pub BindGroup);
225+
pub struct SkyboxBindGroup(pub (BindGroup, u32));
190226

191227
fn prepare_skybox_bind_groups(
192228
mut commands: Commands,
193229
pipeline: Res<SkyboxPipeline>,
194230
view_uniforms: Res<ViewUniforms>,
231+
skybox_uniforms: Res<ComponentUniforms<SkyboxUniforms>>,
195232
images: Res<RenderAssets<Image>>,
196233
render_device: Res<RenderDevice>,
197-
views: Query<(Entity, &Skybox)>,
234+
views: Query<(Entity, &Skybox, &DynamicUniformIndex<SkyboxUniforms>)>,
198235
) {
199-
for (entity, skybox) in &views {
200-
if let (Some(skybox), Some(view_uniforms)) =
201-
(images.get(&skybox.0), view_uniforms.uniforms.binding())
202-
{
236+
for (entity, skybox, skybox_uniform_index) in &views {
237+
if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = (
238+
images.get(&skybox.image),
239+
view_uniforms.uniforms.binding(),
240+
skybox_uniforms.binding(),
241+
) {
203242
let bind_group = render_device.create_bind_group(
204243
"skybox_bind_group",
205244
&pipeline.bind_group_layout,
206245
&BindGroupEntries::sequential((
207246
&skybox.texture_view,
208247
&skybox.sampler,
209248
view_uniforms,
249+
skybox_uniforms,
210250
)),
211251
);
212252

213-
commands.entity(entity).insert(SkyboxBindGroup(bind_group));
253+
commands
254+
.entity(entity)
255+
.insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index())));
214256
}
215257
}
216258
}

crates/bevy_core_pipeline/src/skybox/skybox.wgsl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@group(0) @binding(0) var skybox: texture_cube<f32>;
55
@group(0) @binding(1) var skybox_sampler: sampler;
66
@group(0) @binding(2) var<uniform> view: View;
7+
@group(0) @binding(3) var<uniform> brightness: f32;
78

89
fn coords_to_ray_direction(position: vec2<f32>, viewport: vec4<f32>) -> vec3<f32> {
910
// Using world positions of the fragment and camera to calculate a ray direction
@@ -62,5 +63,5 @@ fn skybox_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
6263
let ray_direction = coords_to_ray_direction(in.position.xy, view.viewport);
6364

6465
// Cube maps are left-handed so we negate the z coordinate.
65-
return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0));
66+
return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)) * brightness;
6667
}

crates/bevy_pbr/src/environment_map/environment_map.wgsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn environment_map_light(
4444
let kD = diffuse_color * Edss;
4545

4646
var out: EnvironmentMapLight;
47-
out.diffuse = (FmsEms + kD) * irradiance;
48-
out.specular = FssEss * radiance;
47+
out.diffuse = (FmsEms + kD) * irradiance * bindings::lights.environment_map_intensity;
48+
out.specular = FssEss * radiance * bindings::lights.environment_map_intensity;
4949
return out;
5050
}

crates/bevy_pbr/src/environment_map/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ impl Plugin for EnvironmentMapPlugin {
5151
pub struct EnvironmentMapLight {
5252
pub diffuse_map: Handle<Image>,
5353
pub specular_map: Handle<Image>,
54+
/// Scale factor applied to the diffuse and specular light generated by this component.
55+
///
56+
/// After applying this multiplier, the resulting values should
57+
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
58+
///
59+
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
60+
pub intensity: f32,
5461
}
5562

5663
impl EnvironmentMapLight {

crates/bevy_pbr/src/light.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ fn calculate_cascade(
557557
/// # use bevy_ecs::system::ResMut;
558558
/// # use bevy_pbr::AmbientLight;
559559
/// fn setup_ambient_light(mut ambient_light: ResMut<AmbientLight>) {
560-
/// ambient_light.brightness = 0.3;
560+
/// ambient_light.brightness = 20.0;
561561
/// }
562562
/// ```
563563
#[derive(Resource, Clone, Debug, ExtractResource, Reflect)]
@@ -572,7 +572,7 @@ impl Default for AmbientLight {
572572
fn default() -> Self {
573573
Self {
574574
color: Color::rgb(1.0, 1.0, 1.0),
575-
brightness: 0.05,
575+
brightness: 8.0,
576576
}
577577
}
578578
}

crates/bevy_pbr/src/render/light.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ pub struct GpuLights {
197197
// offset from spot light's light index to spot light's shadow map index
198198
spot_light_shadowmap_offset: i32,
199199
environment_map_smallest_specular_mip_level: u32,
200+
environment_map_intensity: f32,
200201
}
201202

202203
// NOTE: this must be kept in sync with the same constants in pbr.frag
@@ -857,18 +858,6 @@ pub fn prepare_lights(
857858
flags |= DirectionalLightFlags::SHADOWS_ENABLED;
858859
}
859860

860-
// convert from illuminance (lux) to candelas
861-
//
862-
// exposure is hard coded at the moment but should be replaced
863-
// by values coming from the camera
864-
// see: https://google.github.io/filament/Filament.html#imagingpipeline/physicallybasedcamera/exposuresettings
865-
const APERTURE: f32 = 4.0;
866-
const SHUTTER_SPEED: f32 = 1.0 / 250.0;
867-
const SENSITIVITY: f32 = 100.0;
868-
let ev100 = f32::log2(APERTURE * APERTURE / SHUTTER_SPEED) - f32::log2(SENSITIVITY / 100.0);
869-
let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2);
870-
let intensity = light.illuminance * exposure;
871-
872861
let num_cascades = light
873862
.cascade_shadow_config
874863
.bounds
@@ -877,9 +866,9 @@ pub fn prepare_lights(
877866
gpu_directional_lights[index] = GpuDirectionalLight {
878867
// Filled in later.
879868
cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT],
880-
// premultiply color by intensity
869+
// premultiply color by illuminance
881870
// we don't use the alpha at all, so no reason to multiply only [0..3]
882-
color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * intensity,
871+
color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * light.illuminance,
883872
// direction is negated to be ready for N.L
884873
dir_to_light: light.transform.back(),
885874
flags: flags.bits(),
@@ -972,6 +961,9 @@ pub fn prepare_lights(
972961
.and_then(|env_map| images.get(&env_map.specular_map))
973962
.map(|specular_map| specular_map.mip_level_count - 1)
974963
.unwrap_or(0),
964+
environment_map_intensity: environment_map
965+
.map(|env_map| env_map.intensity)
966+
.unwrap_or(1.0),
975967
};
976968

977969
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query

crates/bevy_pbr/src/render/mesh_view_types.wgsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ struct Lights {
5858
n_directional_lights: u32,
5959
spot_light_shadowmap_offset: i32,
6060
environment_map_smallest_specular_mip_level: u32,
61+
environment_map_intensity: f32,
6162
};
6263

6364
struct Fog {

crates/bevy_pbr/src/render/pbr_functions.wgsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ fn apply_pbr_lighting(
386386

387387
// Total light
388388
output_color = vec4<f32>(
389-
transmitted_light + direct_light + indirect_light + emissive_light,
389+
view_bindings::view.exposure * (transmitted_light + direct_light + indirect_light + emissive_light),
390390
output_color.a
391391
);
392392

@@ -423,7 +423,7 @@ fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4<f32>, fragment_
423423
0.0
424424
),
425425
fog_params.directional_light_exponent
426-
) * light.color.rgb;
426+
) * light.color.rgb * view_bindings::view.exposure;
427427
}
428428
}
429429

crates/bevy_pbr/src/render/pbr_transmission.wgsl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ fn specular_transmissive_light(world_position: vec4<f32>, frag_coord: vec3<f32>,
4242
background_color = fetch_transmissive_background(offset_position, frag_coord, view_z, perceptual_roughness);
4343
}
4444

45+
// Compensate for exposure, since the background color is coming from an already exposure-adjusted texture
46+
background_color = vec4(background_color.rgb / view_bindings::view.exposure, background_color.a);
47+
4548
// Dot product of the refracted direction with the exit normal (Note: We assume the exit normal is the entry normal but inverted)
4649
let MinusNdotT = dot(-N, T);
4750

0 commit comments

Comments
 (0)