Vulkan_PBR—基于物理的渲染基础

本文主要介绍shader部分,至于main代码,如果你看过之前章节,应该可轻松实现,基础理论主要参照 Learn OpenGL–PBR,本部分做简述及BRDF实现

1.理论

PBR,或者用更通俗一些的称呼是指基于物理的渲染(Physically Based Rendering),它指的是一些在不同程度上都基于与现实世界的物理原理更相符的基本理论所构成的渲染技术的集合。正因为基于物理的渲染目的便是为了使用一种更符合物理学规律的方式来模拟光线,因此这种渲染方式与我们原来的Phong或者Blinn-Phong光照算法相比总体上看起来要更真实一些。除了看起来更好些以外,由于它与物理性质非常接近,因此我们(尤其是美术师们)可以直接以物理参数为依据来编写表面材质,而不必依靠粗劣的修改与调整来让光照效果看上去正常。使用基于物理参数的方法来编写材质还有一个更大的好处,就是不论光照条件如何,这些材质看上去都会是正确的,而在非PBR的渲染管线当中有些东西就不会那么真实了。

2.微平面模型

所有的PBR技术都基于微平面理论。这项理论认为,达到微观尺度之后任何平面都可以用被称为微平面(Microfacets)的细小镜面来进行描绘。根据平面粗糙程度的不同,这些细小镜面的取向排列可以相当不一致:
在这里插入图片描述
产生的效果就是:一个平面越是粗糙,这个平面上的微平面的排列就越混乱。这些微小镜面这样无序取向排列的影响就是,当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散(Scatter)开来,进而产生出分布范围更广泛的镜面反射。而与之相反的是,对于一个光滑的平面,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射:
在这里插入图片描述
在微观尺度下,没有任何平面是完全光滑的。然而由于这些微平面已经微小到无法逐像素的继续对其进行区分,因此我们只有假设一个粗糙度(Roughness)参数,然后用统计学的方法来概略的估算微平面的粗糙程度。我们可以基于一个平面的粗糙度来计算出某个向量的方向与微平面平均取向方向一致的概率。这个向量便是位于光线向量l和视线向量v之间的中间向量(Halfway Vector)。它的计算方法如下:
在这里插入图片描述
微平面的取向方向与中间向量的方向越是一致,镜面反射的效果就越是强烈越是锐利。然后再加上一个介于0到1之间的粗糙度参数(roughness),这样我们就能概略的估算微平面的取向情况了:

			uint32_t objcount = 5;
			for (uint32_t x = 0; x < objcount; x++) {
				glm::vec3 pos = glm::vec3(float(x - (objcount / 2.0f)) * 2.5f, 0.0f, 0.0f);
				mat.params.roughness = glm::clamp((float)x / (float)objcount, 0.1f, 1.0f);
				vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::vec3), &pos);
				vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(glm::vec3), sizeof(Material::PushBlock), &mat);
				vkCmdDrawIndexed(drawCmdBuffers[i], models.objects[models.objectIndex].indexCount, 1, 0, 0, 0);
			}

在这里插入图片描述

我们可以看到,较高的粗糙度值显示出来的镜面反射的轮廓要更大一些。与之相反地,较小的粗糙值显示出的镜面反射轮廓则更小更锐利。

首先我们来看一下顶点着色器,以便后续主要介绍的片元着色器可以顺利进行:

#version 450

layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inNormal;

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 model;
	mat4 view;
	vec3 camPos;
} ubo;

layout (location = 0) out vec3 outWorldPos;
layout (location = 1) out vec3 outNormal;

layout(push_constant) uniform PushConsts {
	vec3 objPos;
} pushConsts;

out gl_PerVertex 
{
	vec4 gl_Position;
};

void main() 
{
	vec3 locPos = vec3(ubo.model * vec4(inPos, 1.0));
	outWorldPos = locPos + pushConsts.objPos;
	outNormal = mat3(ubo.model) * inNormal;
	gl_Position =  ubo.projection * ubo.view * vec4(outWorldPos, 1.0);
}

