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

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

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

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

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的公式。不过那玩意计算起来比较费时,业界一般用它的一个近似版本:

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

当然,其他版本的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是将整个图片作为光源时应采用的公式。α表示表面粗糙度)

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

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

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

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

法线分布函数D

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

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

(α表示表面粗糙度)

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

当然,其他版本的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中表示这个离散公式的代码如下:

 #version  core
out vec4 FragColor;
in vec3 WorldPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main()
{
vec3 N = normalize(WorldPos); vec3 irradiance = vec3(0.0); // tangent space calculation from origin point
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, N);
up = cross(N, right); float sampleDelta = 0.025;
float nrSamples = 0.0f;
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
// spherical to cartesian (in tangent space)
vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
// tangent space to world
vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
nrSamples++;
}
}
irradiance = PI * irradiance * (1.0 / float(nrSamples)); FragColor = vec4(irradiance, 1.0);
}

代码中的双重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. Redis基础类型常用操作命令

    Redis基础类型常用操作命令 概念:Redis是用C语言开发的一个开源的高性能键值对数据库. 特征: 数据间没有必然的联系 内部采用单线程机制进行工作 高性能 多数据类型支持 字符串类型 Strin ...

  2. 如何在Python中调用打包好的Jar文件?

    首先是在anaconda中进入我这个项目对应的一个环境,然后在这个环境中下载并且安装jpype.那么就可以直接import了.但是这里出现了一系列的问题 第一个问题,getDefaultJVM()报错 ...

  3. VS 2017 中取消自动补全花括号

    输入 "{", VS 会很智能的给你补全,得到 “{}”, 如果不想享受这个服务,可以按以下设置取消: Tools -> Options -> Text Editor ...

  4. 菜鸟刷面试题(二、RabbitMQ篇)

    目录: rabbitmq 的使用场景有哪些? rabbitmq 有哪些重要的角色? rabbitmq 有哪些重要的组件? rabbitmq 中 vhost 的作用是什么? rabbitmq 的消息是怎 ...

  5. post请求四种传送正文的方式

    一.简介 HTTP协议规定post提交的数据必须放在消息主体(entity-body)中,但协议没有规定数据必须使用什么编码方式.HTTP协议是以ASCII码传输,建立再TCP/IP协议之上的应用层规 ...

  6. ArrayList和LinkedList介绍

    java.util.ArrayList集合的数据存储结构是数组,且是多线程,元素增删慢,查找快, 由于日常使用开发大多数为查询数据,遍历数据,所以ArrayList是最常用的集合.上一节已写了. ja ...

  7. AES 对称加密

    package com.skynet.rimp.common.utils.string; import java.io.UnsupportedEncodingException; import jav ...

  8. Hyperledger Fabric相关文件解析

    1相关文件说明 这一部分涉及相关配置文件的解析, 网络的启动涉及到多个文件,本文按以下顺序进行分析: . ├── base │   ├── docker-compose-base.yaml #1 │  ...

  9. Java题库——Chapter3 操作符、选择

    1)The "less than or equal to" comparison operator in Java is ________. A)<< B) != C) ...

  10. RabbitMQ的高级特性概念理解

    1.RabbitMQ中的消息如何保障百分之百的投递成功? 答:百分之百的投递成功,方案可以参考下面的2.3. 2.什么是生产者端的可靠性投递? 答:第一步,生产者保障消息的成功发出.第二步,保障Rab ...