Ragnarok Note

尝试在Android中实现PBR管线——基本原理以及直接光照明

从本个世代起,基于物理的渲染(Physically Based Rendering,简称PBR)基本上就成为了事实上当下3A大作的渲染标准,自从迪士尼在在SIGGRAPH 2012上提出的著名的“迪士尼原则的BRDF”以及基于金属工作流来实现的方案,由于高度的易用性,美术设计师可以根据现实物理的参数来构建表面材质,可以实现惊人的显示效果,目前已经被广泛的在业界中使用

这篇文章将会尝试在Android中基于Kotlin来实现PBR管线,在正式开始代码实现之前,我们先来讨论下PBR管线的基础理论。


Before PBR

首先我们来看看,在PBR出现之前,传统的Blinn-Phong光照是怎么做的,我们先来看看渲染方程:

$$L=Diffuse\ast N\cdot L+Specular+(N\cdot L)^{shiness}$$

我们可以看到,光照颜色的输出,主要有两部分构成,漫反射项以及高光项,在渲染方程中:

  • $Diffuse$是漫反射颜色

  • $Specular$是高光颜色,或者叫做镜面反射的颜色

  • $N\cdot L$ 是表面法向量跟光线方向向量的点乘

  • $N\cdot H$ 是表面法向量跟半程向量(光线方向向量跟视角向量的中间向量)的点乘

  • $shiness$是高光度的反光度参数,值越大,高光点越集中

这个是经典的Blinn-Phong光照模型的,除了这个基本公式以外,后续也有针对这个模型一些改进项,使其更加符合物理上的直觉效果

这个光照模型并不复杂,但终究并非基于真实的物理参数进行材质的构建,实际渲染的时候往往得不到令人信服的效果。


PBR基础理论

接下来,我们再来介绍PBR的基础理论,以下内容以及图片参考自:PBR理论

微平面模型

所有的PBR理论都基于一种叫做微平面模型的理论,这个理论认为在微观尺度上,所有物体的表面都可以使用所谓的微平面(Microfacets)的细小镜面来进行描述,根据表面粗糙度的不同,这些微平面的排列可以相当不一致:

因此,这些微平面的排列则极大的影响了光线的反射:

因此,粗糙度越高的表面,表面光线反射越分散,反之则越集中:

在实际的PBR实现中,粗糙度(Roughness)是用来调整PBR效果的一个重要参数之一

能量守恒

在PBR中,能量守恒定律认为,出射的光线能量,永远不能超过入射光线的能量。在微平面模型中,对于一束光进入到进入到物体表面之后,我们分成了几个部分:

  • 折射部分,指的是被吸收到物体表面的那部分光线能量,具体表现出来就是物体的漫反射(Diffuse)

  • 反射部分,指的是光线反射出来离开物体表面的那部分光照,具体指的就是镜面高光(Specular)

对于非金属物质来说,光线射进物体都会产生折射跟反射部分,而对于金属物质来说,微平面理论认为金属表面不会显示出漫反射部分,所有光线都会被处理成镜面高光

而能量守恒的要求,则是要求漫反射+高光部分占比加起来为1,实际实现中,我们往往先算出高光部分占比,然后在算出漫反射部分占比:

  float kS = calculateSpecularComponent(); // 反射/镜面 部分
  float kD = 1.0 - ks; // 折射/漫反射 部分

对比传统的Blinn-Phong模型,由于起没有考虑能量守恒,所以往往很难得出令人信服的效果

基于物理的BRDF

BRDF(Bidirectional Reflectance Distribution Function),即为双向反射分布函数,是整个PBR管线中最为重要的函数之一,可以用来求出每条光线对了一个给定材质属性的表面上,最终反射出来的光线的贡献程度。类比Blinn-Phong模型来说,我们拿漫反射项颜色乘以NdotL,其实也是一种BRDF计算方式,只是没有基于物理理论来建模,所以没有办法得出令人信服的效果。

类似Blinn-Phong模型,PBR的BRDF也是接受入射光方向Wi,反射光方向Wo,以及一个跟微平面理论相关的物理参数粗糙度(Roughness),实际实现中,最为常用的则是被称为Cook-Torrance的BRDF模型,同时兼具漫反射以及镜面反射两部分:

$$L=K_{d}f_{lambert} + K_{s}f_{cook-torrance}$$

  • 其中Kd表示漫反射光照的能量占比,Ks则表示镜面反射的能量占比,两项加起来为1,而对于漫反射中的Lambert项,我们经常用这个公式计算:

$$f_{lambert}=\frac{diffuse}{\pi}$$

我们将漫反射颜色除以PI,作为BRDF公式的漫反射部分。这部分并没有什么特别的,实际上在Blinn-Phong模型的改进版本中,也有将漫反射颜色除以PI的做法,用于得到更加真实的效果

  • 而BRDF公式的镜面高光部分则比较复杂:

$$f_{cook-torrance}=\frac{DFG}{4(W_{o}\cdot n)(W_{i}\cdot n)}$$

这里包含了三个主要的参数,D/F/G分别代表三种不同种类的函数,分别用来模拟反射的不同部分的特性,分母则为一个配平参数用来作为标准化因子。D/F/G三个函数则分别为:

  • 法线分布函数(Normal Distribution Function):用于估算在收到表面粗糙度的影响下,取向方向与半程向量一致的微平面向量
  • 几何函数(Geometry Function):用于描述微平面自成阴影的的函数,在表面粗糙度比较大的时候,平面上的微表面可能挡住了其他微表面的光线
  • 菲涅尔方程(Fresnel Rquation):用于描述光线在不同的入射角度下表面反射光线所占的比率

以上每一种函数都描述了对应不同的物理现象,而实际渲染中,我们都会采用某种近似的函数,接下来我们来说下这几种近似函数的公式:


BRDF近似项计算

这里BRDF近似项的选择,同样参照自:PBR理论

法线分布函数(Normal Distribution Function)的近似

法线分布函数从统计学的角度,描述了微平面模型对半程向量H的扰动程度,如果说给定H向量,NDF返回0.35,我们就认为整个平面中有35%的H向量与给定的向量一致,NDF的返回受表面粗糙度的影响,不同NDF的返回对镜面高光的区域影响很大:

可以看到到NDF返回值越小,镜面高光区域越小

实际渲染中,我们所使用的NDF方程为Trowbridge-Reitz GGX分布:

$$NDF(n,h,a)=\frac{a^{2}}{\pi((n\cdot h)^{2}(a^{2}-1)+1)^{2}}$$

这里参数a取为粗糙度的平方

使用glsl的实现则如下:

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 saturateMediump(nom / denom);
}


几何函数(Geometry Function)的近似

几何函数描述了微平面中互相被遮蔽的比率,跟法线分布函数类似,都是从统计学的角度描述了微平面的属性:

这里我们使用的几何函数为Schlick-GGX近似:

$$G(n,v,k)=\frac{n\cdot v}{(n\cdot v)(1-k)+k)}$$

其中参数k在计算直接光的时候为:

$$k=\frac{(a+1)^{2}}{8}$$

这里参数a同样也是取为粗糙度的平方

而在实际渲染中,我们还将需要光线的方向向量来将两者纳入其中:

$$G_{smith}=G(n,v,k)G(n,l,k)$$

通过以上公式,在不同的粗糙度下可以得到如下的效果:

以上方程使用glsl的实现如下:

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 saturateMediump(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 saturateMediump(ggx1 * ggx2);
}


菲涅尔方程(Fresnel Rquation)的近似

菲涅尔方程描述了光线被反射部分的比率,因而会受到观察方向的影响,结合能量守恒,我们可以得出剩下的漫反射的比率。而要计算出菲涅尔方程,我们需要一个基础反射率(F0)的参数,描述的是在表面的掠射角方向望过去(此时表面法线跟视线方向成90度),不同材料的表面反射率都不太一样:

我们这里取0.04作为近似的基础反射率,另外,这里我们还需要额外引入一个叫做金属度(Metallic)的参数,结合F0,一般我们这样子来计算出材质的真实F0:

vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);

有了F0之后,我们使用Fresnel-Schlick近似来计算菲涅尔方程的近似:

$$F(h,v,F_{0})=F_{0}+(1-F_{0})(1-h\cdot v)^{5}$$

在glsl中的实现则为:

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

其中cosTheta则为HdotV

终于,我们计算出了BRDF公式中的所有近似项,并且,我们有了两项粗糙度(Roughness)以及金属度(Metallic)两项额外的参数可以用于调整渲染效果,这两个参数的取值范围均为[0, 1]


直接光照明

接下来,在得到了BRDF的真实公式之后,我们尝试基于公式直接计算直接光的照明,我们会在场景中添加若干个点光源,以及一个方向光源,首先我们定义光源的一些基本变量:

#define POINT_LIGHT_NUMBER ${PointLightPositions.size}
// lights
uniform vec3 pointLightPositions[POINT_LIGHT_NUMBER];
uniform vec3 pointLightColors[POINT_LIGHT_NUMBER];

uniform vec3 directionLightDir;
uniform vec3 directionLightColor;

我们将点光源的位置以及颜色分别存储在数组中,而对于方向光源,则分别需要声明起方向以及颜色,而实际在光照计算,我们需要对遍历每一个光源,分别使用BRDF函数计算其最终反射的颜色并相加起来

我们首先来看下点光源的计算,由于点光源强度会随着距离而进行衰减,这里我们使用这里教程介绍到的平方衰减函数,单个点光源的完整计算逻辑如下

// calculate per-light radiance
vec3 L = normalize(pointLightPositions[i] - WorldPos);
vec3 H = normalize(V + L);
float distance = length(pointLightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = pointLightColors[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 nominator    = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
vec3 specular = nominator / max(denominator, 0.001); // prevent divide by zero for NdotV=0.0 or NdotL=0.0
// 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
// note that we already multiplied the BRDF by the Fresnel (kS so we won't multiply by kS again
Lo += (kD * albedo / PI + specular) * radiance * NdotL;

可以看出来实现基本上就是直接对着公式翻译了一遍,注意在计算kD的时候,我们最后还乘以了(1.0-metallic),这说明在表面材质完全是金属的时候,将不会有漫反射

对于方向光源,我们也是类似这样子计算:

vec3 L = normalize(-directionLightDir);
vec3 H = normalize(V + L);
vec3 radiance = directionLightColor;
// 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 nominator    = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
vec3 specular = nominator / max(denominator, 0.001); // prevent divide by zero for NdotV=0.0 or NdotL=0.0
// 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
// note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again
Lo += (kD * albedo + specular) * radiance * NdotL;

注意跟点光源计算不同,我们在计算的时候并没有根据距离来衰减光照能量

最后,在计算出光照颜色之后,由于我们整个计算过程都是在线性空间中计算,并且没有对输出颜色归一,所以最后我们还需要进行Gamma校正以及色调映射(Tone Mapping):

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

下图便是在Android中,在不同的粗糙度下的渲染结果:

本文章具体实现代码在:AndroidPBR,渲染的界面提供了两个slidebar用于调整金属度以及粗糙度参数

PBR管线除了直接光照明部分以外,还有环境光照明部分,这部分是PBR对比与上一世代光照模型的一个很重要的区别,所谓全局光照的实体,我将会在下一篇文章尝试介绍这一部分的内容

References