PBR Part Ⅱ 直接光照篇

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u013746357/article/details/84955095

这篇文章为翻译文章,为避免翻译的文章不在原创列表列里,设置为原创,特此声明

原文地址: https://learnopengl.com/PBR/Theory

Lighting

照明

前一篇教程中我们介绍了PBR渲染的基本原理。这个教程中我们会把前面讨论的原理使用直接光源(点光源、平行光、聚光灯等)应用到真实的渲染中。

让我们重新看一下前面讲的反射方程:
在这里插入图片描述
我们现在已经知道了这个公式是如何进行处理的,但是我们依然不知道如何精确的表示场景中辐射率L的总和也即是辐射度。我们知道辐射率L计算指定光源在一个给定的立体角ω上的辐射通量Φ。我们假设这个立体角ω无限小,以至于辐射率只需要计算光源在一个单一光线方向上的通量。
在这个前提下,我们如何把这个假设转换到我们已有的光照知识的计算中?想象我们有一个转换到RGB三元组的辐射通量(23.47,21.31,20.79)的点光源。光照强度在所有方向上都相同。然而,当对一个指定的点p进行着色的时候,所有的通过包围这个点p的包围半球Ω的可能的入射光线中,只有一个入射方向向量wi是直接来自于这个点光源的。因为我们只有一个点光源,对这个点p来讲所有其他的潜在的入射光线的方向上辐照率都为0。
在这里插入图片描述
首先我们假设点光源的衰减(随着距离光照变暗)可以忽略,无论光源在什么位置,入射光线在p点的的辐射率都是一致的,这是因为点光源在所有方向上都有相同的辐射强度,因此可以使用它的辐射通量(23.47,21.31,20.79)来作为辐射强度。
然而,辐射率会把位置p作为输入参数,点光源的衰减也要进行计算,点光源的辐射强度是通过光源和点p之间的距离进行计算的。除此之外,就像原始的辐射方程指出的那样,辐射度受表面法线和入射光线方向的夹角影响,夹角的余弦通过点乘这两个向量的归一化后的值进行计算。
在使用点光源的情况下,使用光照颜色作为辐射率、光照强度随着离点p的距离衰减并且辐射度根据法线和入射光线的夹角的余弦等比缩放。这个过程用代码实现如下:

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;

除了一些不一样的术语名称,这个代码段的内容很容易理解,几乎和我们处理传统的漫反射(diffuse)光照一样。当处理单个直接光源的时候,辐射率的计算和我们之前处理光源的方式一样。

需要注意的是,这里是假定点光源无限小,以至于只是空间中的一个点。如果我们的光源有体积,它的辐射率的计算,在多个入射光源方向上都会有非零的值。

对于其他来自于一个点的光源类型,我们计算辐照度的方式几乎一样。例如:平行光源有一个固定的光照方向wi并且不会衰减,而探照灯的辐射强度会根据探照灯的前向向量进行衰减。
现在开始计算表面的半球Ω的积分∫。当对单个表面的点进行光照计算时,因为假定每一个光源只有一个光照方向影响到了这个点的辐照度,我们已知了对该点有贡献的光源的信息,我们不需要计算所有方向上的积分,我们只需要计算这些有限个的光源的辐照度之和。我们只需要遍历对这个点有贡献的光源即可,这使得对于直接光源的PBR的计算相对简单。当我们之后把环境光考虑在内时,就像在后边的IBL 的教程中描述的那样,我们才真正需要对所有方向上的光照进行积分,因为这时,任何一个方向上都有光照产生。

A PBR surface model

基于物理的表面模型

我们来写一个片元着色器来实现前面描述的PBR模型。首先,我们需要PBR渲染相关的输入:

#version 330 coreout 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;

我们使用顶点着色器的标准输出数据(世界坐标,顶点法线,UV)以及一整套的常量材质属性(表面反照率,金属度,粗糙度和ao)作为输入。
然后在片元着色器的开始,获取归一化的法线和观察方向:

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

Direct lighting

直接光源

在这个教程的demo中,我们使用4个点光源来表示场景的辐射度。为了满足反射方程,我们遍历每一个光源,计算他们单独的辐射率并且根据BRDF和光源的入射角度进行求和。我们可以把这个循环遍历看作对半球Ω的直接光源的积分。首先我们计算出相对于每一个光源的一些变量:

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;
 [...]

因为我们是在线性空间下(在着色器的结尾会进行伽马校正)计算光照,我们使用距离平方的倒数来计算光源衰减,这种方式在物理上更加正确。

尽管使用距离平方的倒数计算衰减度在物理上是正确的,我们依然会希望使用常量、线性、二次方程等这些物理上不正确的方式来模拟光照衰减方程,因为这样可以提供更加灵活的光照衰减。

然后对于每一个光源我们都计算完全的Cook-Torrance镜面反射BRDF因子:
在这里插入图片描述
我们要做的第一件事是计算镜面反射和漫反射的比例。这个计算可以通过菲涅尔方程进行计算。

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

这个方程需要一个基本反射率F0作为输入。这个基本反射率对于不同的材质都不同,我们可以从一个大的材质数据库找到这个值。在PBR的金属工作流中,我们假定大部分非金属表面使用视觉上可靠的常量0.04作为基本反射率,并对金属表面使用表面反照率Albedo来计算基本反射率,代码实现如下:

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