在上述顶点着色器中采用推入式常量来定义了不同位置,并以此来产生多个小球,并在不用小球上产生不同效果。

3.能量守恒

微平面近似法使用了这样一种形式的能量守恒(Energy Conservation):出射光线的能量永远不能超过入射光线的能量(发光面除外)。如图示我们可以看到,随着粗糙度的上升镜面反射区域的会增加,但是镜面反射的亮度却会下降。如果不管反射轮廓的大小而让每个像素的镜面反射强度(Specular Intensity)都一样的话,那么粗糙的平面就会放射出过多的能量,而这样就违背了能量守恒定律。这也就是为什么正如我们看到的一样,光滑平面的镜面反射更强烈而粗糙平面的反射更昏暗。

为了遵守能量守恒定律,我们需要对漫反射光和镜面反射光之间做出明确的区分。当一束光线碰撞到一个表面的时候,它就会分离成一个折射部分和一个反射部分。反射部分就是会直接反射开来而不会进入平面的那部分光线,这就是我们所说的镜面光照。而折射部分就是余下的会进入表面并被吸收的那部分光线,这也就是我们所说的漫反射光照。

这里还有一些细节需要处理,因为当光线接触到一个表面的时候折射光是不会立即就被吸收的。通过物理学我们可以得知,光线实际上可以被认为是一束没有耗尽就不停向前运动的能量,而光束是通过碰撞的方式来消耗能量。每一种材料都是由无数微小的粒子所组成,这些粒子都能如下图所示一样与光线发生碰撞。这些粒子在每次的碰撞中都可以吸收光线所携带的一部分或者是全部的能量而后转变成为热量。
在这里插入图片描述

一般来说,并非所有能量都会被全部吸收,而光线也会继续沿着(基本上)随机的方向发散,然后再和其他的粒子碰撞直至能量完全耗尽或者再次离开这个表面。而光线脱离物体表面后将会协同构成该表面的(漫反射)颜色。不过在基于物理的渲染之中我们进行了简化,假设对平面上的每一点所有的折射光都会被完全吸收而不会散开。而有一些被称为次表面散射(Subsurface Scattering)技术的着色器技术将这个问题考虑了进去,它们显著的提升了一些诸如皮肤,大理石或者蜡质这样材质的视觉效果,不过伴随而来的则是性能下降代价。

对于金属(Metallic)表面,当讨论到反射与折射的时候还有一个细节需要注意。金属表面对光的反应与非金属材料(也被称为介电质(Dielectrics)材料)表面相比是不同的。它们遵从的反射与折射原理是相同的,但是所有的折射光都会被直接吸收而不会散开,只留下反射光或者说镜面反射光。亦即是说,金属表面不会显示出漫反射颜色。由于金属与电介质之间存在这样明显的区别,因此它们两者在PBR渲染管线中被区别处理,而我们将在文章的后面进一步详细探讨这个问题。

反射光与折射光之间的这个区别使我们得到了另一条关于能量守恒的经验结论:反射光与折射光它们二者之间是互斥的关系。无论何种光线,其被材质表面所反射的能量将无法再被材质吸收。因此,诸如折射光这样的余下的进入表面之中的能量正好就是我们计算完反射之后余下的能量。

我们按照能量守恒的关系,首先计算镜面反射部分,它的值等于入射光线被反射的能量所占的百分比。然后折射光部分就可以直接由镜面反射部分计算得出:

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

这样我们就能在遵守能量守恒定律的前提下知道入射光线的反射部分与折射部分所占的总量了。按照这种方法折射/漫反射与反射/镜面反射所占的份额都不会超过1.0,如此就能保证它们的能量总和永远不会超过入射光线的能量。而这些都是我们在前面的光照教程中没有考虑的问题。

4.BRDF

在这里插入图片描述
BRDF,或者说双向反射分布函数,它接受入射(光)方向ωi,出射(观察)方向ωo,平面法线n以及一个用来表示微平面粗糙程度的参数a作为函数的输入参数。BRDF可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的贡献程度。举例来说,如果一个平面拥有完全光滑的表面(比如镜面),那么对于所有的入射光线ωi(除了一束以外)而言BRDF函数都会返回0.0 ,只有一束与出射光线ωo拥有相同(被反射)角度的光线会得到1.0这个返回值。

