LearnOpenGL.PBR.光照

光源辐射率: 
    辐射率(radiance)表示光源在给定立体角ω下的辐射通量(或光源发射的能量)。
    那么假设立体角ω无限小时,辐射率就表示单束光线(或说某个单一方向)的辐射通量。
点光源:point light,在所有方向都有相同的亮度,辐射强度(radiant intensity)等于其发射出来的所有方向的辐射通量(radiant flux)。
    对于场景中的一个点p,只会有一束光直接照射到点p,其他 光线的辐射率都为0。
    辐射强度:点光源无论从任何角度看,点光源都有相同的辐射强度,所以可以简单地 使用其辐射通量表示辐射强度,也就是一个RGB常亮。
    辐射率:需要考虑点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漫反射光照一样。
    前提:假设点光源无限小,如果有体积,点光源会有一个以上的入射光线辐射率不为0。
方向光:directional light,辐射率拥有恒定的入射方向,而且不会有衰减。
聚光灯:spotlight,没有恒定的辐射强度,而是会根据照射方向有所不同。
 
直接光照:
    辐照度=所有光源的辐射率,所以直接光照的计算非常简单,只需要逐个光源计算辐射率,然后加在一起。接着根据BRDF和光源的入射角来缩放该辐射率。
    这个算法也是符合反射率方程(The reflectance equation)的积分运算的。
    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;
        [...]  
    由于我们在线性空间内计算光照,我们使用在物理上更为准确的平方倒数(inverse-square law)作为衰减因子。
    对于每一个光源都需要计算完整的BRDF项:
    Ks = F,也就是菲涅尔系数,表示光线给反射的百分比:
    vec3 fresnelSchlick(float cosTheta, vec3 F0)
    {
        return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
    }
    Fo表示0度入射角的反射(surface relfection at zero incidence),也就是垂直看向材质时的反光率。Fo会因材质的不同而不同,而且金属的反光还会带有颜色。大多数非金属材质取0.04都能取得视觉上物理可信的效果。对于金属材质则直接取albedo贴图的颜色值。
    vec3 F0 = vec3(0.04);
    F0      = mix(F0, albedo, metallic);
    vec3 F  = fresnelSchlick(max(dot(H, V), 0.0), F0);
    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 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;
    }
    根据Disney公司的观察和Epic采用经验,在D和G的计算公式中 使用roughness * roughness来进行计算会有更正确的光照效果
    float NDF = DistributionGGX(N, H, roughness);       
    float G   = GeometrySmith(N, V, L, roughness);
    Cook-Torrance BRDF反射高光部分:
    vec3 nominator    = NDF * G * F;
    float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001;
    vec3 specular     = nominator / denominator;
    额外加了一个0.001是为了防止除0。
    菲涅尔方程求出了Ks,那么Kd=1-Ks,但同时考虑到金属没有折射光线,也就是没有漫反射,所以金属的Kd=0,代码如下:
    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;

    kD *= 1.0 - metallic;   
    最终的反射率:
    const float PI = 3.14159265359;

    float NdotL = max(dot(N, L), 0.0);        
    Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}   
    可以看到specular没有再乘一次Ks,是因为DFG里面的F实际上就是Ks。
    再加一个环境光项(ao贴图):
    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color   = ambient + Lo;  
 
IBL环境光照:
    需要计算积分,因为光线会在任何一个方向入射到物体表面。
 
线性空间和HDR渲染:
    我们之前所有的运算都必须在线性空间(linear space)进行,所以我们需要在shader的最后做伽马校正(gamma correct)。
    线性空间:PBR要求所有的输入都是线性的,不然计算的结果会不正确。
    我们希望输入的光照都尽可能接近真实,那么计算出的辐射率范围可能会非常大,也就是说Lo是一个HDR的值(大于1.0)。但最终的颜色输出范围是LDR的,所以在gamma correct之前,我们会先通过色调映射(tone or exposure map)将HDR值映射到LDR的值。
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2)); 
    这里我们使用了Reinhard方法来进行色调映射,我们没有使用framebuffer或post-processing的方式来进行色调映射,所以我们直接在shader的最后添加了这两步。
    PBR pipeline需要考虑线性空间和HDR的因素,不然可能渲染出来的画面会丢失细节,并且视觉上不正确看起来也不好看。
 
完整的直接光照PBR shader:
    #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, 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);
    }  
 
带贴图的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空间,所以需要在计算光照之前手动转到线性空间。ao贴图也一样。不过metallic和roughness多数都会直接使用线性空间来保存。
 

猜你喜欢

转载自www.cnblogs.com/sifenkesi/p/11924857.html