Skip to content

Commit

Permalink
Exposure settings (adopted) (#11347)
Browse files Browse the repository at this point in the history
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 <robert.swain@gmail.com>
Co-authored-by: GitGhillie <jillisnoordhoek@gmail.com>
Co-authored-by: Marco Buono <thecoreh@gmail.com>
Co-authored-by: vero <email@atlasdostal.com>
Co-authored-by: atlas dostal <rodol@rivalrebels.com>
  • Loading branch information
6 people authored Jan 16, 2024
1 parent 184f233 commit fcd7c0f
Show file tree
Hide file tree
Showing 70 changed files with 436 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,17 @@ impl ViewNode for MainOpaquePass3dNode {
}

// Draw the skybox using a fullscreen triangle
if let (Some(skybox_pipeline), Some(skybox_bind_group)) =
if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) =
(skybox_pipeline, skybox_bind_group)
{
let pipeline_cache = world.resource::<PipelineCache>();
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &skybox_bind_group.0, &[view_uniform_offset.offset]);
render_pass.set_bind_group(
0,
&skybox_bind_group.0,
&[view_uniform_offset.offset, skybox_bind_group.1],
);
render_pass.draw(0..3, 0..1);
}
}
Expand Down
78 changes: 60 additions & 18 deletions crates/bevy_core_pipeline/src/skybox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@ use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle};
use bevy_ecs::{
prelude::{Component, Entity},
query::With,
query::{QueryItem, With},
schedule::IntoSystemConfigs,
system::{Commands, Query, Res, ResMut, Resource},
};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
camera::ExposureSettings,
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_asset::RenderAssets,
render_resource::{
binding_types::{sampler, texture_cube, uniform_buffer},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState,
RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages,
SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState,
TextureFormat, TextureSampleType, VertexState,
*,
},
renderer::RenderDevice,
texture::{BevyDefault, Image},
Expand All @@ -34,7 +33,10 @@ impl Plugin for SkyboxPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl);

app.add_plugins(ExtractComponentPlugin::<Skybox>::default());
app.add_plugins((
ExtractComponentPlugin::<Skybox>::default(),
UniformComponentPlugin::<SkyboxUniforms>::default(),
));

let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
Expand Down Expand Up @@ -68,8 +70,41 @@ impl Plugin for SkyboxPlugin {
/// To do so, use `EnvironmentMapLight` alongside this component.
///
/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
#[derive(Component, ExtractComponent, Clone)]
pub struct Skybox(pub Handle<Image>);
#[derive(Component, Clone)]
pub struct Skybox {
pub image: Handle<Image>,
/// Scale factor applied to the skybox image.
/// After applying this multiplier to the image samples, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
pub brightness: f32,
}

impl ExtractComponent for Skybox {
type Data = (&'static Self, Option<&'static ExposureSettings>);
type Filter = ();
type Out = (Self, SkyboxUniforms);

fn extract_component(
(skybox, exposure_settings): QueryItem<'_, Self::Data>,
) -> Option<Self::Out> {
let exposure = exposure_settings
.map(|e| e.exposure())
.unwrap_or_else(|| ExposureSettings::default().exposure());

Some((
skybox.clone(),
SkyboxUniforms {
brightness: skybox.brightness * exposure,
},
))
}
}

// TODO: Replace with a push constant once WebGPU gets support for that
#[derive(Component, ShaderType, Clone)]
pub struct SkyboxUniforms {
brightness: f32,
}

#[derive(Resource)]
struct SkyboxPipeline {
Expand All @@ -88,6 +123,7 @@ impl SkyboxPipeline {
sampler(SamplerBindingType::Filtering),
uniform_buffer::<ViewUniform>(true)
.visibility(ShaderStages::VERTEX_FRAGMENT),
uniform_buffer::<SkyboxUniforms>(true),
),
),
),
Expand Down Expand Up @@ -186,31 +222,37 @@ fn prepare_skybox_pipelines(
}

#[derive(Component)]
pub struct SkyboxBindGroup(pub BindGroup);
pub struct SkyboxBindGroup(pub (BindGroup, u32));

fn prepare_skybox_bind_groups(
mut commands: Commands,
pipeline: Res<SkyboxPipeline>,
view_uniforms: Res<ViewUniforms>,
skybox_uniforms: Res<ComponentUniforms<SkyboxUniforms>>,
images: Res<RenderAssets<Image>>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &Skybox)>,
views: Query<(Entity, &Skybox, &DynamicUniformIndex<SkyboxUniforms>)>,
) {
for (entity, skybox) in &views {
if let (Some(skybox), Some(view_uniforms)) =
(images.get(&skybox.0), view_uniforms.uniforms.binding())
{
for (entity, skybox, skybox_uniform_index) in &views {
if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = (
images.get(&skybox.image),
view_uniforms.uniforms.binding(),
skybox_uniforms.binding(),
) {
let bind_group = render_device.create_bind_group(
"skybox_bind_group",
&pipeline.bind_group_layout,
&BindGroupEntries::sequential((
&skybox.texture_view,
&skybox.sampler,
view_uniforms,
skybox_uniforms,
)),
);

commands.entity(entity).insert(SkyboxBindGroup(bind_group));
commands
.entity(entity)
.insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index())));
}
}
}
3 changes: 2 additions & 1 deletion crates/bevy_core_pipeline/src/skybox/skybox.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@group(0) @binding(0) var skybox: texture_cube<f32>;
@group(0) @binding(1) var skybox_sampler: sampler;
@group(0) @binding(2) var<uniform> view: View;
@group(0) @binding(3) var<uniform> brightness: f32;

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