BRDF基于我们之前所探讨过的微平面理论来近似的求得材质的反射与折射属性。对于一个BRDF,为了实现物理学上的可信度,它必须遵守能量守恒定律,也就是说反射光线的总和永远不能超过入射光线的总量。严格上来说,同样采用ωi和ωo作为输入参数的 Blinn-Phong光照模型也被认为是一个BRDF。然而由于Blinn-Phong模型并没有遵循能量守恒定律,因此它不被认为是基于物理的渲染。现在已经有很好几种BRDF都能近似的得出物体表面对于光的反应,但是几乎所有实时渲染管线使用的都是一种被称为Cook-Torrance BRDF模型。

Cook-Torrance BRDF兼有漫反射和镜面反射两个部分:
在这里插入图片描述
这里的kd是早先提到过的入射光线中被折射部分的能量所占的比率,而ks是被反射部分的比率。BRDF的左侧表示的是漫反射部分,这里用flambert来表示。它被称为Lambertian漫反射,这和我们之前在漫反射着色中使用的常数因子类似,用如下的公式来表示:
在这里插入图片描述
表示表面颜色(回想一下漫反射表面纹理)。除以π是为了对漫反射光进行标准化,因为前面含有BRDF的积分方程是受π影响的。

  • 这个Lambertian漫反射和我们之前经常使用的漫反射到底有什么关系:之前我们是用表面法向量与光照方向向量进行点乘,然后再将结果与平面颜色相乘得到漫反射参数。点乘依然还在,但是却不在BRDF之内,而是转变成为了Lo积分末公式末尾处的n⋅ωi 。
    目前存在着许多不同类型的模型来实现BRDF的漫反射部分,大多看上去都相当真实,但是相应的运算开销也非常的昂贵。不过按照Epic公司给出的结论,Lambertian漫反射模型已经足够应付大多数实时渲染的用途了。

BRDF的镜面反射部分要稍微更高级一些,它的形式如下所示:
在这里插入图片描述

Cook-Torrance BRDF的镜面反射部分包含三个函数,此外分母部分还有一个标准化因子 。字母D,F与G分别代表着一种类型的函数,各个函数分别用来近似的计算出表面反射特性的一个特定部分。三个函数分别为正态分布函数(Normal Distribution Function)菲涅尔方程(Fresnel Rquation)几何函数(Geometry Function)

  • 正态分布函数:估算在受到表面粗糙度的影响下,取向方向与中间向量一致的微平面的数量。这是用来估算微平面的主要函数。
  • 几何函数:描述了微平面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。
  • 菲涅尔方程:菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率。

以上的每一种函数都是用来估算相应的物理参数的,而且你会发现用来实现相应物理机制的每种函数都有不止一种形式。它们有的非常真实,有的则性能高效。你可以按照自己的需求任意选择自己想要的函数的实现方法。

4.1 正态分布函数

正态分布函数D,或者说镜面分布,从统计学上近似的表示了与某些(中间)向量h取向一致的微平面的比率。举例来说,假设给定向量h,如果我们的微平面中有35%与向量h取向一致,则正态分布函数或者说NDF将会返回0.35。目前有很多种NDF都可以从统计学上来估算微平面的总体取向度,只要给定一些粗糙度的参数以及一个我们马上将会要用到的参数Trowbridge-Reitz GGX:
在这里插入图片描述
在这里h表示用来与平面上微平面做比较用的中间向量,而a表示表面粗糙度。
如果我们把h当成是不同粗糙度参数下,平面法向量和光线方向向量之间的中间向量的话,我们可以得到如下图示的效果:

当粗糙度很低(也就是说表面很光滑)的时候,与中间向量取向一致的微平面会高度集中在一个很小的半径范围内。由于这种集中性,NDF最终会生成一个非常明亮的斑点。但是当表面比较粗糙的时候,微平面的取向方向会更加的随机。你将会发现与h向量取向一致的微平面分布在一个大得多的半径范围内,但是同时较低的集中性也会让我们的最终效果显得更加灰暗。
在这里插入图片描述
使用GLSL代码编写的Trowbridge-Reitz GGX正态分布函数是下面这个样子的:

// 正态分布函数
float D_GGX(float dotNH, float roughness)
{
	float alpha = roughness * roughness;
	float alpha2 = alpha * alpha;
	float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
	return (alpha2)/(PI * denom*denom); 
}

4.2 几何函数

几何函数从统计学上近似的求得了微平面间相互遮蔽的比率,这种相互遮蔽会损耗光线的能量。
在这里插入图片描述
与NDF类似,几何函数采用一个材料的粗糙度参数作为输入参数,粗糙度较高的表面其微平面间相互遮蔽的概率就越高。我们将要使用的几何函数是GGX与Schlick-Beckmann近似的结合体,因此又称为Schlick-GGX:
在这里插入图片描述
使用GLSL编写的几何函数代码如下:

// 几何函数
float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{
	float r = (roughness + 1.0);
	float k = (r*r) / 8.0;
	float GL = dotNL / (dotNL * (1.0 - k) + k);
	float GV = dotNV / (dotNV * (1.0 - k) + k);
	return GL * GV;
}

4.3 菲涅尔方程

菲涅尔方程描述的是被反射的光线对比光线被折射的部分所占的比率,这个比率会随着我们观察的角度不同而不同。当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度告诉我们被反射的光线所占的百分比。利用这个反射比率和能量守恒原则,我们可以直接得出光线被折射的部分以及光线剩余的能量。

当垂直观察的时候,任何物体或者材质表面都有一个基础反射率(Base Reflectivity),但是如果以一定的角度往平面上看的时候所有反光都会变得明显起来。你可以自己尝试一下,用垂直的视角观察你自己的木制/金属桌面,此时一定只有最基本的反射性。但是如果你从近乎90度(译注:应该是指和法线的夹角)的角度观察的话反光就会变得明显的多。如果从理想的90度视角观察,所有的平面理论上来说都能完全的反射光线。这种现象因菲涅尔而闻名,并体现在了菲涅尔方程之中。

菲涅尔方程是一个相当复杂的方程式,不过幸运的是菲涅尔方程可以用Fresnel-Schlick近似法求得近似解:
在这里插入图片描述
F0表示平面的基础反射率,它是利用所谓折射指数(Indices of Refraction)或者说IOR计算得出的。然后正如你可以从球体表面看到的那样,我们越是朝球面掠角的方向上看(此时视线和表面法线的夹角接近90度)菲涅尔现象就越明显,反光就越强:
在这里插入图片描述

使用GLSL编写的菲涅尔方程代码如下:

// 菲涅尔方程
vec3 F_Schlick(float cosTheta, float metallic)
{
	vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
	vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); 
	return F;    
}

使用GLSL编写的双向反射分布函数(BRDF)代码如下:

// 双向反射分布函数,它的作用是基于表面材质属性来对入射辐射率进行缩放或者加权
vec3 BRDF(vec3 L, vec3 V, vec3 N, float metallic, float roughness)
{
	// 预先计算向量和点积	
	vec3 H = normalize (V + L);
	float dotNV = clamp(dot(N, V), 0.0, 1.0);
	float dotNL = clamp(dot(N, L), 0.0, 1.0);
	float dotLH = clamp(dot(L, H), 0.0, 1.0);
	float dotNH = clamp(dot(N, H), 0.0, 1.0);

	// 白色光色
	vec3 lightColor = vec3(1.0);

	vec3 color = vec3(0.0);

	if (dotNL > 0.0)
	{
		float rroughness = max(0.05, roughness);
		// D = 正态分布函数 (微观层面的分布)
		float D = D_GGX(dotNH, roughness); 
		// G = 几何函数 (Microfacets shadowing)
		float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);
		// F = 菲涅尔方程 (反射率取决于入射角)
		vec3 F = F_Schlick(dotNV, metallic);

		vec3 spec = D * F * G / (4.0 * dotNL * dotNV);

		color += spec * dotNL * lightColor;
	}

	return color;
}

