From 73f4a9d18f609e52d28e2963635560e0b78a2f9f Mon Sep 17 00:00:00 2001 From: MsK` Date: Fri, 14 May 2021 20:37:34 +0000 Subject: [PATCH] Directional light (#2112) This PR adds a `DirectionalLight` component to bevy_pbr. --- crates/bevy_pbr/src/lib.rs | 6 +- crates/bevy_pbr/src/light.rs | 108 +++++++++++++- .../bevy_pbr/src/render_graph/lights_node.rs | 70 +++++++--- crates/bevy_pbr/src/render_graph/mod.rs | 6 +- .../src/render_graph/pbr_pipeline/pbr.frag | 132 +++++++++++------- 5 files changed, 248 insertions(+), 74 deletions(-) diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 9737a39e28d13..e279fe8b8d90f 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -10,7 +10,11 @@ pub use material::*; pub mod prelude { #[doc(hidden)] - pub use crate::{entity::*, light::PointLight, material::StandardMaterial}; + pub use crate::{ + entity::*, + light::{DirectionalLight, PointLight}, + material::StandardMaterial, + }; } use bevy_app::prelude::*; diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 7e5b9766a4dd9..dff5f475d880d 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -1,11 +1,12 @@ use bevy_core::Byteable; use bevy_ecs::reflect::ReflectComponent; +use bevy_math::Vec3; use bevy_reflect::Reflect; use bevy_render::color::Color; use bevy_transform::components::GlobalTransform; /// A point light -#[derive(Debug, Reflect)] +#[derive(Debug, Clone, Copy, Reflect)] #[reflect(Component)] pub struct PointLight { pub color: Color, @@ -37,7 +38,7 @@ pub(crate) struct PointLightUniform { unsafe impl Byteable for PointLightUniform {} impl PointLightUniform { - pub fn from(light: &PointLight, global_transform: &GlobalTransform) -> PointLightUniform { + pub fn new(light: &PointLight, global_transform: &GlobalTransform) -> PointLightUniform { let (x, y, z) = global_transform.translation.into(); // premultiply color by intensity @@ -52,6 +53,109 @@ impl PointLightUniform { } } +/// A Directional light. +/// +/// Directional lights don't exist in reality but they are a good +/// approximation for light sources VERY far away, like the sun or +/// the moon. +/// +/// Valid values for `illuminance` are: +/// +/// | Illuminance (lux) | Surfaces illuminated by | +/// |-------------------|------------------------------------------------| +/// | 0.0001 | Moonless, overcast night sky (starlight) | +/// | 0.002 | Moonless clear night sky with airglow | +/// | 0.05–0.3 | Full moon on a clear night | +/// | 3.4 | Dark limit of civil twilight under a clear sky | +/// | 20–50 | Public areas with dark surroundings | +/// | 50 | Family living room lights | +/// | 80 | Office building hallway/toilet lighting | +/// | 100 | Very dark overcast day | +/// | 150 | Train station platforms | +/// | 320–500 | Office lighting | +/// | 400 | Sunrise or sunset on a clear day. | +/// | 1000 | Overcast day; typical TV studio lighting | +/// | 10,000–25,000 | Full daylight (not direct sun) | +/// | 32,000–100,000 | Direct sunlight | +/// +/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lux) +#[derive(Debug, Clone, Copy, Reflect)] +#[reflect(Component)] +pub struct DirectionalLight { + pub color: Color, + pub illuminance: f32, + direction: Vec3, +} + +impl DirectionalLight { + /// Create a new directional light component. + pub fn new(color: Color, illuminance: f32, direction: Vec3) -> Self { + DirectionalLight { + color, + illuminance, + direction: direction.normalize(), + } + } + + /// Set direction of light. + pub fn set_direction(&mut self, direction: Vec3) { + self.direction = direction.normalize(); + } + + pub fn get_direction(&self) -> Vec3 { + self.direction + } +} + +impl Default for DirectionalLight { + fn default() -> Self { + DirectionalLight { + color: Color::rgb(1.0, 1.0, 1.0), + illuminance: 100000.0, + direction: Vec3::new(0.0, -1.0, 0.0), + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub(crate) struct DirectionalLightUniform { + pub dir: [f32; 4], + pub color: [f32; 4], +} + +unsafe impl Byteable for DirectionalLightUniform {} + +impl DirectionalLightUniform { + pub fn new(light: &DirectionalLight) -> DirectionalLightUniform { + // direction is negated to be ready for N.L + let dir: [f32; 4] = [ + -light.direction.x, + -light.direction.y, + -light.direction.z, + 0.0, + ]; + + // 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; + + // premultiply color by intensity + // we don't use the alpha at all, so no reason to multiply only [0..3] + let color: [f32; 4] = (light.color * intensity).into(); + + DirectionalLightUniform { dir, color } + } +} + // Ambient light color. #[derive(Debug)] pub struct AmbientLight { diff --git a/crates/bevy_pbr/src/render_graph/lights_node.rs b/crates/bevy_pbr/src/render_graph/lights_node.rs index 8f89570eae1a4..19ea0faee7af2 100644 --- a/crates/bevy_pbr/src/render_graph/lights_node.rs +++ b/crates/bevy_pbr/src/render_graph/lights_node.rs @@ -1,5 +1,7 @@ use crate::{ - light::{AmbientLight, PointLight, PointLightUniform}, + light::{ + AmbientLight, DirectionalLight, DirectionalLightUniform, PointLight, PointLightUniform, + }, render_graph::uniform, }; use bevy_core::{AsBytes, Byteable}; @@ -21,12 +23,14 @@ use bevy_transform::prelude::*; pub struct LightsNode { command_queue: CommandQueue, max_point_lights: usize, + max_dir_lights: usize, } impl LightsNode { - pub fn new(max_lights: usize) -> Self { + pub fn new(max_point_lights: usize, max_dir_lights: usize) -> Self { LightsNode { - max_point_lights: max_lights, + max_point_lights, + max_dir_lights, command_queue: CommandQueue::default(), } } @@ -48,6 +52,8 @@ impl Node for LightsNode { #[derive(Debug, Clone, Copy)] struct LightCount { // storing as a `[u32; 4]` for memory alignement + // Index 0 is for point lights, + // Index 1 is for directional lights pub num_lights: [u32; 4], } @@ -59,6 +65,7 @@ impl SystemNode for LightsNode { config.0 = Some(LightsNodeSystemState { command_queue: self.command_queue.clone(), max_point_lights: self.max_point_lights, + max_dir_lights: self.max_dir_lights, light_buffer: None, staging_buffer: None, }) @@ -74,6 +81,7 @@ pub struct LightsNodeSystemState { staging_buffer: Option, command_queue: CommandQueue, max_point_lights: usize, + max_dir_lights: usize, } pub fn lights_node_system( @@ -83,7 +91,8 @@ pub fn lights_node_system( // TODO: this write on RenderResourceBindings will prevent this system from running in parallel // with other systems that do the same mut render_resource_bindings: ResMut, - query: Query<(&PointLight, &GlobalTransform)>, + point_lights: Query<(&PointLight, &GlobalTransform)>, + dir_lights: Query<&DirectionalLight>, ) { let state = &mut state; let render_resource_context = &**render_resource_context; @@ -92,16 +101,31 @@ pub fn lights_node_system( let ambient_light: [f32; 4] = (ambient_light_resource.color * ambient_light_resource.brightness).into(); let ambient_light_size = std::mem::size_of::<[f32; 4]>(); - let point_light_count = query.iter().len().min(state.max_point_lights); - let size = std::mem::size_of::(); + + let point_light_count = point_lights.iter().len().min(state.max_point_lights); + let point_light_size = std::mem::size_of::(); + let point_light_array_size = point_light_size * point_light_count; + let point_light_array_max_size = point_light_size * state.max_point_lights; + + let dir_light_count = dir_lights.iter().len().min(state.max_dir_lights); + let dir_light_size = std::mem::size_of::(); + let dir_light_array_size = dir_light_size * dir_light_count; + let dir_light_array_max_size = dir_light_size * state.max_dir_lights; + let light_count_size = ambient_light_size + std::mem::size_of::(); - let point_light_array_size = size * point_light_count; - let point_light_array_max_size = size * state.max_point_lights; - let current_point_light_uniform_size = light_count_size + point_light_array_size; - let max_light_uniform_size = light_count_size + point_light_array_max_size; + + let point_light_uniform_start = light_count_size; + let point_light_uniform_end = light_count_size + point_light_array_size; + + let dir_light_uniform_start = light_count_size + point_light_array_max_size; + let dir_light_uniform_end = + light_count_size + point_light_array_max_size + dir_light_array_size; + + let max_light_uniform_size = + light_count_size + point_light_array_max_size + dir_light_array_max_size; if let Some(staging_buffer) = state.staging_buffer { - if point_light_count == 0 { + if point_light_count == 0 && dir_light_count == 0 { return; } @@ -133,23 +157,33 @@ pub fn lights_node_system( let staging_buffer = state.staging_buffer.unwrap(); render_resource_context.write_mapped_buffer( staging_buffer, - 0..current_point_light_uniform_size as u64, + 0..max_light_uniform_size as u64, &mut |data, _renderer| { // ambient light data[0..ambient_light_size].copy_from_slice(ambient_light.as_bytes()); // light count - data[ambient_light_size..light_count_size] - .copy_from_slice([point_light_count as u32, 0, 0, 0].as_bytes()); + data[ambient_light_size..light_count_size].copy_from_slice( + [point_light_count as u32, dir_light_count as u32, 0, 0].as_bytes(), + ); - // light array - for ((point_light, global_transform), slot) in query.iter().zip( - data[light_count_size..current_point_light_uniform_size].chunks_exact_mut(size), + // point light array + for ((point_light, global_transform), slot) in point_lights.iter().zip( + data[point_light_uniform_start..point_light_uniform_end] + .chunks_exact_mut(point_light_size), ) { slot.copy_from_slice( - PointLightUniform::from(&point_light, &global_transform).as_bytes(), + PointLightUniform::new(&point_light, &global_transform).as_bytes(), ); } + + // directional light array + for (dir_light, slot) in dir_lights.iter().zip( + data[dir_light_uniform_start..dir_light_uniform_end] + .chunks_exact_mut(dir_light_size), + ) { + slot.copy_from_slice(DirectionalLightUniform::new(&dir_light).as_bytes()); + } }, ); render_resource_context.unmap_buffer(staging_buffer); diff --git a/crates/bevy_pbr/src/render_graph/mod.rs b/crates/bevy_pbr/src/render_graph/mod.rs index 55e3fbaa868d4..bc0fcb4732599 100644 --- a/crates/bevy_pbr/src/render_graph/mod.rs +++ b/crates/bevy_pbr/src/render_graph/mod.rs @@ -27,6 +27,7 @@ use bevy_render::{ use bevy_transform::prelude::GlobalTransform; pub const MAX_POINT_LIGHTS: usize = 10; +pub const MAX_DIRECTIONAL_LIGHTS: usize = 1; pub(crate) fn add_pbr_graph(world: &mut World) { { let mut graph = world.get_resource_mut::().unwrap(); @@ -39,7 +40,10 @@ pub(crate) fn add_pbr_graph(world: &mut World) { AssetRenderResourcesNode::::new(true), ); - graph.add_system_node(node::LIGHTS, LightsNode::new(MAX_POINT_LIGHTS)); + graph.add_system_node( + node::LIGHTS, + LightsNode::new(MAX_POINT_LIGHTS, MAX_DIRECTIONAL_LIGHTS), + ); // TODO: replace these with "autowire" groups graph diff --git a/crates/bevy_pbr/src/render_graph/pbr_pipeline/pbr.frag b/crates/bevy_pbr/src/render_graph/pbr_pipeline/pbr.frag index 3891e90e0ba5d..52a005dfe7608 100644 --- a/crates/bevy_pbr/src/render_graph/pbr_pipeline/pbr.frag +++ b/crates/bevy_pbr/src/render_graph/pbr_pipeline/pbr.frag @@ -34,13 +34,20 @@ // // The above integration needs to be approximated. -const int MAX_LIGHTS = 10; +// reflects the constants defined bevy_pbr/src/render_graph/mod.rs +const int MAX_POINT_LIGHTS = 10; +const int MAX_DIRECTIONAL_LIGHTS = 1; struct PointLight { vec4 pos; vec4 color; vec4 lightParams; }; + +struct DirectionalLight { + vec4 direction; + vec4 color; +}; layout(location = 0) in vec3 v_WorldPosition; layout(location = 1) in vec3 v_WorldNormal; @@ -61,8 +68,9 @@ layout(std140, set = 0, binding = 1) uniform CameraPosition { layout(std140, set = 1, binding = 0) uniform Lights { vec4 AmbientColor; - uvec4 NumLights; - PointLight PointLights[MAX_LIGHTS]; + uvec4 NumLights; // x = point lights, y = directional lights + PointLight PointLights[MAX_POINT_LIGHTS]; + DirectionalLight DirectionalLights[MAX_DIRECTIONAL_LIGHTS]; }; layout(set = 3, binding = 0) uniform StandardMaterial_base_color { @@ -277,6 +285,70 @@ vec3 reinhard_extended_luminance(vec3 color, float max_white_l) { #endif +vec3 point_light(PointLight light, float roughness, float NdotV, vec3 N, vec3 V, vec3 R, vec3 F0, vec3 diffuseColor) { + vec3 light_to_frag = light.pos.xyz - v_WorldPosition.xyz; + float distance_square = dot(light_to_frag, light_to_frag); + float rangeAttenuation = + getDistanceAttenuation(distance_square, light.lightParams.r); + + // Specular. + // Representative Point Area Lights. + // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 + float a = roughness; + float radius = light.lightParams.g; + vec3 centerToRay = dot(light_to_frag, R) * R - light_to_frag; + vec3 closestPoint = light_to_frag + centerToRay * saturate(radius * inversesqrt(dot(centerToRay, centerToRay))); + float LspecLengthInverse = inversesqrt(dot(closestPoint, closestPoint)); + float normalizationFactor = a / saturate(a + (radius * 0.5 * LspecLengthInverse)); + float specularIntensity = normalizationFactor * normalizationFactor; + + vec3 L = closestPoint * LspecLengthInverse; // normalize() equivalent? + vec3 H = normalize(L + V); + float NoL = saturate(dot(N, L)); + float NoH = saturate(dot(N, H)); + float LoH = saturate(dot(L, H)); + + vec3 specular = specular(F0, roughness, H, NdotV, NoL, NoH, LoH, specularIntensity); + + // Diffuse. + // Comes after specular since its NoL is used in the lighting equation. + L = normalize(light_to_frag); + H = normalize(L + V); + NoL = saturate(dot(N, L)); + NoH = saturate(dot(N, H)); + LoH = saturate(dot(L, H)); + + vec3 diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); + + // 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 + + // 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 + // 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.color.rgb) * (rangeAttenuation * NoL); +} + +vec3 dir_light(DirectionalLight light, float roughness, float NdotV, vec3 normal, vec3 view, vec3 R, vec3 F0, vec3 diffuseColor) { + vec3 incident_light = light.direction.xyz; + + vec3 half_vector = normalize(incident_light + view); + float NoL = saturate(dot(normal, incident_light)); + float NoH = saturate(dot(normal, half_vector)); + float LoH = saturate(dot(incident_light, half_vector)); + + vec3 diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); + float specularIntensity = 1.0; + vec3 specular = specular(F0, roughness, half_vector, NdotV, NoL, NoH, LoH, specularIntensity); + + return (specular + diffuse) * light.color.rgb * NoL; +} + void main() { vec4 output_color = base_color; #ifdef STANDARDMATERIAL_BASE_COLOR_TEXTURE @@ -343,55 +415,11 @@ void main() { // accumulate color vec3 light_accum = vec3(0.0); - for (int i = 0; i < int(NumLights.x) && i < MAX_LIGHTS; ++i) { - PointLight light = PointLights[i]; - vec3 light_to_frag = light.pos.xyz - v_WorldPosition.xyz; - float distance_square = dot(light_to_frag, light_to_frag); - float rangeAttenuation = - getDistanceAttenuation(distance_square, light.lightParams.r); - - // Specular. - // Representative Point Area Lights. - // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 - float a = roughness; - float radius = light.lightParams.g; - vec3 centerToRay = dot(light_to_frag, R) * R - light_to_frag; - vec3 closestPoint = light_to_frag + centerToRay * saturate(radius * inversesqrt(dot(centerToRay, centerToRay))); - float LspecLengthInverse = inversesqrt(dot(closestPoint, closestPoint)); - float normalizationFactor = a / saturate(a + (radius * 0.5 * LspecLengthInverse)); - float specularIntensity = normalizationFactor * normalizationFactor; - - vec3 L = closestPoint * LspecLengthInverse; // normalize() equivalent? - vec3 H = normalize(L + V); - float NoL = saturate(dot(N, L)); - float NoH = saturate(dot(N, H)); - float LoH = saturate(dot(L, H)); - - vec3 specular = specular(F0, roughness, H, NdotV, NoL, NoH, LoH, specularIntensity); - - // Diffuse. - // Comes after specular since its NoL is used in the lighting equation. - L = normalize(light_to_frag); - H = normalize(L + V); - NoL = saturate(dot(N, L)); - NoH = saturate(dot(N, H)); - LoH = saturate(dot(L, H)); - - vec3 diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); - - // 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 - - // 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 - // 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 - light_accum += - ((diffuse + specular) * light.color.rgb) * (rangeAttenuation * NoL); + for (int i = 0; i < int(NumLights.x) && i < MAX_POINT_LIGHTS; ++i) { + light_accum += point_light(PointLights[i], roughness, NdotV, N, V, R, F0, diffuseColor); + } + for (int i = 0; i < int(NumLights.y) && i < MAX_DIRECTIONAL_LIGHTS; ++i) { + light_accum += dir_light(DirectionalLights[i], roughness, NdotV, N, V, R, F0, diffuseColor); } vec3 diffuse_ambient = EnvBRDFApprox(diffuseColor, 1.0, NdotV);