Skip to content

Commit

Permalink
bevy_pbr2: Improve lighting units and documentation (#2704)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
superdump and cart committed Aug 23, 2021
1 parent 993ce84 commit c3d3ae7
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 9 deletions.
3 changes: 3 additions & 0 deletions examples/3d/3d_scene_pipelined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ fn setup(
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(1.0, 2.0, 0.0),
point_light: PointLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::RED,
..Default::default()
},
Expand All @@ -136,6 +137,7 @@ fn setup(
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(-1.0, 2.0, 0.0),
point_light: PointLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::GREEN,
..Default::default()
},
Expand All @@ -162,6 +164,7 @@ fn setup(
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(0.0, 4.0, 0.0),
point_light: PointLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::BLUE,
..Default::default()
},
Expand Down
24 changes: 21 additions & 3 deletions pipelined/bevy_pbr2/src/light.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
use bevy_render2::{camera::OrthographicProjection, color::Color};

/// 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)]
pub struct PointLight {
pub color: Color,
Expand All @@ -18,7 +34,8 @@ impl Default for PointLight {
fn default() -> Self {
PointLight {
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,
radius: 0.0,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
Expand Down Expand Up @@ -61,6 +78,7 @@ impl PointLight {
#[derive(Debug, Clone)]
pub struct DirectionalLight {
pub color: Color,
/// Illuminance in lux
pub illuminance: f32,
pub shadow_projection: OrthographicProjection,
pub shadow_depth_bias: f32,
Expand Down Expand Up @@ -95,11 +113,11 @@ impl DirectionalLight {
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
}

// Ambient light color.
/// Ambient light.
#[derive(Debug)]
pub struct AmbientLight {
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,
}

Expand Down
6 changes: 5 additions & 1 deletion pipelined/bevy_pbr2/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct ExtractedAmbientLight {

pub struct ExtractedPointLight {
color: Color,
/// luminous intensity in lumens per steradian
intensity: f32,
range: f32,
radius: f32,
Expand Down Expand Up @@ -239,7 +240,10 @@ pub fn extract_lights(
for (entity, point_light, transform) in point_lights.iter() {
commands.get_or_spawn(entity).insert(ExtractedPointLight {
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,
radius: point_light.radius,
transform: *transform,
Expand Down
13 changes: 8 additions & 5 deletions pipelined/bevy_pbr2/src/render/pbr.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -349,17 +349,20 @@ fn point_light(

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 }⟨n⋅l⟩
// where
// 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
// 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
// light.color.rgb is premultiplied with light.intensity on the CPU

return ((diffuse + specular_light) * light.color.rgb) * (rangeAttenuation * NoL);
}
Expand Down

0 comments on commit c3d3ae7

Please sign in to comment.