Learn OpenGL 笔记7.2 PBR Lighting(physically based rendering基于物理的渲染 光照)

在上一章中,我们为实现基于物理的逼真渲染器奠定了基础。 在本章中,我们将专注于将前面讨论的理论转化为使用直接(或分析)光源的实际渲染器:想想点光源、定向光源和/或聚光灯。

让我们从重新审视上一章的最终反射率方程开始:

我们现在大部分都知道发生了什么,但仍然是一个很大的未知数是我们将如何准确地表示irradiance辐照度,即场景的总radiance辐射度L。 我们知道,辐射度 L(如计算机图形领域中所解释的那样)测量给定solid angle立体角 ω 上光源的radiant flux辐射通量 φ 或光能。 在我们的例子中,我们假设立体角 ω 无限小,在这种情况下,辐射度表示光源在单个光线方向矢量上的flux通量。 

假设我们有一个single point light单点光源(一个在所有方向上都同样明亮的光源),其radiant flux辐射通量为 (23.47, 21.31, 20.79),记录为 RGB triplet三元组。 该光源的radiant intensity辐射强度等于其在所有出射方向光线的radiant flux辐射通量。 然而,当对表面上的特定点 p 进行着色时,在其半球 Ω 上所有可能的入射光方向中,只有一个入射方向矢量 wi 直接来自点光源。 由于我们的场景中只有一个光源,假设是空间中的一个点,所有其他可能的入射光方向在表面点 p 上观察到的辐射度为零:(wi等于一个极度小的立体角solid angle

如果一开始,我们假设light attenuation光衰减(光随距离变暗)不会影响点光源,那么无论我们将光放在哪里,入射光线的radiance辐射率都是相同的(不包括通过入射角cosθ变化,会缩放辐射率)。这是因为无论我们从哪个角度看,点光源都具有相同的radiant intensity辐射强度,因此有效地将其radiant intensity辐射强度(强度)建模为其radiant flux辐射通量(量):一个常数向量 (23.47, 21.31, 20.79)。(理解:不管是把点光源放到左边,还是右边,射入p点的辐射强度大小都一致)

然而,radiance辐射也将位置 p 作为输入,并且由于任何现实的点光源都考虑了光衰减,点光源的radiant intensity辐射强度通过点 p 和光源之间的距离的某种度量来缩放。然后,从原始辐射方程中提取,结果通过surface normal表面法线 n incoming light direction入射光方向 wi 之间的点积进行缩放。

更实际地说:在直接点光源的情况下,direct point light the radiance function辐射函数 L 测量光的颜色,在点光源到 p 的距离上衰减并按 n·wi 缩放,但仅在hits击中 p 单个光线 wi 等于来自 p 的光的方向向量。在代码中,这转化为:

//radiant intensity辐射强度(强度)建模为其radiant flux辐射通量(量)
vec3  lightColor  = vec3(23.47, 21.31, 20.79);
//入射的极小的solid angle 立体角
vec3  wi          = normalize(lightPos - fragPos);
// n 点积 wi
float cosTheta    = max(dot(N, Wi), 0.0);
// 点光源的衰减
float attenuation = calculateAttenuation(fragPos, lightPos);
// 辐射
vec3  radiance    = lightColor * attenuation * cosTheta;

除了不同的术语,这段代码对你来说应该非常熟悉:这正是我们迄今为止所做的漫反射照明的方式。在直接照明方面,辐射度的计算方式与我们之前计算照明的方式类似,因为只有一个光方向矢量对表面的辐射度有贡献。

请注意,这个假设成立,因为点光源无限小并且在空间中只有一个点。如果我们要对具有面积或体积的光进行建模,它的辐射在多个入射光方向上将是非零的。
对于源自单个点的其他类型的光源,我们类似地计算辐射度。例如,定向光源具有恒定的 wi 而没有衰减因子。聚光灯不会具有恒定的辐射强度,而是由聚光灯的前向矢量缩放的。

这也让我们回到了surface's hemisphere表面半球 Ω 上的integral积分 ∫。正如我们在shading a single surface point为单个表面点着色时预先知道所有贡献光源的单个位置一样,不需要尝试求解积分。我们可以直接获取(已知的)光源数量并计算它们的总irradiance辐照度,因为每个光源只有一个影响surface's radiance表面辐射度的光方向。这使得直接光源上的 PBR(physic based render)相对简单,因为我们实际上只需要遍历贡献的光源。当我们稍后在 IBL 章节中考虑环境照明时,我们必须考虑积分,因为光可以来自任何方向。

1. A PBR surface model (PBR 曲面模型)

让我们从编写一个片段着色器开始,它实现了前面描述的 PBR 模型。 首先,我们需要获取对表面进行着色所需的相关 PBR 输入:

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

我们采用从通用顶点着色器和对象表面上的一组恒定材质属性计算的标准输入。

然后在片段着色器开始时,我们进行任何光照算法所需的常规计算:

void main()
{
    //法线向量
    vec3 N = normalize(Normal); 
    //视图向量
    vec3 V = normalize(camPos - WorldPos);
    [...]
}

1.1 Direct lighting 直接照明

在本章的示例演示中,我们总共有 4 个点光源,它们一起代表场景的irradiance辐照度。 为了满足reflectance equation反射方程,我们循环遍历每个光源,计算其单独的radiance 辐射率并将其按 BRDF(bidirectional reflectance distribution function)和光的incident angle入射角缩放的贡献相加。 我们可以将for循环视为解决直接光源的integral积分∫,over  Ω。 首先,我们计算相关的 per-light 变量:

vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i) 
{
    // light向量
    vec3 L = normalize(lightPositions[i] - WorldPos);
    // halfway向量
    vec3 H = normalize(V + L);
    // 物体p与光源的距离 distance
    float distance    = length(lightPositions[i] - WorldPos);
    // 衰减
    float attenuation = 1.0 / (distance * distance);
    // 辐射量
    vec3 radiance     = lightColors[i] * attenuation; 
    [...]  

当我们在线性空间中计算光照时(我们将在着色器的末尾进行伽马校正),我们通过更物理上正确的平方反比定律来衰减光源。

虽然物理上正确,但您可能仍想使用常数线性二次衰减方程(虽然物理上不正确)可以为您提供对光能量衰减的更多控制。
然后,对于每个灯光,我们要计算完整的 Cook-Torrance 镜面反射 BRDF 项:

 

我们要做的第一件事是计算specular镜面反射和diffuse漫反射之间的比率,或者表面反射光的程度与refracts light折射光的程度。 从上一章我们知道,Fresnel equation菲涅耳方程就是这样计算的(注意这里的clamp钳位是为了防止黑点):

这里算的是F的值:

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

Fresnel-Schlick 近似需要一个 F0 参数,该参数称为零入射时的表面反射或直接看表面时表面反射的程度。 正如我们在大型材料数据库中发现的那样,F0 因材料而异,并且在金属上着色。 在 PBR 金属工作流程中,我们做了一个简化假设,即大多数电介质表面在视觉上是正确的,常数 F0 为 0.04,而我们确实为金属表面指定了 F0,然后由albedo value反照率值给出。 这转换为代码如下:

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

如您所见,对于非金属表面,F0 始终为 0.04。 对于金属表面,我们通过在原始 F0 和给定金属属性的反照率值之间进行线性插值来改变 F0。

给定 F,其余要计算的项是正态分布函数 D 和几何函数 G。

在直接 PBR 光照着色器中,它们的代码等价物是:把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;
}

