bevy_pbr2: Improve lighting units and documentation (#2704)

# Objective

A question was raised on Discord about the units of the `PointLight` `intensity` member.

After digging around in the bevy_pbr2 source code and [Google Filament documentation](https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower) I discovered that the intention by Filament was that the 'intensity' value for point lights would be in lumens. This makes a lot of sense as these are quite relatable units given basically all light bulbs I've seen sold over the past years are rated in lumens as people move away from thinking about how bright a bulb is relative to a non-halogen incandescent bulb.

However, it seems that the derivation of the conversion between luminous power (lumens, denoted `Φ` in the Filament formulae) and luminous intensity (lumens per steradian, `I` in the Filament formulae) was missed and I can see why as it is tucked right under equation 58 at the link above. As such, while the formula states that for a point light, `I = Φ / 4 π` we have been using `intensity` as if it were luminous intensity `I`.

Before this PR, the intensity field is luminous intensity in lumens per steradian. After this PR, the intensity field is luminous power in lumens, [as suggested by Filament](https://google.github.io/filament/Filament.html#table_lighttypesunits) (unfortunately the link jumps to the table's caption so scroll up to see the actual table).

I appreciate that it may be confusing to call this an intensity, but I think this is intended as more of a non-scientific, human-relatable general term with a bit of hand waving so that most light types can just have an intensity field and for most of them it works in the same way or at least with some relatable value. I'm inclined to think this is reasonable rather than throwing terms like luminous power, luminous intensity, blah at users.

## Solution

- Documented the `PointLight` `intensity` member as 'luminous power' in units of lumens.
- Added a table of examples relating from various types of household lighting to lumen values.
- Added in the mapping from luminous power to luminous intensity when premultiplying the intensity into the colour before it is made into a graphics uniform.
- Updated the documentation in `pbr.wgsl` to clarify the earlier confusion about the missing `/ 4 π`.
- Bumped the intensity of the point lights in `3d_scene_pipelined` to 1600 lumens.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Robert Swain 2021-08-23 23:48:11 +00:00
parent 993ce84bc7
commit c3d3ae7f92
4 changed files with 37 additions and 9 deletions

View file

@ -110,6 +110,7 @@ fn setup(
// transform: Transform::from_xyz(5.0, 8.0, 2.0), // transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(1.0, 2.0, 0.0), transform: Transform::from_xyz(1.0, 2.0, 0.0),
point_light: PointLight { point_light: PointLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::RED, color: Color::RED,
..Default::default() ..Default::default()
}, },
@ -136,6 +137,7 @@ fn setup(
// transform: Transform::from_xyz(5.0, 8.0, 2.0), // transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(-1.0, 2.0, 0.0), transform: Transform::from_xyz(-1.0, 2.0, 0.0),
point_light: PointLight { point_light: PointLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::GREEN, color: Color::GREEN,
..Default::default() ..Default::default()
}, },
@ -162,6 +164,7 @@ fn setup(
// transform: Transform::from_xyz(5.0, 8.0, 2.0), // transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(0.0, 4.0, 0.0), transform: Transform::from_xyz(0.0, 4.0, 0.0),
point_light: PointLight { point_light: PointLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::BLUE, color: Color::BLUE,
..Default::default() ..Default::default()
}, },

View file

@ -1,6 +1,22 @@
use bevy_render2::{camera::OrthographicProjection, color::Color}; use bevy_render2::{camera::OrthographicProjection, color::Color};
/// A light that emits light in all directions from a central point. /// A light that emits light in all directions from a central point.
///
/// Real-world values for `intensity` (luminous power in lumens) based on the electrical power
/// consumption of the type of real-world light are:
///
/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts |
/// |------|-----|----|--------|-------|
/// | 200 | 25 | | 3-5 | 3 |
/// | 450 | 40 | 29 | 9-11 | 5-8 |
/// | 800 | 60 | | 13-15 | 8-12 |
/// | 1100 | 75 | 53 | 18-20 | 10-16 |
/// | 1600 | 100 | 72 | 24-28 | 14-17 |
/// | 2400 | 150 | | 30-52 | 24-30 |
/// | 3100 | 200 | | 49-75 | 32 |
/// | 4000 | 300 | | 75-100 | 40.5 |
///
/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting)
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct PointLight { pub struct PointLight {
pub color: Color, pub color: Color,
@ -18,7 +34,8 @@ impl Default for PointLight {
fn default() -> Self { fn default() -> Self {
PointLight { PointLight {
color: Color::rgb(1.0, 1.0, 1.0), color: Color::rgb(1.0, 1.0, 1.0),
intensity: 200.0, /// Luminous power in lumens
intensity: 800.0, // Roughly a 60W non-halogen incandescent bulb
range: 20.0, range: 20.0,
radius: 0.0, radius: 0.0,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
@ -61,6 +78,7 @@ impl PointLight {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DirectionalLight { pub struct DirectionalLight {
pub color: Color, pub color: Color,
/// Illuminance in lux
pub illuminance: f32, pub illuminance: f32,
pub shadow_projection: OrthographicProjection, pub shadow_projection: OrthographicProjection,
pub shadow_depth_bias: f32, pub shadow_depth_bias: f32,
@ -95,11 +113,11 @@ impl DirectionalLight {
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
} }
// Ambient light color. /// Ambient light.
#[derive(Debug)] #[derive(Debug)]
pub struct AmbientLight { pub struct AmbientLight {
pub color: Color, pub color: Color,
/// Color is premultiplied by brightness before being passed to the shader /// A direct scale factor multiplied with `color` before being passed to the shader
pub brightness: f32, pub brightness: f32,
} }

View file

@ -25,6 +25,7 @@ pub struct ExtractedAmbientLight {
pub struct ExtractedPointLight { pub struct ExtractedPointLight {
color: Color, color: Color,
/// luminous intensity in lumens per steradian
intensity: f32, intensity: f32,
range: f32, range: f32,
radius: f32, radius: f32,
@ -239,7 +240,10 @@ pub fn extract_lights(
for (entity, point_light, transform) in point_lights.iter() { for (entity, point_light, transform) in point_lights.iter() {
commands.get_or_spawn(entity).insert(ExtractedPointLight { commands.get_or_spawn(entity).insert(ExtractedPointLight {
color: point_light.color, color: point_light.color,
intensity: point_light.intensity, // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
// for details.
intensity: point_light.intensity / (4.0 * std::f32::consts::PI),
range: point_light.range, range: point_light.range,
radius: point_light.radius, radius: point_light.radius,
transform: *transform, transform: *transform,

View file

@ -349,17 +349,20 @@ fn point_light(
let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH);
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
// Lout = f(v,l) Φ / { 4 π d^2 }nl // Lout = f(v,l) Φ / { 4 π d^2 }nl
// where // where
// f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color // f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color
// Φ is light intensity // Φ is luminous power in lumens
// our rangeAttentuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius // our rangeAttentuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius
// It's not 100% clear where the 1/4π goes in the derivation, but we follow the filament shader and leave it out
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation // For a point light, luminous intensity, I, in lumens per steradian is given by:
// I = Φ / 4 π
// The derivation of this can be seen here: https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
// NOTE: light.color.rgb is premultiplied with light.intensity / 4 π (which would be the luminous intensity) on the CPU
// TODO compensate for energy loss https://google.github.io/filament/Filament.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance // TODO compensate for energy loss https://google.github.io/filament/Filament.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance
// light.color.rgb is premultiplied with light.intensity on the CPU
return ((diffuse + specular_light) * light.color.rgb) * (rangeAttenuation * NoL); return ((diffuse + specular_light) * light.color.rgb) * (rangeAttenuation * NoL);
} }