learnopengl——Lighting——确实懂了

英文版 :https://learnopengl.com/PBR/Lighting
中文版:https://learnopengl-cn.github.io/07%20PBR/02%20Lighting/

in the previous tutorial we laid 打下了基础 the foundation for getting a realistic physically based renderer off the ground. in this tutorial we will focus on translating the previously dicussed theory into an actual renderer that uses direct (or analytic) light sources; think of point lights, directional lights and/or spotlights.

let us start by re-visiting the final reflectance equation of the previous tutorial:
在这里插入图片描述
we now know mostly what is going on, but what still remained a big unkown is how exactly we are going to represent irradiance 辐照度, the total radiance L 辐射率, of the scene. we know that radiance L (as interpreted in computer graphics land) measures the radiant flux ϕ or light energy of a light source over a given solid angle ω. 今天才知道 solid不是固体的意思,还有立体角的意思 ,也就是立体角。 In our case we assumed the solid angle ω to be infinitely small in which case radiance measures the flux of a light source over a single light ray or direction vector.

given this knowledge, how do we translate this into some of the lighting knowledge we have accumulated from previous tutorials? well, imagine we have a single point light 点光源 (a light source that shines equally bright in all directions) with a radiant flux of (23.47, 21.31, 20.79) as translated to an RGB triplet. 三元组。
the radiant intensity of this light source equals its radiant flux at all outgoing direction rays.
however, when shading a specific point p on a surface, of all possible incoming light directions over its hemisphere Ω only one incoming direction vector wi directly comes from the point light source. as we only have a single light source in our scene, assumed to be at a single point in space, all other possible incoming light directions have zero radiance observed over the surface point p:
这段话有点意思,就是说我们看下面的p点:
对于p点,我们应该考虑上半球所有入射光的照射效果,但是呢?由于我们只有一个点光源,如下图的右上角的点光源,那么此点光源只有一个入摄光线照射到p点,半球的其他方向上没有其他的光源,所以只要考虑这一个方向即可。
在这里插入图片描述

if at first, we assum that light attenuation (dimming of light over distance) does not affect the point light source, the radiance of the incoming light ray is the same regardless of where we positon the light (excluding scaling the radiance by the incident angle 入射角 cosθ). this, because the point light has the same radiant intensity regardless of the angle we look at it, effectively modeling its radiant intensity as its radiant flux: a constant vector (23.47, 21.31, 20.79).

However, radiance also takes a position p as input and as any realistic point light source takes light attenuation into account, the radiant intensity of the point light source is scaled by some measure of the distance between point p and the light source. Then, as extracted from the original radiance equation, the result is scaled by the dot product between the surface’s normal vector n and the incoming light direction wi. 辐射强度依赖于两个因素:一个是衰减,一个是法线和入射光线的夹角(也就是入射角)的大小。

To put this in more practical terms: in the case of a direct point light the radiance function L measures
the light color, attenuated over its distance to p and
scaled by n⋅wi,
but only over the single light ray wi that hits p which equals the light’s direction vector from p. In code this translates to:

vec3  lightColor  = vec3(23.47, 21.31, 20.79);
vec3  wi          = normalize(lightPos - fragPos);
float cosTheta    = max(dot(N, Wi), 0.0);
float attenuation = calculateAttenuation(fragPos, lightPos);
float radiance    = lightColor * attenuation * cosTheta;

aside from some different terminology this piece of code should be awfully familiar to u: this is exactly how we’ve been doing (diffuse) lighting so far. When it comes to direct lighting, radiance is calculated similarly to how we’ve calculated lighting before as only a single light direction vector contributes to the surface’s radiance.
Note that this assumption holds as point lights are infinitely small and only a single point in space. If we were to model a light that has volume, its radiance would equal non-zero in more than one incoming light directions.

对于其它类型的从单点发出来的光源我们类似地计算出辐射率。比如,定向光(directional light)拥有恒定的wi而不会有衰减因子;而一个聚光灯光源则没有恒定的辐射强度,其辐射强度是根据聚光灯的方向向量来缩放的。

这也让我们回到了对于表面的半球领域(hemisphere)Ω的积分∫上。由于我们事先知道的所有贡献光源的位置,因此对物体表面上的一个点着色并不需要我们尝试去求解积分。我们可以直接拿光源的(已知的)数目,去计算它们的总辐照度,因为每个光源仅仅只有一个方向上的光线会影响物体表面的辐射率。这使得PBR对直接光源的计算相对简单,因为我们只需要有效地遍历所有有贡献的光源。而当我们后来把环境照明也考虑在内的IBL教程中,我们就必须采取积分去计算了,这是因为光线可能会在任何一个方向入射。

a PBR surface model
let us start by writing a fragment shader that implements the previously described PBR models.
first, we need to take the relevant PBR inputs required for shading the surface:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

uniform vec3 camPos;

uniform vec3  albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

we take the standard inputs as calcualted from a generic vertex shader and a set of constant material properties over the surface of the object.