//计算G的分步骤
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;
}

//计算G的第一个步骤
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;
}

这里需要注意的是,与理论章节相反,我们将粗糙度参数直接传递给这些函数; 通过这种方式,我们可以对原始粗糙度值进行一些特定于术语的修改。 根据 Disney 的观察并被 Epic Games 采用,照明看起来更正确地平方几何和正态分布函数的粗糙度。

定义这两个函数后,计算reflectance loop反射环中的 NDF 和 G 项很简单:

(NDF就是D,normal distribution function)

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)  + 0.0001;
// 镜面反射
vec3 specular     = numerator / denominator;  

请注意,我们将 0.0001 添加到分母以防止被零除以防任何点积最终为 0.0。

现在我们终于可以计算出每个光对反射方程的贡献了。 由于Fresnel value菲涅耳值直接对应于 ks,我们可以使用 F 来表示任何照射到表面的光的镜面反射贡献。 然后我们可以从 ks 计算折射比 kd:

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

因为 ks代表被反射的光的能量,所以光能的剩余比率是被折射的光,我们将其存储为 kd。 此外,由于金属表面不折射光,因此没有漫反射,如果表面是金属的,我们通过使 kd 无效来强制执行此属性。 这为我们提供了计算每个光的出射反射率值所需的最终数据:

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

