Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposure settings #8407

Closed
wants to merge 16 commits into from
Closed
14 changes: 1 addition & 13 deletions crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,18 +854,6 @@ pub fn prepare_lights(
flags |= DirectionalLightFlags::SHADOWS_ENABLED;
}

// convert from illuminance (lux) to candelas
//
// exposure is hard coded at the moment but should be replaced
// by values coming from the camera
// see: https://google.github.io/filament/Filament.html#imagingpipeline/physicallybasedcamera/exposuresettings
const APERTURE: f32 = 4.0;
const SHUTTER_SPEED: f32 = 1.0 / 250.0;
const SENSITIVITY: f32 = 100.0;
let ev100 = f32::log2(APERTURE * APERTURE / SHUTTER_SPEED) - f32::log2(SENSITIVITY / 100.0);
let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2);
let intensity = light.illuminance * exposure;

let num_cascades = light
.cascade_shadow_config
.bounds
Expand All @@ -876,7 +864,7 @@ pub fn prepare_lights(
cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT],
// premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3]
color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * intensity,
color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * light.illuminance,
// direction is negated to be ready for N.L
dir_to_light: light.transform.back(),
flags: flags.bits,
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_pbr/src/render/pbr_functions.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ fn pbr(

// Total light
output_color = vec4<f32>(
direct_light + indirect_light + emissive_light,
view.exposure * (direct_light + indirect_light + emissive_light),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that indirect light is already in photometric units. There may need to be a scale factor to undo any pre-exposure that's baked into the contents of the cubemaps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. There was talk about an "intensity" field for environment map lighting. I decided to leave it out in my original PR because I was worried it would be abused. Maybe it's time for that now.

output_color.a
);

Expand Down
36 changes: 36 additions & 0 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@ pub struct ComputedCameraValues {
old_viewport_size: Option<UVec2>,
}

#[derive(Component)]
pub struct ExposureSettings {
pub aperture_f_stops: f32,
pub shutter_speed_s: f32,
pub sensitivity_iso: f32,
}

impl Default for ExposureSettings {
fn default() -> Self {
Self {
aperture_f_stops: 4.0,
shutter_speed_s: 1.0 / 250.0,
sensitivity_iso: 100.0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest ditching these individual settings and just passing the ev100 value directly. There's lots of sources online that provide tables of ev100 values, for instance this one from wikipedia

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, might be worth providing constants with a couple defaults. For instance, EXPOSURE_DIRECT_SUNLIGHT, EXPOSURE_INDOOR_LIGHTING, EXPOSURE_MOONLIGHT, etc.

Copy link
Contributor

@JMS55 JMS55 Apr 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! You could also provide an Ev100::from_camera(aperture_f_stops, shutter_speed_s, etc...) function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unless we had other effects of these parameters they should really be collapsed down into one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea @JMS55 - I was thinking that it would be good to expose (HA!!!) the functionality to provide the parameters for those who it is more familiar. Was first thinking of an enum, but I agree the constructor approach could be nicer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alice-i-cecile hmm... now that you mention it... the filament docs did talk about how the different parameters affect depth of field (or focal depth - the aperture setting I think, I'm not familiar with cameras beyond physics stuff) and motion blur (shutter speed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct :) At the same time, I don't think we want those baked into our rendering engine by default: motion blur and focal depth are very specific effects that should be used artistically.

We're not simulating a real camera IMO, so we should just expose Ev100 and then provide independent control for things like motion blur and focal depth when we have them.

}
}
}

impl ExposureSettings {
#[inline]
pub fn ev100(&self) -> f32 {
(self.aperture_f_stops * self.aperture_f_stops / self.shutter_speed_s).log2()
- (self.sensitivity_iso / 100.0).log2()
}

#[inline]
pub fn exposure(&self) -> f32 {
1.0 / (2.0f32.powf(self.ev100()) * 1.2)
}
}

/// The defining component for camera entities, storing information about how and what to render
/// through this camera.
///
Expand Down Expand Up @@ -573,6 +603,7 @@ pub struct ExtractedCamera {
pub output_mode: CameraOutputMode,
pub msaa_writeback: bool,
pub sorted_camera_index_for_target: usize,
pub exposure: f32,
}

pub fn extract_cameras(
Expand All @@ -585,6 +616,7 @@ pub fn extract_cameras(
&GlobalTransform,
&VisibleEntities,
Option<&ColorGrading>,
Option<&ExposureSettings>,
Option<&TemporalJitter>,
)>,
>,
Expand All @@ -598,6 +630,7 @@ pub fn extract_cameras(
transform,
visible_entities,
color_grading,
exposure_settings,
temporal_jitter,
) in query.iter()
{
Expand Down Expand Up @@ -630,6 +663,9 @@ pub fn extract_cameras(
msaa_writeback: camera.msaa_writeback,
// this will be set in sort_cameras
sorted_camera_index_for_target: 0,
exposure: exposure_settings
.map(|e| e.exposure())
.unwrap_or_else(|| ExposureSettings::default().exposure()),
},
ExtractedView {
projection: camera.projection_matrix(),
Expand Down
27 changes: 18 additions & 9 deletions crates/bevy_render/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use visibility::*;
pub use window::*;

use crate::{
camera::{ExtractedCamera, TemporalJitter},
camera::{ExposureSettings, ExtractedCamera, TemporalJitter},
extract_resource::{ExtractResource, ExtractResourcePlugin},
prelude::{Image, Shader},
render_asset::RenderAssets,
Expand Down Expand Up @@ -168,6 +168,7 @@ pub struct ViewUniform {
projection: Mat4,
inverse_projection: Mat4,
world_position: Vec3,
exposure: f32,
// viewport(x_origin, y_origin, width, height)
viewport: Vec4,
color_grading: ColorGrading,
Expand Down Expand Up @@ -313,26 +314,31 @@ pub fn prepare_view_uniforms(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut view_uniforms: ResMut<ViewUniforms>,
views: Query<(Entity, &ExtractedView, Option<&TemporalJitter>)>,
views: Query<(
Entity,
Option<&ExtractedCamera>,
&ExtractedView,
Option<&TemporalJitter>,
)>,
) {
view_uniforms.uniforms.clear();

for (entity, camera, temporal_jitter) in &views {
let viewport = camera.viewport.as_vec4();
let unjittered_projection = camera.projection;
for (entity, extracted_camera, extracted_view, temporal_jitter) in &views {
let viewport = extracted_view.viewport.as_vec4();
let unjittered_projection = extracted_view.projection;
let mut projection = unjittered_projection;

if let Some(temporal_jitter) = temporal_jitter {
temporal_jitter.jitter_projection(&mut projection, viewport.zw());
}

let inverse_projection = projection.inverse();
let view = camera.transform.compute_matrix();
let view = extracted_view.transform.compute_matrix();
let inverse_view = view.inverse();

let view_uniforms = ViewUniformOffset {
offset: view_uniforms.uniforms.push(ViewUniform {
view_proj: camera
view_proj: extracted_view
.view_projection
.unwrap_or_else(|| projection * inverse_view),
unjittered_view_proj: unjittered_projection * inverse_view,
Expand All @@ -341,9 +347,12 @@ pub fn prepare_view_uniforms(
inverse_view,
projection,
inverse_projection,
world_position: camera.transform.translation(),
world_position: extracted_view.transform.translation(),
exposure: extracted_camera
.map(|c| c.exposure)
.unwrap_or_else(|| ExposureSettings::default().exposure()),
viewport,
color_grading: camera.color_grading,
color_grading: extracted_view.color_grading,
}),
};

Expand Down
1 change: 1 addition & 0 deletions crates/bevy_render/src/view/view.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct View {
projection: mat4x4<f32>,
inverse_projection: mat4x4<f32>,
world_position: vec3<f32>,
exposure: f32,
// viewport(x_origin, y_origin, width, height)
viewport: vec4<f32>,
color_grading: ColorGrading,
Expand Down
103 changes: 97 additions & 6 deletions examples/3d/lighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

use std::f32::consts::PI;

use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*};
use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*, render::camera::ExposureSettings};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (movement, animate_light_direction))
.add_systems(Update, (update_exposure, movement, animate_light_direction))
.run();
}

Expand Down Expand Up @@ -207,6 +207,7 @@ fn setup(
// directional 'sun' light
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 1000.0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sun has an illuminance of ~100,000 lux. Though, having that bright a directional light that bright is of course going to overpower all the other lights in the scene (even stuff in shadows will mostly be lit by bounce lighting and light from the sky)

shadows_enabled: true,
..default()
},
Expand All @@ -227,11 +228,101 @@ fn setup(
..default()
});

let exposure_settings = ExposureSettings::default();
let style = TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
};

commands.spawn(
TextBundle::from_sections(vec![
TextSection::new(
format!("Aperture: f/{:.0}\n", exposure_settings.aperture_f_stops),
style.clone(),
),
TextSection::new(
format!(
"Shutter speed: 1/{:.0}s\n",
1.0 / exposure_settings.shutter_speed_s
),
style.clone(),
),
TextSection::new(
format!(
"Sensitivity: ISO {:.0}\n",
exposure_settings.sensitivity_iso
),
style.clone(),
),
TextSection::new("\n\n", style.clone()),
TextSection::new("Controls\n", style.clone()),
TextSection::new("---------------\n", style.clone()),
TextSection::new("1/2 - Decrease/Increase aperture\n", style.clone()),
TextSection::new("3/4 - Decrease/Increase shutter speed\n", style.clone()),
TextSection::new("5/6 - Decrease/Increase sensitivity\n", style),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(10.0),
left: Val::Px(10.0),
..default()
}),
);

// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn((
Camera3dBundle {
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
exposure_settings,
));
}

fn update_exposure(
key_input: Res<Input<KeyCode>>,
mut query: Query<&mut ExposureSettings>,
mut text: Query<&mut Text>,
) {
let mut text = text.single_mut();
let mut exposure_settings = query.single_mut();
if key_input.just_pressed(KeyCode::Key2) {
exposure_settings.aperture_f_stops *= 2.0;
text.sections[0].value = format!("Aperture: f/{:.0}\n", exposure_settings.aperture_f_stops);
} else if key_input.just_pressed(KeyCode::Key1) {
exposure_settings.aperture_f_stops *= 0.5;
text.sections[0].value = format!("Aperture: f/{:.0}\n", exposure_settings.aperture_f_stops);
}
if key_input.just_pressed(KeyCode::Key4) {
exposure_settings.shutter_speed_s *= 2.0;
text.sections[1].value = format!(
"Shutter speed: 1/{:.0}s\n",
1.0 / exposure_settings.shutter_speed_s
);
} else if key_input.just_pressed(KeyCode::Key3) {
exposure_settings.shutter_speed_s *= 0.5;
text.sections[1].value = format!(
"Shutter speed: 1/{:.0}s\n",
1.0 / exposure_settings.shutter_speed_s
);
}
if key_input.just_pressed(KeyCode::Key6) {
exposure_settings.sensitivity_iso += 100.0;
text.sections[2].value = format!(
"Sensitivity: ISO {:.0}\n",
exposure_settings.sensitivity_iso
);
} else if key_input.just_pressed(KeyCode::Key5) {
exposure_settings.sensitivity_iso -= 100.0;
text.sections[2].value = format!(
"Sensitivity: ISO {:.0}\n",
exposure_settings.sensitivity_iso
);
}
if key_input.just_pressed(KeyCode::D) {
*exposure_settings = ExposureSettings::default();
}
}

fn animate_light_direction(
Expand Down