then at the start of the fragment shader we do the usual calculations required for any lighting algorithm:

void main()
{
    vec3 N = normalize(Normal); 
    vec3 V = normalize(camPos - WorldPos);
    [...]
}

direct lighting
in this tutorial’s example demo we have a total of 4 point lights that directly represent the scene’s irradiance.
to satisfy the reflectance equation we loop over each light source, calcualte its individual randiance and sum its contribution scaled by the BRDF and the light’s incident angle. 入射角 we can think of the loop as solving the intergral ∫ over Ω for direct light sources. first, we calcualte the relevant per-light variables:

vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i) 
{
    vec3 L = normalize(lightPositions[i] - WorldPos);
    vec3 H = normalize(V + L);
  
    float distance    = length(lightPositions[i] - WorldPos);
    float attenuation = 1.0 / (distance * distance);
    vec3 radiance     = lightColors[i] * attenuation; 
    [...]  

as we calcualte lighting in linear space (we will gamma correct at the end of the shader) we attenuate the light sources by the more physical correct inverse-square law.

while physically correct, u may still want to use the constant, linear, quadratic attenuation equation that (while not physically correct) can offer u significantly more control over the light's energy falloff.

then, for each light we want to calcualte the full cook-torrance specular BRDF term:
在这里插入图片描述

the first thing we want to do is calculate the ratio between specular and diffuse reflection, or how much the surface reflects lights versus how much it refracts light 计算反射和折射的比率. we know from the previous tutorial that the fresnel equation calcualtes just that:

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
} 

the fresel-schlick approximation expects a F0 parameter which is known as the surface reflection at zero incidence 0度入射角 or how much the surface reflects if looking directly at the surface. 垂直看表面。 the F0 varies per material and is tinted on metal as we find in large material databases. in the PBR metallic workflow we make the simplifying assumption that most dielectric surface look visually correct with a constant F0 of 0.04 while we do specify F0 for metallic surfaces as then given by the albedo value. 对于绝缘体使用0.04,而对于金属则应该使用albedo反射率。

在这里插入图片描述

this translates to code as follows:

vec3 F0 = vec3(0.04); 
F0      = mix(F0, albedo, metallic);
vec3 F  = fresnelSchlick(max(dot(H, V), 0.0), F0);

我们想下,F0=0.04,说明反射的比折射的要小很多,也就是说绝缘体,反射的光不强烈,所以这个值应该很小。有道理。

as u can see, for non-metallic surfaces F0 is always 0.04, while we do vary F0 based on the metalness of a surface by linearly interpolating between the original F0 and the albedo value given the metallic property.

given F, the remaing terms to calculate are the normal distribution function D and the geometry function G.
in a direct PBR lighting shader their code equivalents are:

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a      = roughness*roughness;
    float a2     = a*a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;
	
    float num   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
	
    return num / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float num   = NdotV;
    float denom = NdotV * (1.0 - k) + k;
	
    return num / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2  = GeometrySchlickGGX(NdotV, roughness);
    float ggx1  = GeometrySchlickGGX(NdotL, roughness);
	
    return ggx1 * ggx2;
}

what is important to note here is that in contrast to the theory tutorial, we pass the roughness parameter directly to these functions; this way we can make some term-specific modifications to the original roughness value. based on observations by disney and adopted by epic games the lighting looks more correct squaring the roughness in both the geometry and normal distribution function.

with both functions defined, calculating the NDF and the G term in the reflectance loop is straightforward:

float NDF = DistributionGGX(N, H, roughness);       
float G   = GeometrySmith(N, V, L, roughness);    

this gives us enough to calcualte the cook-torrance BRDF:

vec3 numerator    = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
vec3 specular     = numerator / max(denominator, 0.001);  

note that we constrain the deominator to 0.001 to prevent a divide by zero in case andy dot product ends up 0.0.
now we can finally calcualte each light’s contribution to the reflectance equation. as the fresel value directly cooresponds to ks we can use F to denote the specular contribution of any light that hits the surface. from ks we can then directly calcualte the ratio of refraction kd:

vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
  
kD *= 1.0 - metallic;	

seeing as ks represents the energy of light that gets reflected, the remaining ratio of light energy is the light that gets refracted which we store as KD. futhermore, because metallic surfaces do not refract light and thus have no diffuse reflections we enfore this property by nullifying kD if the surface is metallic. this gives use the final data we need to calcualte each light’s outgoing reflectance value:

   const float PI = 3.14159265359;
  
    float NdotL = max(dot(N, L), 0.0);        
    Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}

the reuslting Lo value, or the outgoing radiance, is effectively the result of the reflectance equation’s intergral ∫ over Ω. we do not really have to try and solve the integral for all possible incoming directions as we know exactly the 4 incoming light directions that can influence the fragment. because of this, we can directly loop over these incoming light directions e.g. 也即是说 the number of lights in the scene.

what 's left is to add an (improvised) 简易的 ambient term to the direct lighting result Lo and we have the final lighted color of the fragment:

vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color   = ambient + Lo; 

linear and HDR rendering

so far we have assumed all our calcualtions to be in linear color space and to account for this we need to gamma correct at the end of the shader. calculating lighting in linear space is incredibly important as PBR requires all inputs to be linear, not taking this into account will result in incorrect lighting. additionally, we want light inputs to be close to their physical equivalents such that their radiance or color values can vary wildly over a high spectrum of values. as a result Lo can rapidly grow really high which then gets clamped between 0.0 and 1.0 due to the default low dynamic range (LDR) output. we fix this by taking Lo and tone or exposure map the high dynamic range (HDR) value correctly to LDR before gamma correction:

 // HDR tonemapping
    color = color / (color + vec3(1.0));
    // gamma correct
    color = pow(color, vec3(1.0/2.2)); 

Here we tone map the HDR color using the Reinhard operator, preserving the high dynamic range of possibly highly varying irradiance after which we gamma correct the color. We don’t have a separate framebuffer or post-processing stage so we can directly apply both the tone mapping step and gamma correction step directly at the end of the forward fragment shader.
在这里插入图片描述
Taking both linear color space and high dynamic range into account is incredibly important in a PBR pipeline. Without these it’s impossible to properly capture the high and low details of varying light intensities and your calculations end up incorrect and thus visually unpleasing.

Full direct lighting PBR shader
All that’s left now is to pass the final tone mapped and gamma corrected color to the fragment shader’s output channel and we have ourselves a direct PBR lighting shader. For completeness’ sake, the complete main function is listed below:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

// material parameters
uniform vec3  albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

uniform vec3 camPos;

const float PI = 3.14159265359;
  
float DistributionGGX(vec3 N, vec3 H, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 fresnelSchlick(float cosTheta, vec3 F0);

void main()
{		
    vec3 N = normalize(Normal);
    vec3 V = normalize(camPos - WorldPos);

    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);
	           
    // reflectance equation
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i) 
    {
        // calculate per-light radiance
        vec3 L = normalize(lightPositions[i] - WorldPos);
        vec3 H = normalize(V + L);
        float distance    = length(lightPositions[i] - WorldPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance     = lightColors[i] * attenuation;        
        
        // cook-torrance brdf
        float NDF = DistributionGGX(N, H, roughness);        
        float G   = GeometrySmith(N, V, L, roughness);      
        vec3 F    = fresnelSchlick(max(dot(H, V), 0.0), F0);       
        
        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;	  
        
        vec3 numerator    = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        vec3 specular     = numerator / max(denominator, 0.001);  
            
        // add to outgoing radiance Lo
        float NdotL = max(dot(N, L), 0.0);                
        Lo += (kD * albedo / PI + specular) * radiance * NdotL; 
    }   
  
    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;
	
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));  
   
    FragColor = vec4(color, 1.0);
} 

hopefully with the theory from the previous tutorial and the knowledge of the reflectance equation this shader should not be as daunting anymore 令人生畏. if we take this shader, 4 point lights and quite a few spheres where we vary both their metallic and roughness values on their vertical and horizontal axis respectively, we would get sth. like this:
在这里插入图片描述

from bottom to top the metallic value ranges from 0.0 to 1.0, with roughness increasing left to right from 0.0 to 1.0. u can see that by only changing these two simple to understand parameters we can already display a wide array of different materials.

You can find the full source code of the demo here. https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.1.lighting/lighting.cpp

textured PBR
extending the system to now accept its surface parameters as textures instead of uniform values gives us per-fragment control over the surface material’s properties:

[...]
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
  
void main()
{
    vec3 albedo     = pow(texture(albedoMap, TexCoords).rgb, 2.2);
    vec3 normal     = getNormalFromNormalMap();
    float metallic  = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao        = texture(aoMap, TexCoords).r;
    [...]
}

note that the albedo textures that come from artists are generally authored in sRGB space which is why we first convert them to linear space before using albedo in our lighting calculations. based on the system artists use to generate ambient occlusion maps u might also have to convert these from sRGB to linear space as well. metallic and roughness maps are almost always authored in linear space.
这里有几个图:
albedo:反射率图
normal:法线图
metallic:金属度图
roughness:粗糙度图
ao:环境遮蔽图

replacing the material properties of the previous set of spheres with textures, already shows a major visual improvement over the previous lighting algorithms we have used:
在这里插入图片描述
u can find the full source code of the textured demo here https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.2.lighting_textured/lighting_textured.cpp
and the texture set i have used here https://freepbr.com/materials/rusted-iron-pbr-metal-material-alt/
with a white ao map.
keep in mind that metallic surfaces tend to look to dark in direct lighting environments as they do not have diffuse reflectance.
they do look more correct when taking the environment’s specular ambient lighting in account which is what we will focus on in the next tutorials.
While not as visually impressive as some of the PBR render demos you find out there, given that we don’t yet have image based lighting built in, the system we have now is still a physically based renderer and even without IBL you’ll see your lighting look a lot more realistic

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值