得到的 Lo 值或出射辐射率实际上是反射方程的积分 ∫ 在 Ω 上的结果。 我们实际上不必尝试解决所有可能的入射光方向的积分,因为我们确切地知道可以影响片段的 4 个入射光方向。 因此,我们可以直接遍历这些入射光方向,例如 场景中的灯光数量。

剩下的就是在直接光照结果 Lo 中添加一个(临时的)环境项,我们得到了片段的最终光照颜色:

// albedo 是反照率  ao是ambient occlusion什么的,就是那个环境遮罩的,角落有阴影的那个
vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color   = ambient + Lo;  

1.2 Linear and HDR rendering (线性和 HDR 渲染 high dynamic range)

到目前为止,我们假设我们所有的计算都在线性颜色空间中,并且考虑到这一点,我们需要在着色器结束时进行伽马校正。 在线性空间中计算光照非常重要,因为 PBR 要求所有输入都是线性的。 不考虑这一点将导致不正确的照明。 此外,我们希望光输入接近于它们的物理等效值,以便它们的辐射或颜色值可以在高光谱值范围内变化很大。 因此,由于默认的低动态范围 (LDR) 输出,Lo 可以迅速增长到非常高的水平,然后被钳制在 0.0 和 1.0 之间。 我们通过在伽马校正之前将高动态范围 (HDR) 值正确地映射到 LDR 来解决此问题

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

在这里,我们使用 Reinhard operator对 HDR 颜色进行色调映射,保留可能高度变化的辐照度的高动态范围,然后对颜色进行伽马校正。 我们没有单独的帧缓冲区或后处理阶段,因此我们可以在前向片段着色器的末尾直接应用色调映射和伽马校正步骤。

 在 PBR 管道中,同时考虑线性色彩空间和高动态范围非常重要。 没有这些,就不可能正确捕捉不同光强度的高低细节,并且您的计算最终会不正确,因此在视觉上令人不快。

1.3 Full direct lighting PBR shader (全直接照明 PBR 着色器)

现在剩下的就是将最终的色调映射和伽马校正颜色传递到片段着色器的输出通道,我们有自己的直接 PBR 光照着色器。 为了完整起见,下面列出了完整的 main 函数:

#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) + 0.0001;
        vec3 specular     = numerator / denominator;  
            
        // 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);
}  

希望有了上一章的理论和反射率方程的知识,这个着色器应该不会再令人生畏了。 如果我们使用这个着色器、4 个点光源和相当多的球体,我们分别在它们的垂直轴和水平轴上改变它们的金属和粗糙度值,我们会得到这样的结果:

 从下到上,金属值范围从 0.0 到 1.0粗糙度从左到右从 0.0 增加到 1.0。 您可以看到,仅通过更改这两个简单易懂的参数,我们就可以显示各种不同的材料。

2 .Textured PBR (图形PBR渲染)

将系统扩展为现在接受其表面参数作为textures纹理而不是uniform values统一值,可以让我们按per-fragment control片段控制表面材质的属性:

[...]
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 空间中创作的,这就是为什么我们在光照计算中使用反照率之前首先将它们转换为线性空间的原因。 根据艺术家用来生成环境遮挡贴图的系统,您可能还必须将这些从 sRGB 转换为线性空间。 金属和粗糙度贴图几乎总是在线性空间中创作。

用纹理替换之前一组球体的材质属性,已经显示出比我们之前使用的光照算法有重大的视觉改进:

金属表面在直接照明环境中往往看起来太暗,因为它们没有漫反射。 考虑到环境的镜面环境光照后,它们看起来确实更正确,这是我们将在接下来的章节中关注的内容。

虽然不像你在那里找到的一些 PBR 渲染演示那样视觉上令人印象深刻,但鉴于我们还没有内置基于图像的照明,我们现在拥有的系统仍然是基于物理的渲染器,即使没有 IBL(image base lighting),你也会 看到你的灯光看起来更真实。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值