// Cube maps are left-handed so we negate the z coordinate.
return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0));
return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)) * brightness;
}
4 changes: 2 additions & 2 deletions crates/bevy_pbr/src/environment_map/environment_map.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn environment_map_light(
let kD = diffuse_color * Edss;

var out: EnvironmentMapLight;
out.diffuse = (FmsEms + kD) * irradiance;
out.specular = FssEss * radiance;
out.diffuse = (FmsEms + kD) * irradiance * bindings::lights.environment_map_intensity;
out.specular = FssEss * radiance * bindings::lights.environment_map_intensity;
return out;
}
7 changes: 7 additions & 0 deletions crates/bevy_pbr/src/environment_map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ impl Plugin for EnvironmentMapPlugin {
pub struct EnvironmentMapLight {
pub diffuse_map: Handle<Image>,
pub specular_map: Handle<Image>,
/// Scale factor applied to the diffuse and specular light generated by this component.
///
/// After applying this multiplier, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
///
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
pub intensity: f32,
}

impl EnvironmentMapLight {
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ fn calculate_cascade(
/// # use bevy_ecs::system::ResMut;
/// # use bevy_pbr::AmbientLight;
/// fn setup_ambient_light(mut ambient_light: ResMut<AmbientLight>) {
/// ambient_light.brightness = 0.3;
/// ambient_light.brightness = 20.0;
/// }
/// ```
#[derive(Resource, Clone, Debug, ExtractResource, Reflect)]
Expand All @@ -572,7 +572,7 @@ impl Default for AmbientLight {
fn default() -> Self {
Self {
color: Color::rgb(1.0, 1.0, 1.0),
brightness: 0.05,
brightness: 8.0,
}
}
}
Expand Down
20 changes: 6 additions & 14 deletions crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ pub struct GpuLights {
// offset from spot light's light index to spot light's shadow map index
spot_light_shadowmap_offset: i32,
environment_map_smallest_specular_mip_level: u32,
environment_map_intensity: f32,
}

// NOTE: this must be kept in sync with the same constants in pbr.frag
Expand Down Expand Up @@ -857,18 +858,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 @@ -877,9 +866,9 @@ pub fn prepare_lights(
gpu_directional_lights[index] = GpuDirectionalLight {
// Filled in later.
cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT],
// premultiply color by intensity
// premultiply color by illuminance
// 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 Expand Up @@ -972,6 +961,9 @@ pub fn prepare_lights(
.and_then(|env_map| images.get(&env_map.specular_map))
.map(|specular_map| specular_map.mip_level_count - 1)
.unwrap_or(0),
environment_map_intensity: environment_map
.map(|env_map| env_map.intensity)
.unwrap_or(1.0),
};

// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_pbr/src/render/mesh_view_types.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ struct Lights {
n_directional_lights: u32,
spot_light_shadowmap_offset: i32,
environment_map_smallest_specular_mip_level: u32,
environment_map_intensity: f32,
};

struct Fog {
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_pbr/src/render/pbr_functions.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ fn apply_pbr_lighting(

// Total light
output_color = vec4<f32>(
transmitted_light + direct_light + indirect_light + emissive_light,
view_bindings::view.exposure * (transmitted_light + direct_light + indirect_light + emissive_light),
output_color.a
);

Expand Down Expand Up @@ -423,7 +423,7 @@ fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4<f32>, fragment_
0.0
),
fog_params.directional_light_exponent
) * light.color.rgb;
) * light.color.rgb * view_bindings::view.exposure;
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_pbr/src/render/pbr_transmission.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ fn specular_transmissive_light(world_position: vec4<f32>, frag_coord: vec3<f32>,
background_color = fetch_transmissive_background(offset_position, frag_coord, view_z, perceptual_roughness);
}

// Compensate for exposure, since the background color is coming from an already exposure-adjusted texture
background_color = vec4(background_color.rgb / view_bindings::view.exposure, background_color.a);

// Dot product of the refracted direction with the exit normal (Note: We assume the exit normal is the entry normal but inverted)
let MinusNdotT = dot(-N, T);

Expand Down
Loading

0 comments on commit fcd7c0f

Please sign in to comment.