• CSharpGL(55)我是这样理解PBR的


    CSharpGL(55)我是这样理解PBR的

    简介

    PBR(Physically Based Rendering),基于物理的渲染,据说是目前最先进的实时渲染方法。它比Blinn-Phong方法的真实感更强,几乎是照片级的效果。

    下图就是PBR的一个例子,读者可在CSharpGL中找到。

    +BIT祝威+悄悄在此留下版了个权的信息说:

     

    应用题

    PBR虽然看起来很复杂,但仍旧是在解一个应用题,只要明确了已知条件和所求问题,就没有什么难以理解的了。

    已知条件如下:

    对于不透明的三维模型(Cube、Sphere、Teapot等等任何三维模型)上的任意一点,我们知道它的位置vec3 p、法线向量vec3 N和纹理坐标vec2 texCoord。当观察者(你,我,摄像机等等)从某个位置观察三维模型上的这个点p时,从点p到观察者的向量记作vec3 v或vec3 wo。照射到点p的每一束光线vec3 Li,根据某种规则,都会被点p反射到很多方向上去。观察者看到的点p的颜色,就是所有恰好反射到v或wo方向上的光线的颜色。

    (注意,为论述方便,在本文中,Li是点p到入射光源的向量;v和wo是点p观察者方向的向量;所有向量的长度都是1。)

    +BIT祝威+悄悄在此留下版了个权的信息说:

     所求问题:

    观察者看到的颜色是什么?(用Lo(p, wo)表示)

    解答:这个问题目前是不可能100%完美解决的,所以只给出各种近似的计算模型,凑合着用。

    Blinn-Phong

    Blinn-Phong模型

    Blinn-Phong模型就是其中一种近似方案。

    (注意,这里“Blinn-Phong模型”中的“模型”与“三维模型”中的“模型”是两个不同的概念。“Blinn-Phong模型”中的“模型”是对光照现象的某种计算方法。“三维模型”中的“模型”指的是三维空间中的物体的形状。)

    Blinn-Phong将物体反射到每一个方向上的光,都分为漫反射diffuse和镜面反射specular这2个部分。它处理的光源,一般是平行光、点光源、聚光灯这种,从某一个点发射光的光源。

    为什么在PBR的文章里要介绍Blinn-Phong?因为PBR可以被(我)认为是Blinn-Phong的进化版本。

    在Blinn-Phong中,漫反射强度由N、Li共同决定:

    float diffuse = dot(N, Li);

    镜面反射强度由N、Li、v共同决定:

    float specular = dot(N, normalize(Li + v));

    (注意,这里的式子没有考虑diffuse和specular小于0的情况,这是为了突出重点。)

    这2种反射光加起来,配合物体的材质和光源的颜色,就得到了物体在点p处被观察者看到的颜色:

    vec3 fragColor = diffuse * material.diffuse * light.diffuse + specular * material.specular * light.specular;

    当然,最后还要加上个环境光(用常量表示):

    vec3 fragColor += ambientColor;

    有的Blinn-Phong实现可能与此稍有不同:有的将ambient和diffuse加在一起,有的用纹理(Texture)表示物体的材质,等等。但是思路都是一样的,不要纠结这里。

    +BIT祝威+悄悄在此留下版了个权的信息说:

    Blinn-Phong的缺点

    Blinn-Phong是个很不错的模型,但是它有一个比较明显的缺点:反射光的总量可能大于入射光的总量。也就是说,有时候物体反射的光的总强度居然比入射光还要大。这是不符合物理实际的。

    例如,当Li、v都等于N(即入射光和观察者都与法线方向重合)时,diffuse=1,specular=1,两者相加=2>1。我们知道,Blinn-Phong将物体反射出来的每一个方向上的光,都分为漫反射diffuse和镜面反射specular这2个部分。即使物体能够100%反射所有的入射光,(diffuse+specular)最多也就是1而已,不可能超过1。

    也就是说,Blinn-Phong虽然能保证diffuse和specular各自不超过1,但是不能保证(diffuse+specular)也不超过1。

    PBR解决了这个问题。

    PBR

    PBR不仅保证了 (diffuse+specular)<=1 ,还有别的优点:

    它能把周围环境当作一个整体的光源,这扩大了光源的范围。

    它以真实的物理量为参数,因而对美工更友好。

    它表现出照片级的真实感,且物体看起来就像本来就属于场景中一样。

    PBR模型

    PBR也将物体反射到每个方向上的光,都分为漫反射diffuse和镜面反射specular这2个部分。

    同时,它对这2种反射光的形成机制给出了自己的解释:

     

     如图所示,一些入射光Li打在点p上。仔细想想,点p实际上不是数学意义上的点,而是由很多微小的平面(长度大于光的波长,小于像素,简称微平面)组成的一小块“褶子”(褶皱程度就是粗糙度roughness)。入射光Li打在褶子上,一部分会被褶子直接反射,另一部分会被吸收进褶子内部。直接反射的,就是specular部分;吸收后,在褶子内部经过若干次碰撞(组成褶子的原子、分子会不断地反射或吸收剩下的光),有一些光会再次被反射出来,这就是diffuse部分。

    PBR模型的关键,就在于光的波长、微平面的大小、像素的大小这三者的大小关系。由于光的波长远远小于微平面的尺寸,所以就不用考虑光的衍射等现象。由于微平面的尺寸远远小于一个像素,所以可以将一个个像素视为一个个“褶子”。这样一来,虽然入射光的diffuse部分,其出射位置与入射位置不完全相同,但仍旧在同一个像素范围内,所以可以视作位置相同。

    (有人会说,会不会有的光在褶子内部被反射的很远,最终超出了一个像素的范围呢?答案是,会。那么,这种情况如何处理呢?PBR的答案是,忽略不计。)

    “褶子”只是一个称呼,事实上完美光滑的“褶子”,即微平面的排列完全平整,一点都不褶(光学平滑)是存在的,你可以在高端望远镜上找到。当然了,这是微平面级别的完美光滑,不是原子级别的。原子级别的完美光滑,据我所知还做不到。

    +BIT祝威+悄悄在此留下版了个权的信息说:

    PBR认为 (diffuse+specular)==1 始终成立。那么,先算出其中一个,自然就得知另一个了(1-specular)。

    Specular部分

    菲涅耳方程F

    当你站在清澈的海边、河边、湖边,低头向下看时,能够看到水面下的沙石泥土,但平视远处的水面时,就只能看到强烈的反光,很难看到水面下的景象。这种现象被称为菲涅耳(Fresnel)效应。更多图文介绍可以参考(http://blog.sina.com.cn/s/blog_798bec050100rigq.html)。

     

     这种现象说明,入射光被拆分后,specular所占的比例,与入射光Li和观察者v的方向有关。当然,它还与物质的材质有关。菲涅耳方程(Fresnel Equation)给出了一个计算specular的公式。不过那玩意计算起来比较费时,业界一般用它的一个近似版本:

     

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

    当然,其他版本的F函数也是存在的。

    其中的cosTheta =  max(0, dot(v, normalize(v + Li))) 。可见“它与入射光Li和观察者v的方向有关”,此言不虚。

    其中的F0就是物质的材质属性。每种材质都一个对应的F0常数。

    其返回结果为vec3 specular,就是说,黄金、白银、钢铁、巧克力,材质对光的RGB通道的反射能力不同。嗯这很科学。

    有了specular,当然就有了 vec3 diffuse = vec3(1, 1, 1) - specular 。我们稍后再讨论diffuse。

    几何函数G

    菲涅耳公式给出的,是在入射光Li和观察者v条件下,specular所占的比例。但是,褶子是粗糙的,会遮挡住specular的一部分。

    +BIT祝威+悄悄在此留下版了个权的信息说:

     

     因此,需要计算出没有被遮挡的比例,这就是几何函数(Geometry Function):

      

      

    (Kdirect是指平行光、点光源、聚光灯这样的光源应采用的公式;KIBL是将整个图片作为光源时应采用的公式。α表示表面粗糙度)

     1 float GeometrySchlickGGX(float NdotV, float roughness)
     2 {
     3     float r = (roughness + 1.0);
     4     float k = (r*r) / 8.0;
     5 
     6     float nom   = NdotV;
     7     float denom = NdotV * (1.0 - k) + k;
     8 
     9     return nom / denom;
    10 }
    11 // ----------------------------------------------------------------------------
    12 float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
    13 {
    14     float NdotV = max(dot(N, V), 0.0);
    15     float NdotL = max(dot(N, L), 0.0);
    16     float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    17     float ggx1 = GeometrySchlickGGX(NdotL, roughness);
    18 
    19     return ggx1 * ggx2;
    20 }

    当然,其他版本的G函数也是存在的。

    +BIT祝威+悄悄在此留下版了个权的信息说:

    从参数可知,遮蔽比例与入射光方向Li、法线N、观察者方向v和粗糙度roughness都是有关的。

    法线分布函数D

    那么,那些没有被遮蔽的specular部分,就全部进入观察者的眼中了吗?并没有。在这些顺利逃出来的specular中,只有那些法线方向与(V+L)相同的微平面反射的光,才能进入观察者眼中。

     

    法线分布函数(Normal Distribution Function)就给出了这个比例:

     

     (α表示表面粗糙度)

     1 float DistributionGGX(vec3 N, vec3 H, float roughness)
     2 {
     3     float a = roughness*roughness;
     4     float a2 = a*a;
     5     float NdotH = max(dot(N, H), 0.0);
     6     float NdotH2 = NdotH*NdotH;
     7 
     8     float nom   = a2;
     9     float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    10     denom = PI * denom * denom;
    11 
    12     return nom / denom;
    13 }

    当然,其他版本的D函数也是存在的。

    经过FGD的层层筛选,specular部分就很接近物理真实了。

    漫反射常量

    diffuse部分相对简单些,用一个常数c表示材质本身的颜色,与diffuse相乘即可。当然,这也是一种近似,其他的近似函数也是存在的。

    反射率方程

    将上面的各种函数综合起来,再配合一些数学系数,总的PBR公式(反射率方程)就是这样:

     

    总结一下就是:

     

     反射率方程左侧的意思是:观察者在wo方向上观察点p,他所看到的光的颜色Lo是多少?

    反射率方程右侧:Kd是diffuse所占的比例,Ks是specular所占的比例(注意Kd+Ks=1);c是材质的颜色,可以是单一的颜色vec3(r, g, b),也可以是用一个材质贴图描述texture(texMaterial, texCoord);π是数学常数;n是点p的法线向量;wi是某个入射光线的方向;DFG是上文所述的法线分布函数、菲涅耳函数和几何函数;Li(p, wi)是在wi方向上照射到点p的入射光的颜色;最左边那个长长的S和Ω符号,加上最右边的dwi符号,是积分的意思,Ω符号表示在法线n方向上的半球范围内积分。

     

     右侧的意思是:将所有入射光Li与其约束比例相乘,再加起来,就是我们应用题的答案。

    本质上这仍旧是将diffuse和specular分别计算后再相加而已,只不过PBR对specular和diffuse的量都做了限制,从而保证其和不超过1。

    其中的fr部分就是常说的BRDF函数。可见它包含了各种玩意,对物体反射光的量进行约束。

    这个公式是如何推导出来的?我不知道,暂时不是解决这个问题的时候。作为工程师,我先理解它,实现它,是第一要务。之后再从理论上推导它。

    反射率方程是不能直接用shader来写的,因为达不到实时的性能。所以我们一步步做简化。

    首先,右侧可以从加法的位置上拆分为diffuse部分和specular部分:

     

     这样,就可以分别去研究如何实现这2个部分,最后简单加起来就行了。

    实现diffuse部分

    首先,diffuse部分可以将一些常数提取出来:

    +BIT祝威+悄悄在此留下版了个权的信息说:

     

     现在,积分内部的含义是,在半球范围内,将所有方向上的入射光向量分别与法线相乘,再加起来。这个积分在shader中当然要用离散的方式计算。半球嘛,立体的,所以分别在水平方向和竖直方向上进行累加比较方便。

     

     此时,我们就可以把上述方程稍微变形下:

     

     然后变为对应的离散的形式:

     

     从原来的积分形式变为离散形式,使用了蒙特卡罗积分原理。感兴趣的同学可以自行搜索研究一下。本文中,只要知道可以这么转换就行了。

    在shader中表示这个离散公式的代码如下:

     1 #version 330 core
     2 out vec4 FragColor;
     3 in vec3 WorldPos;
     4 
     5 uniform samplerCube environmentMap;
     6 
     7 const float PI = 3.14159265359;
     8 
     9 void main()
    10 {        
    11     vec3 N = normalize(WorldPos);
    12 
    13     vec3 irradiance = vec3(0.0);   
    14     
    15     // tangent space calculation from origin point
    16     vec3 up    = vec3(0.0, 1.0, 0.0);
    17     vec3 right = cross(up, N);
    18     up         = cross(N, right);
    19        
    20     float sampleDelta = 0.025;
    21     float nrSamples = 0.0f;
    22     for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
    23     {
    24         for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
    25         {
    26             // spherical to cartesian (in tangent space)
    27             vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
    28             // tangent space to world
    29             vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 
    30 
    31             irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
    32             nrSamples++;
    33         }
    34     }
    35     irradiance = PI * irradiance * (1.0 / float(nrSamples));
    36     
    37     FragColor = vec4(irradiance, 1.0);
    38 }

    代码中的双重for循环,就是在离散地计算积分值。最后得到的irradiance,再乘以Kd*c,就是diffuse部分的颜色值了。这个值加上接下来马上要讲解的specular部分的颜色值,就是应用题的答案。

    +BIT祝威+悄悄在此留下版了个权的信息说:

    所有Fragment Shader的计算结果都会保存到一个立方体贴图中。这个贴图叫做irradianceMap。这个计算过程叫做“卷积”。

    注意,计算diffuse部分的输入数据中,用到了一个立方体贴图samplerCube environmentMap,它其实就是物体所处于的环境,也叫天空盒。这里实际上就是将整个天空盒当作一个大光源来处理了。下图展示了将输入的立方体贴图(左侧)卷积后得到的irradianceMap(右侧):

     

     另外,这里将点p选在原点(0, 0, 0)上,稍后计算specular部分时也会这样设置。读者会问,那就只能描述在原点处的光照喽?也不尽然。只要在场景中的其他关键位置上也分别执行一遍PBR公式,就可以在整个场景中安排好这种“探针”。计算光照时,将距离物体最近的那几个探针的颜色加权平均一下,就可以得到需要的颜色了。本文不讨论“探针”的问题。

    实现specular部分

    现在,提取出specular部分:

     

     这个积分里有wi和wo两个变量,如果要离散地计算,就得对wi和wo的所有组合都算一遍。这是达不到实时要求的。Epic游戏公司给了一个近似公式,可以解决这个问题:

     

     左边的积分和上文的diffuse部分很相似,不同之处是,要对不同的粗糙度分别计算结果,并依次保存到一个立方体贴图的不同mipmap层上(越高的粗糙度保存在越高(分辨率小)的mipmap层上)。这个过程也是卷积,得到的贴图是个多mipmap层的立方体贴图,叫做prefilterMap。下图展示了一个被卷积好了的prefilterMap:

     

     右边的积分,以n与wi的乘积为参数1,以粗糙度为参数2,进行卷积,得到一个普通的二维纹理,叫做brdfLUT。下图就是:

     

    +BIT祝威+悄悄在此留下版了个权的信息说:

     分别从卷积贴图里采样,再算到公式里就得到specular部分的颜色了。

    贴图总结

    首先,我们需要从一个*.hdr文件加载二维纹理texHDR。

    然后,将texHDR转换为天空盒纹理sampleCube environmentMap。

    然后,用environmentMap分别生成irradianceMap和多mipmap层的prefilterMap。

    最后,brdfLUT是独立生成的,与别的贴图无关。

    只需加载其他的*.hdr文件,就可以将物体置于其他天空盒下。PBR将天空盒视作光源,照射物体。这就是PBR能让物体保持融入各个场景中原因。

    下图是我在CSharpGL中使用的newport_loft.hdr加载后的样子:

     

     这样的样式,在头顶和脚底方向上的数据损失会多一点。不过,一般用户关注的都是平视方向,所以没问题。

     总结

    PBR是对Blinn-Phong的一种极大的改进。它用几个贴图帮助求解积分,所以显得难以理解,难以实现。其实也就那么回事。

    更新取消

  • 相关阅读:
    Linux系统网络文件配置
    Linux系统修改日期时间
    8、mysql索引
    7、mysql正则表达式、事务、alter命令
    6、mysql数据操作
    5、mysql数据类型
    4、mysql数据库操作
    3、mysql管理
    2、mysql安装
    1、mysql教程
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/csharpgl-55-How-I-understand-PBR.html
Copyright © 2020-2023  润新知