整体片元着色器如下:

#version 450

layout (location = 0) in vec3 inWorldPos;
layout (location = 1) in vec3 inNormal;

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 model;
	mat4 view;
	vec3 camPos;
} ubo;

layout (binding = 1) uniform UBOShared {
	vec4 lights[4];
} uboParams;

layout (location = 0) out vec4 outColor;

layout(push_constant) uniform PushConsts {
	layout(offset = 12) float roughness;
	layout(offset = 16) float metallic;
	layout(offset = 20) float r;
	layout(offset = 24) float g;
	layout(offset = 28) float b;
} material;

const float PI = 3.14159265359;

//#define ROUGHNESS_PATTERN 1

vec3 materialcolor()
{
	return vec3(material.r, material.g, material.b);
}

// 正态分布函数
float D_GGX(float dotNH, float roughness)
{
	float alpha = roughness * roughness;
	float alpha2 = alpha * alpha;
	float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
	return (alpha2)/(PI * denom*denom); 
}

// 几何函数
float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{
	float r = (roughness + 1.0);
	float k = (r*r) / 8.0;
	float GL = dotNL / (dotNL * (1.0 - k) + k);
	float GV = dotNV / (dotNV * (1.0 - k) + k);
	return GL * GV;
}

// 菲涅尔方程
vec3 F_Schlick(float cosTheta, float metallic)
{
	vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
	vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); 
	return F;    
}

// 双向反射分布函数,它的作用是基于表面材质属性来对入射辐射率进行缩放或者加权
vec3 BRDF(vec3 L, vec3 V, vec3 N, float metallic, float roughness)
{
	// 预先计算向量和点积	
	vec3 H = normalize (V + L);
	float dotNV = clamp(dot(N, V), 0.0, 1.0);
	float dotNL = clamp(dot(N, L), 0.0, 1.0);
	float dotLH = clamp(dot(L, H), 0.0, 1.0);
	float dotNH = clamp(dot(N, H), 0.0, 1.0);

	// 白色光色
	vec3 lightColor = vec3(1.0);

	vec3 color = vec3(0.0);

	if (dotNL > 0.0)
	{
		float rroughness = max(0.05, roughness);
		// D = 正态分布函数 (微观层面的分布)
		float D = D_GGX(dotNH, roughness); 
		// G = 几何函数 (Microfacets shadowing)
		float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);
		// F = 菲涅尔方程 (反射率取决于入射角)
		vec3 F = F_Schlick(dotNV, metallic);

		vec3 spec = D * F * G / (4.0 * dotNL * dotNV);

		color += spec * dotNL * lightColor;
	}

	return color;
}

void main()
{		  
	vec3 N = normalize(inNormal);
	vec3 V = normalize(ubo.camPos - inWorldPos);

	float roughness = material.roughness;

	// 基于顶点位置将条纹图案添加到粗糙度中(效果对比用)
#ifdef ROUGHNESS_PATTERN
	roughness = max(roughness, step(fract(inWorldPos.y * 2.02), 0.5));
#endif

	// 镜面的贡献
	vec3 Lo = vec3(0.0);
	for (int i = 0; i < uboParams.lights.length(); i++) {
		vec3 L = normalize(uboParams.lights[i].xyz - inWorldPos);
		Lo += BRDF(L, V, N, material.metallic, roughness);
	};

	// 考虑环境光
	vec3 color = materialcolor() * 0.02;
	color += Lo;

	// Gamma矫正
	color = pow(color, vec3(0.4545));

	outColor = vec4(color, 1.0);
}

编译shader,运行,可看到如下效果:
在这里插入图片描述
,这样看来效果不是太明显,因此可以将片元着色器中开启基于顶点位置将条纹图案添加到粗糙度:

define ROUGHNESS_PATTERN 1

编译shader后运行,调整视角,可见:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章