对于非金属表面,F0总是0.04,而对于金属表面,我们根据金属度,对表面反照率Albedo和0.04来插值计算不同的F0。
有了函数F,剩下的就是计算法线分布函数D和几何函数G。它们对应的代码如下:

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;
}

需要注意的是,对比于原理篇,我们直接把表面粗糙度参数传入了这些函数;因为这样我们可以对原始的粗糙度值进行特殊的调整。基于迪士尼和Epic Games的观察结果,这些修改会使光照更加正确。
利用上面的方程,计算法线分布函数-NDF和几何函数-G部分会非常简单:

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

有了这些计算,我们就有了计算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);

需要注意的是,我们限制分母部分有一个最小值0.001,以免出现分母为0的情况,因为前面向量点乘的结果可能为0。

现在我们可以计算每一个光源对反射方程的贡献度。因为菲涅尔值F直接对应于反射率kS,我们可以使用F来表示投射到表面的光源的镜面反射贡献度。有了kS,就可以很容易的计算出对应的折射比例kD:

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

可以看到,kS代表的是辐射能量中发生镜面反射的比例,剩余的部分就是发生折射的比例kD。另外,因为金属表面会把进入内部的光线全部吸收不会有漫反射发生,我们把kD的值根据非金属度(1-金属度)进行了缩放。

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

这里的Lo的累加结果就是我们对半球Ω的∫。我们不需要测试并且计算半球所有入射方向上的积分,因为我们清楚的知道只有4个点光源的光线会影响到这个片元。正是因为这样,我们可以直接遍历场景内的光源来计算积分。
最后我们为最终的直接光积分结果Lo临时加上了一个环境光(ambient)来作为最终的光照输出。

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

Linear and HDR rendering

线性和高动态范围(HDR)渲染

目前为止,我们都是在线性颜色空间下进行计算,因此我们需要在着色器的结尾进行伽马校正(gamma correct)。在线性颜色空间下进行光照计算是非常重要的,因为PBR要求所有的输入数据都必须是线性的,如果不注意这一点会得到不正确的光照结果。除此之外,我们希望光照输入能尽可能的接近他们的物理上对应值,以便他们的辐射率或者颜色值能够在一个比较宽的高光谱范围内。因为Lo的结果可能在一个比较大的范围内然后被收缩到低动态范围(LDR) 0.0-1.0的范围内。为了解决这个问题,我们在伽马校正之前把HDR下的Lo和色调/曝光正确的映射到LDR范围。

color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2));

在这里我们使用Reinhard运算符对HDR颜色进行色调映射,保持可能高度变化的高动态范围的辐照度,之后我们进行伽马校正。因为我们没有额外的帧缓冲或者后处理阶段,因此我们可以直接把色调映射和伽马校正的应用到前向片元着色器的结尾。
在这里插入图片描述

在BRP管线中把线性空间和HDR考虑在内是非常重要的。没有这些内容几乎不可能捕捉到光照迁都变换的高低细节,并且计算结果会不正确以至于视觉上不舒服。

Full direct lighting PBR shader

完整的直接光照的PBR着色器

目前剩下的工作就是把最终的色调映射和伽马校正的颜色从片元着色器输出,然后我们就有了完整的直接光的PBR着色器。完整起见,着色器的主函数如下:

#version 330 coreout 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 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness);
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);
}

希望有了上一章的理论和反射方程的知识,这里的代码能够看上去不再生涩难懂。如果我们使用这个着色器,四个点光源,然后分别按照竖直方向和水平方向的排列方式调整了一系列球体的金属度和粗糙度的值,他们的显示结果就会如下所示:
在这里插入图片描述
从上到下金属度范围从0.0到1.0,而粗糙度按照从左到右0.0-1.0的方式设置。可以看到,只需要修改这两个简单的参数就可以得到各种的材质效果。

Textured PBR

基于纹理贴图的PBR

通过把统一的参数扩展为使用纹理贴图,我们可以逐片元的控制这些表面材质属性:

[...]
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;
[...]
}

需要注意的是,艺术家提供的反照率贴图通常实是sRGB颜色空间下,因此我们需要先把他们转换到线性空间下才能用于我们的光照计算。基于艺术家使用的用于生成AO贴图的系统,我们可能也需要把他们从sRGB空间转换到线性空间。金属度和粗糙度贴图基本上都是直接在线性线性空间下制作的。

在这里插入图片描述

需要注意的是金属表面在只考虑直接光源的环境下通常会看起来很暗,因为他们没有漫反射(所有折射的光线都被吸收掉)。但是如果我们把环境的镜面反射环境照明考虑在内的话会看起来更加真实,这个内容我们会下一个教程中介绍。

尽管没有使用基于图片的照明IBL,我们的效果也没有你在其他地方看到的那些效果在视觉更加震撼,我们目前的系统依然是一个PBR的渲染流程。即使没有IBL,你的照明结果依然会看起来更加真实。

猜你喜欢

转载自blog.csdn.net/u013746357/article/details/84955095
PBR
今日推荐