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共同决定:

  1. float diffuse = dot(N, Li);

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

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

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

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

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

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

  1. 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)<= ,还有别的优点:

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

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

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

PBR模型

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

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

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

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

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

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

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

PBR认为 (diffuse+specular)== 始终成立。那么,先算出其中一个,自然就得知另一个了(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(, dot(v, normalize(v + Li))) 。可见“它与入射光Li和观察者v的方向有关”,此言不虚。

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

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

有了specular,当然就有了 vec3 diffuse = vec3(, , ) - 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 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的一种极大的改进。它用几个贴图帮助求解积分,所以显得难以理解,难以实现。其实也就那么回事。

更新取消

CSharpGL(55)我是这样理解PBR的的更多相关文章

  1. 我是如何理解并使用maven的

    前言 一直想写一篇关于Maven的文章,但是不知如何下笔,如果说能使用,会使用Maven的话,一.两个小时足矣,不需要搞懂各种概念.那么给大家来分享下我是如何理解并使用maven的. 什么是Maven ...

  2. vue是一个渐进式的框架,我是这么理解的

    vue是一个渐进式的框架,我是这么理解的 原文地址 时间:2017-10-26 10:37来源:未知 作者:admin 每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主 ...

  3. 我是这样理解EventLoop的

    我是这样理解EventLoop的 一.前言   众所周知,在使用javascript时,经常需要考虑程序中存在异步的情况,如果对异步考虑不周,很容易在开发中出现技术错误和业务错误.作为一名合格的jav ...

  4. 我是如何理解Java抽象类和接口的

    在面试中我们经常被问到:Java中抽象类和接口的区别是什么? 然后,我们就大说一通抽象类可以有方法,接口不能有实际的方法啦:一个类只能继承一个抽象类,却可以继承多个接口啦,balabala一大堆,就好 ...

  5. 我是如何理解ThreadLocal

    ThreadLocal的概念 ThreadLocal从英文的角度看,可以看成thread和local的组合,就是线程本地的意思,我们都知道,看过jvm内存分配的人都知道在jvm虚拟机中对每一个线程都分 ...

  6. 我是这样理解--SVM,不需要繁杂公式的那种!(附代码)

    1. 讲讲SVM 1.1 一个关于SVM的童话故事 支持向量机(Support Vector Machine,SVM)是众多监督学习方法中十分出色的一种,几乎所有讲述经典机器学习方法的教材都会介绍.关 ...

  7. 我是如何理解Android的Handler模型_3

    AsyncTask则相当于现代化的电话系统,接线员的功能被完全封装了. 对于上例,新建更新TextView的类并继承AsyncTack类,如下: class UpdataTV extends Asyn ...

  8. 我是如何理解Android的Handler模型_2

    对比例程说明,如: 例:在新新线程中替换TextView显示内容. 界面如下,单击按键后original data 替换为 changed data Handler Message部分实现步骤: 1. ...

  9. 我是这样理解HTTP和HTTPS区别的

    为何要用https? http协议的缺点 通信使用明文,内容可能被窃听(重要密码泄露) 不验证通信方身份,有可能遭遇伪装(跨站点请求伪造) 无法证明报文的完整性,有可能已遭篡改(运营商劫持) 用htt ...

随机推荐

  1. Vue 中的keep-alive 什么用处?

    keep-alive keep-alive是Vue提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在v页面渲染完毕后不会被渲染成一个DOM元素 <keep-aliv ...

  2. arcgis api 4.x for js 自定义叠加图片图层实现地图叠加图片展示(附源码下载)

    前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 4.x for js:esri 官网 api,里面详细的介绍 arcgis api 4.x 各个类 ...

  3. Oracle 定时备份数据库

    [操作说明] 在前面的博客中,学习了如何Oracle如何备份数据库,实际开发过程中数据库应该每隔一段时间就要备份一次,所以我们就需要一个定时执行这个代码的功能,同时备份的文件可能进行一些处理,比如压缩 ...

  4. 一文解读JSON (转)

    JSON作为目前Web主流的数据交换格式,是每个IT技术人员都必须要了解的一种数据交换格式.尤其是在Ajax和REST技术的大行其道的当今,JSON无疑成为了数据交换格式的首选! 今天我们一起来学习一 ...

  5. MYSQL5.7 INDEXES之如何使用索引(一)

    Most MySQL indexes (PRIMARY KEY, UNIQUE, INDEX, and FULLTEXT) are stored in B-trees. Exceptions: Ind ...

  6. 如何用Jpype创建HashMap和ArrayList

    近期在Python中使用java语言的时候有涉及到如何创建HashMap和ArrayList等容器,最开始的疑惑是,java里面的容器是有泛型做类型检测的,而在python中却没有泛型这个说法,那么如 ...

  7. Cocos2d-x.3.0开发环境搭建之—— 极简式环境搭建

    配置:win7 + VS2012 + Cocos2d-x.3.0 + Cocos Studio v1.4.0.1 使用此法可以方便的创建Cocos2d-x项目.如果需要运行Cocos2d-x引擎自带的 ...

  8. 一、I/O模型之BIO

    I/O模型之BIO 基本介绍 Java BIO 就是传统的 Java IO 编程,其相关的类和接口再 java.io 包下 BIO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线 ...

  9. 【Hash一致性算法】什么是Hash一致性算法

    目录 1. 一致性Hash算法简介 环形Hash空间 把数据通过一定的hash算法处理后映射到环上 将机器通过hash算法映射到环上 机器的删除与添加 平衡性 本文转载自博客 1. 一致性Hash算法 ...

  10. RK3399安装Qt

    更新软件源.升级软件 sudo apt-get update sudo apt-get upgrade 安装Qt sudo apt-get install qt5-default sudo apt-g ...