mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
Directional light (#2112)
This PR adds a `DirectionalLight` component to bevy_pbr.
This commit is contained in:
parent
d1f40148fd
commit
73f4a9d18f
5 changed files with 248 additions and 74 deletions
|
@ -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::*;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<BufferId>,
|
||||
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<RenderResourceBindings>,
|
||||
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::<PointLightUniform>();
|
||||
|
||||
let point_light_count = point_lights.iter().len().min(state.max_point_lights);
|
||||
let point_light_size = std::mem::size_of::<PointLightUniform>();
|
||||
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::<DirectionalLightUniform>();
|
||||
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::<LightCount>();
|
||||
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);
|
||||
|
|
|
@ -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::<RenderGraph>().unwrap();
|
||||
|
@ -39,7 +40,10 @@ pub(crate) fn add_pbr_graph(world: &mut World) {
|
|||
AssetRenderResourcesNode::<StandardMaterial>::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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue