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
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,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
83 changes: 71 additions & 12 deletions crates/bevy_core_pipeline/src/skybox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
use bevy_ecs::{
prelude::{Component, Entity},
query::With,
query::{QueryItem, With},
schedule::IntoSystemConfigs,
system::{Commands, Query, Res, ResMut, Resource},
};
use bevy_reflect::TypeUuid;
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
camera::ExposureSettings,
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_asset::RenderAssets,
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
Expand All @@ -34,7 +38,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 render_app = match app.get_sub_app_mut(RenderApp) {
Ok(render_app) => render_app,
Expand Down Expand Up @@ -70,8 +77,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 Query = (&'static Self, Option<&'static ExposureSettings>);
type Filter = ();
type Out = (Self, SkyboxUniforms);

fn extract_component(
(skybox, exposure_settings): QueryItem<'_, Self::Query>,
) -> 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
superdump marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Component, ShaderType, Clone)]
pub struct SkyboxUniforms {
brightness: f32,
}

#[derive(Resource)]
struct SkyboxPipeline {
Expand Down Expand Up @@ -109,6 +149,16 @@ impl SkyboxPipeline {
},
count: None,
},
BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(SkyboxUniforms::min_size()),
},
count: None,
},
],
};

Expand Down Expand Up @@ -207,20 +257,23 @@ 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(&BindGroupDescriptor {
label: Some("skybox_bind_group"),
layout: &pipeline.bind_group_layout,
Expand All @@ -237,10 +290,16 @@ fn prepare_skybox_bind_groups(
binding: 2,
resource: view_uniforms,
},
BindGroupEntry {
binding: 3,
resource: skybox_uniforms,
},
],
});

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

struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
Expand Down Expand Up @@ -48,5 +50,5 @@ fn skybox_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// position, to the fragment world position on the near clipping plane
let ray_direction = in.world_position - view.world_position;
// 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 @@ -39,7 +39,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;
}
4 changes: 4 additions & 0 deletions crates/bevy_pbr/src/environment_map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ impl Plugin for EnvironmentMapPlugin {
pub struct EnvironmentMapLight {
pub diffuse_map: Handle<Image>,
pub specular_map: Handle<Image>,
/// Scale factor applied to both the diffuse and specular cubemap.
/// 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 intensity: f32,
}

impl EnvironmentMapLight {
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 @@ -203,6 +203,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 @@ -848,18 +849,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 @@ -868,9 +857,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 @@ -962,6 +951,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 @@ -60,6 +60,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
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 @@ -272,7 +272,7 @@ fn pbr(

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

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

/// How much energy a [`Camera3d`] absorbs from incoming light.
///
/// <https://en.wikipedia.org/wiki/Exposure_(photography)>
#[derive(Component)]
pub struct ExposureSettings {
/// <https://en.wikipedia.org/wiki/Exposure_value#Tabulated_exposure_values>
pub ev100: f32,
}

impl ExposureSettings {
pub const EV100_SUNLIGHT: f32 = 15.0;
pub const EV100_OVERCAST: f32 = 12.0;
pub const EV100_INDOOR: f32 = 7.0;

pub fn from_physical_camera(physical_camera_parameters: PhysicalCameraParameters) -> Self {
Self {
ev100: physical_camera_parameters.ev100(),
}
}

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

impl Default for ExposureSettings {
fn default() -> Self {
Self {
ev100: Self::EV100_OVERCAST,
}
}
}

/// Parameters based on physical camera characteristics for calculating
/// EV100 values for use with [`ExposureSettings`].
#[derive(Clone, Copy)]
pub struct PhysicalCameraParameters {
/// <https://en.wikipedia.org/wiki/F-number>
pub aperture_f_stops: f32,
/// <https://en.wikipedia.org/wiki/Shutter_speed>
pub shutter_speed_s: f32,
/// <https://en.wikipedia.org/wiki/Film_speed>
pub sensitivity_iso: f32,
}

impl PhysicalCameraParameters {
/// Calculate the [EV100](https://en.wikipedia.org/wiki/Exposure_value).
pub fn ev100(&self) -> f32 {
(self.aperture_f_stops * self.aperture_f_stops / self.shutter_speed_s).log2()
- (self.sensitivity_iso / 100.0).log2()
}
}

impl Default for PhysicalCameraParameters {
fn default() -> Self {
Self {
aperture_f_stops: 4.0,
shutter_speed_s: 1.0 / 250.0,
sensitivity_iso: 100.0,
}
}
}

/// The defining [`Component`] for camera entities,
/// storing information about how and what to render through this camera.
///
Expand Down Expand Up @@ -630,6 +695,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 @@ -642,6 +708,7 @@ pub fn extract_cameras(
&GlobalTransform,
&VisibleEntities,
Option<&ColorGrading>,
Option<&ExposureSettings>,
Option<&TemporalJitter>,
Option<&RenderLayers>,
)>,
Expand All @@ -656,6 +723,7 @@ pub fn extract_cameras(
transform,
visible_entities,
color_grading,
exposure_settings,
temporal_jitter,
render_layers,
) in query.iter()
Expand Down Expand Up @@ -696,6 +764,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
Loading