LearnOpenGL笔记——六、PBR:光照

六、PBR:6.2 光照

  • 把以前讨论过的理论转化为实际的渲染器,这个渲染器将使用直接的(或解析的)光源:比如点光源,定向灯或聚光灯。

  • 我们先来看看上一个章提到的反射方程的最终版:
    在这里插入图片描述

  • 我们大致上清楚这个反射方程在干什么,但我们仍然留有一些迷雾尚未揭开。

  • 比如说我们究竟将怎样表示场景上的辐照度(Irradiance), 辐射率(Radiance) L

  • 我们知道辐射率L(在计算机图形领域中)表示在给定立体角ω的情况下光源的辐射通量(Radiant flux)?或光源在角度ω下发送出来的光能。

  • 在我们的情况下,不妨假设立体角ω无限小,这样辐射度就表示光源在一条光线或单个方向向量上的辐射通量。

  • 基于以上的知识,我们如何将其转化为以前的教程中积累的一些光照知识呢?

  • 那么想象一下,我们有一个点光源(一个光源在所有方向具有相同的亮度),它的辐射通量为用RBG表示为 (23.47,21.31,20.79)

  • 该光源的辐射强度(Radiant Intensity)等于其在所有出射光线的辐射通量。

  • 然而,当我们为一个表面上的特定的点p着色时,在其半球领域Ω的所有可能的入射方向上,只有一个入射方向向量ωi直接来自于该点光源。

  • 假设我们在场景中只有一个光源,位于空间中的某一个点,因而对于p点的其他可能的入射光线方向上的辐射率为0。
    LearnOpenGL笔记——六、PBR:光照_第1张图片

  • 如果从一开始,我们就假设点光源不受光线衰减(光照强度会随着距离变暗)的影响,那么无论我们把光源放在哪,入射光线的辐射率总是一样的(除去入射角cosθ对辐射率的影响之外)。

  • 正是因为无论我们从哪个角度观察它,点光源总具有相同的辐射强度,我们可以有效地将其辐射强度建模为其辐射通量: 一个常量向量 (23.47,21.31,20.79)

  • 然而,辐射率也需要将位置p作为输入,正如所有现实的点光源都会受光线衰减影响一样,点光源的辐射强度应该根据点p所在的位置和光源的位置以及他们之间的距离而做一些缩放。

  • 因此,根据原始的辐射方程,我们会根据表面法向量n和入射角度wi来缩放光源的辐射强度。

  • 在实现上来说:对于直接点光源的情况,辐射率函数L先获取光源的颜色值, 然后光源和某点p的距离衰减,接着按照n?wi缩放,但是仅仅有一条入射角为wi的光线打在点p上, 这个wi同时也等于在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))光照的!

  • 当涉及到直接照明(direct lighting)时,辐射率的计算方式和我们之前计算当只有一个光源照射在物体表面的时候非常相似。

    • 请注意,这个假设是成立的条件是点光源体积无限小,相当于在空间中的一个点。如果我们认为该光源是具有体积的,它的辐射会在一个以上的入射光的方向不等于零。
  • 对于其它类型的从单点发出来的光源我们类似地计算出辐射率。

    • 比如,定向光(directional light)拥有恒定的wi而不会有衰减因子
    • 而一个聚光灯光源则没有恒定的辐射强度,其辐射强度是根据聚光灯的方向向量来缩放的。
  • 这也让我们回到了对于表面的半球领域(hemisphere)Ω的积分上。

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

一个PBR表面模型

  • 此部分为反射方程在片段着色器中的实现,见官网即可

线性空间和HDR渲染

  • 我们没有使用一个独立的帧缓冲或者采用后期处理,所以我们需要直接在每一步光照计算后采用色调映射和伽马矫正。

完整的直接光照PBR着色器

#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 a = roughness*roughness;
    float a2 = a*a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / 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;
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// ----------------------------------------------------------------------------
void main()
{		
    vec3 N = normalize(Normal);
    vec3 V = normalize(camPos - WorldPos);

    // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0 
    // of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
    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(clamp(dot(H, V), 0.0, 1.0), F0);
           
        vec3 numerator    = NDF * G * F; 
        float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; // + 0.0001 to prevent divide by zero
        vec3 specular = numerator / denominator;
        
        // kS is equal to Fresnel
        vec3 kS = F;
        // for energy conservation, the diffuse and specular light can't
        // be above 1.0 (unless the surface emits light); to preserve this
        // relationship the diffuse component (kD) should equal 1.0 - kS.
        vec3 kD = vec3(1.0) - kS;
        // multiply kD by the inverse metalness such that only non-metals 
        // have diffuse lighting, or a linear blend if partly metal (pure metals
        // have no diffuse light).
        kD *= 1.0 - metallic;	  

        // scale light by NdotL
        float NdotL = max(dot(N, L), 0.0);        

        // add to outgoing radiance Lo
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;  // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again
    }   
    
    // ambient lighting (note that the next IBL tutorial will replace 
    // this ambient lighting with environment lighting).
    vec3 ambient = vec3(0.03) * albedo * ao;

    vec3 color = ambient + Lo;

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

    FragColor = vec4(color, 1.0);
}

带贴图的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;
        [...]
    }
    
  • 不过需要注意的是一般来说反射率(albedo)纹理在美术人员创建的时候就已经在sRGB空间了,因此我们需要在光照计算之前先把他们转换到线性空间。

  • 一般来说,环境光遮蔽贴图(ambient occlusion maps)也需要我们转换到线性空间。

  • 不过金属性(Metallic)和粗糙度(Roughness)贴图大多数时间都会保证在线性空间中。

  • 只是把之前的球体的材质性质换成纹理属性,就在视觉上有巨大的提升。

  • 相比起在网上找到的其他PBR渲染结果来说,尽管在视觉上不算是非常震撼,因为我们还没考虑到基于图片的关照,IBL。

  • 我们现在也算是有了一个基于物理的渲染器了(虽然还没考虑IBL)!你会发现你的光照看起来更加真实了。

  • 两个小坑

    • 首先是球体的生成,主流的球体顶点生成有两种方法,作者源码采用的是UVSphere方法, 还有IcoSpher方法
    • 对于贴图的PBR来说,我们需要TBN矩阵做坐标转换(切线空间-> 世界空间 或者 世界空间 -> 切线空间,参考 法线贴图 章节。)。
      • 这有两种方法
      • 一种是在片段着色器中使用叉乘计算TBN矩阵(作者采用的方法);
      • 另外一种是在根据顶点预计算TBN然后VAO中传入TBN矩阵,
      • 理论上来说后者会比较快(但是比较麻烦),不过在译者的实际测试中两者速度差距不大。

你可能感兴趣的:(java,后端,人工智能,几何学,机器学习)