OpenGL-06 PBR

一、理论

1. 反射率方程

\[L_o(p,\omega_o)=\int_\Omega f_r(p,\omega_i,\omega_o)L_i(p,\omega_i)n\cdot \omega_i d\omega_i \]

反射率方程应该这样理解:

  • 辐射radiance某个着色点或者光源位置附近单位面积(法向)从某个方向的单位立体角接受或者发射的功率。可以看出,radiance约定了方向和源(或者说目标),实际上我们是使用辐射radiance来作为描述光的功率传播的概念,通常的,我们把它绑定到光线上作为光线传播的属性。我们的一切计算都是基础radiance的。

\[L=\frac{d^2\Phi}{d\omega dAcos\theta} \]

  • 辐照度irradiance是指空间中在单位面积(无法向要求)上的功率。

\[E=\frac{d\Phi}{dA} \]

  • 实际上,我们在研究辐照度时并没有在定义中强调他的方向性,只是在说明了他的空间分布特征,然而研究方向性是有必要的,这在下面会体现出来。显然的是,辐射radiance便是存在着方向性的辐照度irradiance,由前面的定义式可得关系如下:

\[dE(\omega)=L(\omega)cos\theta d\omega \]

  • 在将功率的空间分布概念,即辐照度irradiance,拆分成了不同方向的辐射radiance之后,这就方便我们将在空间中分布的功率概念使用提供功率的源来解释,这样一来我们就可以为空间中分布的功率以他们的来源方向不同赋予不同而属性,事实上这是有价值的,因为在光的可观性方面,出射角和入射角是有很重要影响的因素。
  • 我们应当这样来看待着色点:着色点会从空间中所有可观的光源那里(一般是半球空间)获得一个辐照度irradiance属性,进一步的正如前面所述,这个辐照度是由半球面内所有方向的radiance贡献的综合,在这之后着色点会将其辐照度向着所有的可发射方向(一般是半球空间)发射能量,每一个方向都会分配到一个radiance分量。然而,吸收和反射这两个过程并不是解耦的,即来自所有方向的radiance并不是首先综合成一个irradiance属性然后再去分配的。这是因为反射面材质的问题,反射光线与入射光线的方向存在关系,尤其是对于镜面反射或者有着glossy属性的镜面反射、以及折射而言,反射光线的方向相对于入射光线的方向是一个分布。因此,有必要探究来自不同方向的入射功率radiance,并考虑到他们的方向之后再去综合对某一个特殊方向的出射功率radiance的综合影响。
  • BRDF便是描述出射功率相对于入射功率的关系影响的因子,它是着色点某个出射方向\(\omega_o\)的radiance对来自某个入射方向\(\omega_i\)的irradiance的微元比值,即:

\[f_r(p,\omega_i,\omega_o)=\frac{dL_o(\omega_o)}{dE_i(\omega_i)} \]

  • 考虑到radiance与irradiance都是单位概念,取微元比值是指输出radiance相对于输入irradiance的微分结果,这样就方便得到输出radiance的积分公式:

\[L_o(\omega_o)=\int_\Omega f_r(p,\omega_i,\omega_o)dE_i(\omega_i) \]

  • 参考radiance与irradiance的关系,我们可以得到:

\[L_o(\omega_o)=\int_\Omega f_r(p,\omega_i,\omega_o)L(\omega)n\cdot \omega_i d\omega \]

  • 显然,这便是反射率方程。自然而然地,我们可以得出结论:反射率方程是用于描述某一个着色点p的向着某一个出射方向\(\omega_o\)的功率radiance,在考虑到出射光与入射光的角度关系对分布的影响后,对所有方向的入射功率radiance转化得到的辐照度irradiance取贡献因子BRDF之后,做积分的结果。

2.Cook-Torrance BRDF

\[f_r=k_df_{lambert}+k_sf_{cook-torrance} \]

我们的BRDF是分成两个部分的,应该这样理解:

  • 光照射到表面后,一部分会被直接反射出去,即镜面反射speculr部分,我们令这一部分占到的功率比重是\(k_s\)
  • 相应的,另一部分的比重就是\(k_f=1-k_s\)。另一部分是会被吸收到物体内部的光线,他们在物体内部不断弹射,一部分会被完全吸收转化为热能等,另一部会从入射点附近重新发射到外部,可以看到从一个着色点入射并吸收的这一部分光如果没有被完全吸收会从多个位置出射,这便是次表面反射。一般来说,我们并不关心次表面反射,直接使用漫反射来描述这个成分。
  • 使用两个因子,就可以保证反射的光功率符合能量守恒了。

2.1 Lambert漫反射

  • 前面提到,有\(k_d\)比重的入射光会发生吸收,最终的解决要么是完全吸收,要么是漫反射,那么我们如何描述这个过程呢?
  • 首先,我们使用材质表面的颜色属性来描述完全吸收与漫反射的比重,这个思路是很直观的,材质对不同波长的光有着不同的反照率,因而会呈现出不同的满反射颜色,当我们使用RGB编码颜色时,一个RGB三通道的三个反照率自然就是漫反射成分在吸收部分中的能量比重了。很熟悉的,我们把这一个属性叫做,颜色color或者反射率Albedo
  • 进一步的,我们假设漫反射光照对所有半球面方向的出射功率是各向一致性的,所以我们在BRDF中不强调光线的方向,相应的,所有出射方向的radiance是均分这一部分的漫反射功率的。考虑到半球面的面积是\(\pi\),这也就是平均的系数了。
  • 那么,结合这两个部分,被吸收的光功率即会发生漫反射的光功率,它的BRDF是:

\[f_{lambert}=\frac{Color}{\pi} \]

or

\[f_{lambert}=\frac{Albedo}{\pi} \]

2.2 Cook-Torrance镜面反射

  • 前面提到,会有\(k_s\)部分的光功率有发生镜面反射的可能。这一部分的光照,我们将会使用微表面模型来描述。
  • 我们在做渲染的时候,一般是使用三角形面作为基本图元,使用更细致的图元划分或者是贴图来实现凹凸效果。这些方法都不错,但是只是在模拟肉眼可观的凹凸现象时可行。实际上,我们在生活中观察到表面,即便是平滑到不必使用这些方法来实现凹凸效果(我们在之前将他们看作Glossy材质),也是存在着肉眼不可见的不光滑的,从物理的角度来看,所谓的镜面反射的Glossy现象就是由这引起的。考虑到这些微观的不光滑,然后来描述镜面反射的模型,便是微表面模型。
  • 针对平面的不光滑,微表面模型致力于描述2种由此引起的效果:
    • 表面不光滑即法线分布不均匀,那么就会存在Glossy现象,即反射光线不是理想镜面反射的集束光,而是光瓣。用于描述这一现象的是法线分布NDF
    • 表面不光滑就会存在自遮挡,同于描述这一现象的是几何项G
    • 在上面两个不光滑效果之外,微表面模型显式地给出了\(k_s\)比重,这一项是菲涅尔项F
    • 考虑到能量守恒,添加了标准化项。可以看到,这一项是与入射和出射方向相关的,因为对每一个出射方向的求解需要对所有入射方向做积分,而考虑到能量守恒有需要对所有出射方向做积分,所以这个项很直观。
    • 最后,为什么他们是乘在一起的呢?首先,一部分光照可能会发生镜面反射(菲涅尔项),然后这一部分功率有一部分会被遮挡而只有一部分会成功反射(几何项),进一步的,发生反射的功率是存在方向上的分布的,只有一部分会朝着观察方向反射(法线分布),最后这一部分就是被观察到的功率
    • 可以看到最终反射出来的能量是小于输入能量的,而在BRDF中能体现出来,只有漫反射中被吸收的部分和镜面反射中被遮挡的部分是不会被计算出来的,既没有莫名其妙的能量计算进来,也没有莫名其妙的能量被忽视。这便是PBR的魅力。

\[f_{cook-torrance}=\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)} \]

2.2.1 法线分布函数 GGXTR NDF

  • 还记得我们之前是如何实现Glossy效果的吗?我们直接使用半程向量与法线的内积作为评判指标。在法线分布函数中,我们仍然使用内积,但是会使用更复杂的方法来描述,我们会将内积作为计算单元,放入类似高斯分布这种衰减函数中。
  • 此外,我们需要一个因子来描述Glossy效果的强度,很显然的这个因子就是粗糙度\(\alpha\)。这个粗糙度会影响衰减的快慢,在高斯函数中就是影响方差。
  • 最后,我们的NDF是参考了余弦值和粗糙度的衰减函数。下面便是其中一种NDF,即Trowbridge-Reitz GGX NDF。

\[NDF(n,h,\alpha)=\frac{\alpha^2}{\pi((n\cdot h)^2(\alpha^2-1)+1)^2} \]

float D_GGX_TR(vec3 N, vec3 H, float a)
{
	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;
	float PI = 3.1415926;
	denom = PI * denom * denom;

	return nom / denom;
}

2.2.2 几何项 Schlick-GGX 与 Smith method

  • 几何项是用于评估光线遮蔽的函数,因此其以观察方向或者光线方向与法线的内积作为评估变量。
  • 此外,几何项的函数分布是关于粗糙度的特征,在Schlick-GGX中使用k,即\(\alpha\)的重映射作为几何分布的影响因子。
  • 最后,可知几何项是综合了观察方向或者光线方向与法线的余弦值和粗糙度的重映射的函数分布。Schlick-GGX如下:

\[G_{Schlick-GGX}(n,v,k)=\frac{n\cdot v}{(n\cdot v)(1-k)+k} \]

  • 其中,重映射k在直接光照和IBL简介光照中取值不同,对于直接光照倾向于更大的粗糙度效果。

\[k_{direct}=\frac{(\alpha+1)^2}{8} \]

\[k_{IBL}=\frac{\alpha^2}{2} \]

  • 显然的是,几何效果不只是观察方向的遮蔽效果,还有光线入射方向的遮蔽效果,因此使用smith method将二者乘在一起作为综合的几何项:

\[G(n,v,l,k)=G_{sub}(n,v,k)G_{sub}(n,l,k) \]

float GeometrySchlickGGX(float NdotV, float k)
{
	float nom = NdotV;
	float denom = NdotV * (1.0 - k) + k;
	return nom / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float k)
{
	float NdotV = max(dot(N, V), 0.0);
	float NdotL = max(dot(N, L), 0.0);
	float ggx1 = GeometrySchlickGGX(NdotV, k);
	float ggx2 = GeometrySchlickGGX(NdotL, k);
	return ggx1 * ggx2;
}

2.2.3 菲涅尔项 Fresnel-Schlick

  • 一个有趣的现象是,当光线与表面法线越是接近,反射比重越小。我们把垂直时的最小反射比重叫做基础菲涅尔项F0
  • 可见,菲涅尔项应当是观察方向与法线余弦的函数分布,由于镜面反射是可逆的,菲涅尔项也是入射方向与法线余弦的函数分布,因此,一般的,我们使用观察方向与半程向量的内积作为菲涅尔项的变量。
  • 不同于法线分布和几何项是关于粗糙度的分布,对于电介质而言,菲涅尔项取决于两侧介质的介电常数
  • 电介质的菲涅尔项表达式过于繁琐,因此使用Fresnel-Schlick近似表达式,同时将介电常数的影响使用F0来描述:

\[F_{Schlick}(n,v,F_0)=F_0+(1-F_0)*(1-(h\cdot v))^5 \]

  • 但是这种计算方法不适用于导体,然而有趣的是,我们可以看出上面的Fresnel-Schlick表达式是对F0的基于\((1-h\cdot v)^5\)插值公式。对于导体,我们预计算出F0并使用同样的表达式进行插值,然而要注意的是虽然我们使用了同一个表达式统一了导体和电介质的菲涅尔项,但是他们的原理是不同的,电介质是对精准表达式的简化,而导体是对基本菲涅尔项的插值。
  • 一般的,我们对电介质的菲涅尔项取0.04就可以获得不错的效果。此外,电介质的镜面反射菲涅尔效应对RGB通道是一致的。
  • 但是,金属的菲涅尔项对不同的通道是不一致的,不仅如此,我们认为金属会将折射的能量全部吸收,即没有漫反射,而金属反射的颜色是由于菲涅尔项引起的,那么我们就可以很自然地使用金属的颜色来描述菲涅尔项,当然这是一个vec3。
  • 我们使用金属度metalness来描述金属的倾向,按理来说这个属性只有二值,但是我们可以在0-1之间取值来达成不错的效果。金属度可见有两个用途:
    • 菲涅尔项:我们使用金属度在电介质菲涅尔项(一般取vec3(0.04))与导体菲涅尔项(即导体颜色)之间进行插值。来描述金属菲涅尔项的颜色特征。
    • Kd:我们使用金属度在Kd与0.0之间插值。来描述金属对折射光的吸收特征。
vec3 F0 = vec3(0.04);
F0 = mix(F0, surfaceColor.rgb, metalness);
float FresnelSchlick(float cosTheta, vec3 F0)
{
	return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5);
}

2.2.4 Cook-Torrance 反射率方程

\[L_o(p,\omega_o)=\int_{\Omega}(k_d\frac{c}{\pi}+k_s\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)})L_i(p,\omega_i)n\cdot \omega_i d\omega_i \]

3. PBR材质

显然的,PBR材质就是对我们的反射率方程中需要用到的参数的界定:在漫反射中,需要Albedo,在D中需要normal、lightDir、viewDir、roughness,在F中需要lightDir、viewDir、F0、metalness,在G中需要normal、lightDir、viewDir、roughness,在标准项中需要normal、lightDir、viewDir,在radiance转irradiance中需要normal和lightDir,在计算Ambient时可以预计算AO即环境遮蔽,在计算kd时需要metalness。其中,F0一般是被提取出来作为参数的,不是用纹理。综合看来,需要Albedo、Normal、Roughness、Metalness、AO

二、光照

1.光照

到目前为止,对于Cook-Torance 反射方程,我们在Li这一项还有存疑:我们应该如何表示这一光的功率项呢?我们应该这样理解radiance。

  • radiant intensity即辐射强度:指单位立体角的光功率。

\[I=\frac{d\Phi}{d\omega} \]

  • 那么可得:

\[L=\frac{dI}{cos\theta dA} \]

  • 也就是指,辐射强度本身只强调单位立体角,但是对面积没有强调,然而辐射在此基础上强调了单位面积的功率密度,如果说辐射强度是从光源划定的锥形分布,那么辐射就是在这个锥形的每一个截面上都做了一次面积密度,那么一来,从辐射强度到辐射其描述对象是从光锥到光线(更合适的说法应当是单位面积锥面的窄光锥)了。那么,我们从辐射强度radiant intensity来获取辐射radiance就是顺水推舟地事情了。
  • 拿点光源为例,假设其总功率是\(\Phi\),而球面的总立体角是\(4\pi\),那么单位立体角地功率,也就是说辐射强度radiant intensity是:

\[I=\frac{\Phi}{4\pi} \]

  • I是指单位立体角的功率,为了得到radiance,还需要求解面积密度

\[L=\frac{\Phi}{4\pi r^2}=\frac{\Phi}{4\pi r^2}=\frac{I}{r^2} \]

  • 一般的,我们不会去描述功率\(\Phi\),可以看出直接给出\(\frac{\Phi}{4\pi}\)就可以了,我们把它称作光源的颜色,使用RGB来描述,这个成分有时候会用辐射通量来描述,但是我们可以看到这其实并不是辐射通量\(\Phi\),而是辐射强度I。
  • 进一步的,给出光源的辐射强度之后,只需要按照lightPos和fragPos的距离做一次衰减就可以了,正如之前我们是使用二次项来实现衰减的。
  • 另外,可以看出反射率方程是对所有立体角的积分,但是对于理想的点光源而言,他对着色点的影响显然只是一个以着色点附近单位面积为截面的光追,亦即一个radiance单位,因此,对于一个点光源而言,其对着色点的光照贡献是一个离散值。当我们考虑多个点光源时,这个积分就是离散求和。

2. 直接光照

接下来,我们将这个计算过程给实现:

  • 首先,我们把NDF、G、F这三项重写一下:
    • 对于NDF项,我们把粗糙度roughness进行了二次幂操作,据说这样会使得效果更好。其他地方没有变动。
float D_GGX_TR(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;
}
  • 对于F项,我们加入了calmp操作,有必要作为一种保险的操作,但是其实我们在外部会进行max操作,所以功能是重叠的。当然,因为是直接光,所以采取了\(k=(\alpha+1)^2/8\)
vec3 FresnelSchlick(float cosTheta, vec3 F0)
{
	return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5);
}
  • 对于G项,我们将k值重构在函数内部实现了,并将参数很显然的改作了roughness。
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;
}
  • 然后,我们实现反射率方程,并对所有点光源进行离散求和。
    • 首先,对于PBR来说,我们的PBR材质参数,即albedo、normal、ao、metalness和roughness,我们暂时不适用纹理,而是采取全局变量的方式输入,当然了其中的normal仍然是使用顶点属性。
    • 然后,我们计算反射方程需要用到几个矢量,即法向量N、光照方向L、观察方向V、半程向量H需要计算出来,显然的是,我们的法向量N在此可以通过normal直接得到,当然如果我们使用纹理的话需要从纹理中提取,这会在会面描述,另外观察矢量V可以通过fragPos和viewPos得到,这两个矢量的获得方法已经很熟悉了吧。对于L和H则是跟光照有关的量,需要放在点光源的循环离散计算中分别实现。其中,光线方向L可以通过lightPos和fragPos来实现,这里的lightPos也是通过全局变量输入的,也是我们熟悉的操作了。至于半程向量H直接通过L和V之和取标准化就可以了。
    • 我们在拿到矢量之后,就可以去计算我们需要的一些内积了,当然需要进行mx操作。如,NdotH(NDF)、NdotV(G)、NdotL(G、辐射强度)、HdotV(F)。当然,而可以看出有些内积的计算我们是放在了函数内部了。
  • 现在,我们就进入每一个点光源的循环内部:
    • 首先,我们去计算点光源的radiance。这个正如我们在之前所说的,使用点光源的颜色lightColors[i](实际上是辐射强度I),然后进行距离平方反比的衰减attenuation就可以了。这就是我们的每一个点光源的radiance即Li了。
		float distance = length(lightPositions[i] - fragPos);
		float attenuation = 1.0 / (distance * distance);
		vec3 radiance = lightColors[i] * attenuation;
  • 在拿到了辐射radiance之后,我们就去计算diffuse和reflection的BRDF,我们首先计算specular,这是因为计算kD需要首先用到菲尼尔项。正如我们在之前所说的DFG三项,我们只需要代入方程就可以了。当然了,在这里有一个有趣的事情,那就是我们需要用到金属度metalness来计算F0了(这个是金属度唯二被用到的地方)。显然的是,这个计算是用金属度对F0(指代电介质的基础菲涅尔项)和Albedo(用于描述金属的基础菲涅尔项)进行插值。有了DFG之后,就拿到了BRDF的分子了,我们只需要进一步利用两个内积求出分母就得到specular的BRDF了。注意,分母因为使用到了余弦值,最好给他加入一个小的offset,避免除以零或者很小的值,否则这个会造成我们只能在有限的区域内部看到光照。
		//Fresnel
		vec3 F0 = vec3(0.04);
		F0 = mix(F0, albedo, metalness);
		float HdotV = max(dot(H, V), 0.0);
		vec3 F = FresnelSchlick(HdotV, F0);

		//Normal
		float NDF = D_GGX_TR(N, H, roughness);
		
		//Geometry
		float G = GeometrySmith(N, V, L, roughness);
		//Cook-Torrance Specular
		vec3 nom = NDF * F * G;
		float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0)+0.0001;
		vec3 specular = nom / denom;
  • 在拿到了菲涅尔项之后,我们拿来计算kD,即1-F,当然,我们还需要使用金属度来做插值(这是最后一个用处了),即对kD和0.0之间的插值。紧接着,我们使用\(Albedo/\pi\),来获得BRDF。
		//Lambert Diffuse
		vec3 kS = F;
		vec3 kD = vec3(1.0) - kS;
		kD = kD * (1.0 - metalness);

		vec3 diffuse = kD * albedo / PI;
  • 到这里,我们拿到了BRDF,即specular+diffuse,我们只需要在利用每一个点光源的radiance和NdotL就可以得到一个离散求和的项了。之后,在对他们进行累加就会得到\(L_o\)了。
		//Cook-Torance Refection Equation
		float NdotL = max(dot(N, L), 0.0);
		Lo += (diffuse + specular) * radiance * NdotL;
  • 需要注意的是,反射率方程只包含了镜面反射和漫反射,而不包括环境光照,实际上,我们会在后面使用反射率方程来实现漂亮的环境光照,但是在这里我们先使用一个常数值来表示,当然不失体面的,我们给他加入一个ao值。
	//ambient
	vec3 ambient = vec3(0.03) * albedo * ao;
  • 这样,我们就拿到了这篇片段的光照计算结果了
	//total color
	vec3 color = ambient + Lo;
  • 最后,需要注意的是,如果我们在后面没有使用其他帧缓冲做后期处理,那么我们就需要在这里实现HDR(这里采取的是Reinhard)和Gamma Correction。
	//Reinhard HDR and Gamma correction
	//color = color / (color + vec3(1.0));
	//color = pow(color, vec3(1.0 / 2.2));

	fragColor = vec4(color, 1.0);


#version 330 core
in vec3 fragPos;
in vec3 normal;
in vec2 texCoord;

out vec4 fragColor;

uniform vec3 viewPos;

uniform vec3 albedo;
uniform float metalness;
uniform floatao;
uniform float roughness;

uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

float D_GGX_TR(vec3 N, vec3 H, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 FresnelSchlick(float cosTheta, vec3 F0);

const float PI = 3.14159265359;

void main()
{
	vec3 albedo = texture(albedoMap, texCoord).rgb;
	float metalness = texture(metalnessMap, texCoord).r;
	float ao = texture(aoMap, texCoord).r;
	float roughness = texture(roughnessMap, texCoord).r;

	vec3 N = GetNormalFromMap();
	vec3 V = normalize(viewPos - fragPos);

	vec3 Lo = vec3(0.0f);
	for (int i = 0; i != 4; i++)
	{
		vec3 L = normalize(lightPositions[i] - fragPos);
		vec3 H = normalize(L + V);

		float distance = length(lightPositions[i] - fragPos);
		float attenuation = 1.0 / (distance * distance);
		vec3 radiance = lightColors[i] * attenuation;

		//Fresnel
		vec3 F0 = vec3(0.04);
		F0 = mix(F0, albedo, metalness);
		float HdotV = max(dot(H, V), 0.0);
		vec3 F = FresnelSchlick(HdotV, F0);

		//Normal
		float NDF = D_GGX_TR(N, H, roughness);
		
		//Geometry
		float G = GeometrySmith(N, V, L, roughness);

		//Cook-Torrance Specular
		vec3 nom = NDF * F * G;
		float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0)+0.0001;
		vec3 specular = nom / denom;

		//Lambert Diffuse
		vec3 kS = F;
		vec3 kD = vec3(1.0) - kS;
		kD = kD * (1.0 - metalness);

		vec3 diffuse = kD * albedo / PI;

		//Cook-Torance Refection Equation
		float NdotL = max(dot(N, L), 0.0);
		Lo += (diffuse + specular) * radiance * NdotL;
	}

	//ambient
	vec3 ambient = vec3(0.03) * albedo * ao;

	//total color
	vec3 color = ambient + Lo;

	//Reinhard HDR and Gamma correction
	//color = color / (color + vec3(1.0));
	//color = pow(color, vec3(1.0 / 2.2));

	fragColor = vec4(color, 1.0);
}

float D_GGX_TR(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;
}

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

vec3 FresnelSchlick(float cosTheta, vec3 F0)
{
	return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5);
}
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
layout(location = 2) in vec3 aNorm;

out vec3 fragPos;
out vec3 normal;
out vec2 texCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
	gl_Position = projection * view * model * vec4(aPos, 1.0f);
	fragPos = vec3(model * vec4(aPos, 1.0f));
	mat3 normModel = transpose(inverse(mat3(model)));
	normal = normalize(normModel * aNorm);
	texCoord = aTexCoord;
}
	//PBR
	Sphere pbrSphere(deferCubeTex, deferCubeTex, shininess);
	pbrSphere.SetModel(glm::vec3(0.0, 0.0, 0.0), 1.0f);
glm::vec3 lightPositions[] =
	{
		glm::vec3(42.0f,42.0f,-30.0f),
		glm::vec3(42.0f,56.0f,-30.0f),
		glm::vec3(56.0f,42.0f,-30.0f),
		glm::vec3(56.0f,56.0f,-30.0f)
	};

	glm::vec3 lightColors[] =
	{
		glm::vec3(300.0f, 300.0f, 300.0f),
		glm::vec3(300.0f, 300.0f, 300.0f),
		glm::vec3(300.0f, 300.0f, 300.0f),
		glm::vec3(300.0f, 300.0f, 300.0f)
	};
//渲染循环:
		//PBR
		pbrShader.use();

		pbrShader.setVec3("albedo", glm::vec3(0.5, 0.0, 0.0));
		pbrShader.setFloat("ao", 1.0);
		pbrShader.setVec3("viewPos", camera.Position);

		for (unsigned int i = 0; i != 4; i++)
		{
			pbrShader.setVec3(("lightPositions[" + std::to_string(i) + "]").c_str(), lightPositions[i]);
			pbrShader.setVec3(("lightColors[" + std::to_string(i) + "]").c_str(), lightColors[i]);
		}

		pbrShader.setMat4("view", glm::value_ptr(view));
		pbrShader.setMat4("projection", glm::value_ptr(projection));

		for (unsigned int i = 0; i != 7; i++)
		{
			float metalness = (float)i / 7.0f;
			pbrShader.setFloat("metalness", metalness);
			for (unsigned int j = 0; j != 7; j++)
			{
				float roughness = glm::clamp((float)j / 7.0f, 0.05f, 1.0f);
				glm::vec3 position = glm::vec3((float)i * 2.5, (float)j * 2.5, 0.0f);
				glm::mat4 model = glm::mat4(1.0f);
				model = glm::translate(model, glm::vec3(40.0, 40.0, -40.0) + position);
				
				pbrShader.setFloat("roughness", roughness);
				pbrShader.setMat4("model", glm::value_ptr(model));

				glBindVertexArray(pbrSphere.VAO);
				glDrawElements(GL_TRIANGLES, pbrSphere.Count, GL_UNSIGNED_INT, 0);
			}
		}

3. PBR贴图

我们已经实现了比较完整的PBR shader了,但是美中不足的是,我们最好把之前提到的五个参数使用纹理来实现。

  • 我们的改动不多,只是将albedo、ao、normal、metalness、roughness这五个PBR参数使用纹理来获取。其中,需要注意的是,albedo和ao是艺术家调出来的sRGB空间的值,所以在读取时需要转化成线性空间,至于其他的值一般都是线性空间。我们在这里修改一下Object类,给他加入PBR属性,以及设置PBR属性的函数。
struct pbrMaps {
	unsigned int albedoMap, normalMap, metalnessMap, aoMap, roughnessMap;
};

unsigned int LoadTexture(const char* path, bool isSRGB = true)
{
	unsigned int texture;
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	stbi_set_flip_vertically_on_load(true);
	int width, height, nrChannels;
	unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);
	if (data)
	{
		if (nrChannels == 1)
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, data);
		else if (nrChannels == 3)
			glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		else if (nrChannels == 4)
			glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture." << std::endl;
	}
	stbi_image_free(data);
	return texture;
}

class Object {
public:
        //...
	pbrMaps PBR;
	void SetPBR(const char* albedo, const char* normal, const char* metalness, const char* ao, const char* roughness);
};

void Object::SetPBR(const char* albedo, const char* normal, const char* metalness, const char* ao, const char* roughness)
{
	//albedo
	if (albedo)
		PBR.albedoMap = LoadTexture(albedo);
	//normal
	if (normal)
		PBR.normalMap = LoadTexture(normal,false);
	//metalness
	if (metalness)
		PBR.metalnessMap = LoadTexture(metalness, false);

	//ao
	if (ao)
		PBR.aoMap = LoadTexture(ao);
	//roughness
	if (roughness)
		PBR.roughnessMap = LoadTexture(roughness, false);
}
//...
  • 另外一个非常重要的地方是,我们的法线纹理是切线空间的,需要转换成世界空间,但是我们之前是将Normal、Tangent和Bitangent矢量作为顶点属性来传输的,而在外部计算是很直观的,那么我们如何在片段着色器中实现这一过程呢?我们是使用dFdx()和dFdy()函数来实现的,这个函数可以获得面元光栅化之后在屏幕空间(即xy坐标空间)下某个片段属性关于x和y的偏导数(直观一点来说就是该片段与相邻x偏移或者y偏移一个像素的片段在某个属性上的差值)来作为之前的edge和deltaUV,另外,可以看出我们是不需要计算行列式一项的,因为这不影响矢量的偏转(也许会影响方向?),这样我们就拿到了TBN矩阵,然后将法线纹理转化为世界坐标。
vec3 GetNormalFromMap()
{
	vec3 tangentNormal = texture(normalMap, texCoord).xyz * 2.0 - 1.0;

	vec3 edge1 = dFdx(fragPos);
	vec3 edge2 = dFdy(fragPos);
	vec2 deltauv1 = dFdx(texCoord);
	vec2 deltauv2 = dFdy(texCoord);

	vec3 N = normalize(normal);
	vec3 T = normalize(deltauv2.y * edge1 - deltauv1.y * edge2);
	vec3 B = normalize(cross(N, T));

	mat3 TBN = mat3(T, B, N);
	return normalize(TBN * tangentNormal);
}

#version 330 core
in vec3 fragPos;
in vec3 normal;
in vec2 texCoord;

out vec4 fragColor;

uniform vec3 viewPos;

uniform sampler2D albedoMap;
uniform sampler2D metalnessMap;
uniform sampler2D aoMap;
uniform sampler2D roughnessMap;
uniform sampler2D normalMap;

uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

float D_GGX_TR(vec3 N, vec3 H, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 FresnelSchlick(float cosTheta, vec3 F0);
vec3 GetNormalFromMap();

const float PI = 3.14159265359;

void main()
{
	vec3 albedo = texture(albedoMap, texCoord).rgb;
	float metalness = texture(metalnessMap, texCoord).r;
	float ao = texture(aoMap, texCoord).r;
	float roughness = texture(roughnessMap, texCoord).r;

	vec3 N = GetNormalFromMap();
	vec3 V = normalize(viewPos - fragPos);

	vec3 Lo = vec3(0.0f);
	for (int i = 0; i != 4; i++)
	{
		vec3 L = normalize(lightPositions[i] - fragPos);
		vec3 H = normalize(L + V);

		float distance = length(lightPositions[i] - fragPos);
		float attenuation = 1.0 / (distance * distance);
		vec3 radiance = lightColors[i] * attenuation;

		//Fresnel
		vec3 F0 = vec3(0.04);
		F0 = mix(F0, albedo, metalness);
		float HdotV = max(dot(H, V), 0.0);
		vec3 F = FresnelSchlick(HdotV, F0);

		//Normal
		float NDF = D_GGX_TR(N, H, roughness);
		
		//Geometry
		float G = GeometrySmith(N, V, L, roughness);

		//Cook-Torrance Specular
		vec3 nom = NDF * F * G;
		float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0)+0.0001;
		vec3 specular = nom / denom;

		//Lambert Diffuse
		vec3 kS = F;
		vec3 kD = vec3(1.0) - kS;
		kD = kD * (1.0 - metalness);

		vec3 diffuse = kD * albedo / PI;

		//Cook-Torance Refection Equation
		float NdotL = max(dot(N, L), 0.0);
		Lo += (diffuse + specular) * radiance * NdotL;
	}

	//ambient
	vec3 ambient = vec3(0.03) * albedo * ao;

	//total color
	vec3 color = ambient + Lo;

	//Reinhard HDR and Gamma correction
	//color = color / (color + vec3(1.0));
	//color = pow(color, vec3(1.0 / 2.2));

	fragColor = vec4(color, 1.0);
}



float D_GGX_TR(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;
}

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

vec3 FresnelSchlick(float cosTheta, vec3 F0)
{
	return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5);
}

vec3 GetNormalFromMap()
{
	vec3 tangentNormal = texture(normalMap, texCoord).xyz * 2.0 - 1.0;

	vec3 edge1 = dFdx(fragPos);
	vec3 edge2 = dFdy(fragPos);
	vec2 deltauv1 = dFdx(texCoord);
	vec2 deltauv2 = dFdy(texCoord);

	vec3 N = normalize(normal);
	vec3 T = normalize(deltauv2.y * edge1 - deltauv1.y * edge2);
	vec3 B = normalize(cross(N, T));

	mat3 TBN = mat3(T, B, N);
	return normalize(TBN * tangentNormal);
}
	//PBR
	Sphere pbrSphere(deferCubeTex, deferCubeTex, shininess);
	pbrSphere.SetModel(glm::vec3(0.0, 0.0, 0.0), 1.0f);
	const char* albedoPbrMap = "res/texture/pbrSphere/rustediron2_basecolor.png";
	const char* normalPbrMap = "res/texture/pbrSphere/rustediron2_normal.png";
	const char* metalnessPbrMap = "res/texture/pbrSphere/rustediron2_metallic.png";
	const char* aoPbrMap = "res/texture/pbrSphere/rustediron2_ao.png";
	const char* roughnessPbrMap = "res/texture/pbrSphere/rustediron2_roughness.png";
	pbrSphere.SetPBR(albedoPbrMap, normalPbrMap, metalnessPbrMap, aoPbrMap, roughnessPbrMap);

	glm::vec3 lightPositions[] =
	{
		glm::vec3(42.0f,42.0f,-30.0f),
		glm::vec3(42.0f,56.0f,-30.0f),
		glm::vec3(56.0f,42.0f,-30.0f),
		glm::vec3(56.0f,56.0f,-30.0f)
	};

	glm::vec3 lightColors[] =
	{
		glm::vec3(300.0f, 300.0f, 300.0f),
		glm::vec3(300.0f, 300.0f, 300.0f),
		glm::vec3(300.0f, 300.0f, 300.0f),
		glm::vec3(300.0f, 300.0f, 300.0f)
	};
\\渲染循环
		//PBR
		pbrShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, pbrSphere.PBR.albedoMap);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, pbrSphere.PBR.normalMap);
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, pbrSphere.PBR.metalnessMap);
		glActiveTexture(GL_TEXTURE3);
		glBindTexture(GL_TEXTURE_2D, pbrSphere.PBR.aoMap);
		glActiveTexture(GL_TEXTURE4);
		glBindTexture(GL_TEXTURE_2D, pbrSphere.PBR.roughnessMap);
		pbrShader.setInt("albedoMap", 0);
		pbrShader.setInt("normalMap", 1);
		pbrShader.setInt("metalnessMap", 2);
		pbrShader.setInt("aoMap", 3);
		pbrShader.setInt("roughnessMap", 4);


		pbrShader.setVec3("viewPos", camera.Position);

		for (unsigned int i = 0; i != 4; i++)
		{
			pbrShader.setVec3(("lightPositions[" + std::to_string(i) + "]").c_str(), lightPositions[i]);
			pbrShader.setVec3(("lightColors[" + std::to_string(i) + "]").c_str(), lightColors[i]);
		}

		pbrShader.setMat4("view", glm::value_ptr(view));
		pbrShader.setMat4("projection", glm::value_ptr(projection));
		for (unsigned int i = 0; i != 7; i++)
		{
			for (unsigned int j = 0; j != 7; j++)
			{
				float roughness = glm::clamp((float)j / 7.0f, 0.05f, 1.0f);
				glm::vec3 position = glm::vec3((float)i * 2.5, (float)j * 2.5, 0.0f);
				glm::mat4 model = glm::mat4(1.0f);
				model = glm::translate(model, glm::vec3(40.0, 40.0, -40.0) + position);
				pbrShader.setMat4("model", glm::value_ptr(model));

				glBindVertexArray(pbrSphere.VAO);
				glDrawElements(GL_TRIANGLES, pbrSphere.Count, GL_UNSIGNED_INT, 0);
			}
		}

三、Ambient(IBL)——漫反射的辐照度图

我们在前面使用反射方程实现了直接光照,但是除了更贴合物理、更方便设计之外,PBR相对于传统的方法在直接光照方面的效果并不算是他最漂亮的地方。要说到PBR的效果,不能不提对间接光照的实现。间接光照(更准确的说是一次弹射的间接光照)的实现仍然需要依赖反射方程,当然,此处的Li指的是间接的入射光照,Lo指的是全局光照。

\[L_o(p,\omega_o)=\int_\Omega(k_d\frac{c}{\pi}+k_s\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)})L_i(p,\omega_i)n\cdot \omega_i d\omega_i \]

  • 很显然的,漫反射部分和镜面反射部分是可以分开计算的。

\[L_o(p,\omega_o)=\int_\Omega k_d\frac{c}{\pi}L_i(p,\omega_i)n\cdot \omega_i d\omega_i+\int_\Omega k_s\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)}L_i(p,\omega_i) n\cdot \omega_i d\omega_i \]

  • 这二者的计算原理不尽相同,首先介绍个简单的漫反射部分。

\[L_o(p,\omega_o)=(k_d\frac{c}{\pi})\int_\Omega L_i(p,\omega_i)n\cdot \omega_i d\omega_i \]

  • 因为漫反射的输出是各向同性的,所以其BRDF相对于某一个着色点而言是常数,所以对于漫反射部分,我们在某一个着色点的ambient计算时只需要计算出该着色点的irradiance(很显然的就是积分部分)就可以得出该着色点的ambient中的diffuse部分了。
  • 那么,要如何计算irradiance呢?要回答这个问题,我们就要首先明确ambient的光照是什么。

1. HDR贴图

  • 我们在实现ambient全局光照,就是要尽量实现所有间接光照的效果,那么很显然的是ambient的光源就是所有间接光照。我们在之前使用点光源、平行光、聚光还有其他的如面光源等来实现直接光照,那么我们用什么来实现全局光照呢。在光追中,这个很显然仍然是使用直接光照的光源来实现的,但是这需要多次弹射的计算才能达成看得过去的效果,这对于实时全局光照来说是比较大的代价。因此,在实时全局光照中,我们使用贴图来记录预计算得到的着色点处的全局光照。这个纹理是什么形式的呢?很显然的是,一个着色点附近的全局光照就是在这个着色点处向所有方向观察的影像了,所以这个纹理就像是我们的SkyBox。
  • 但是,需要注意的是,我们的skybox是sRGB空间中8bits的数据,这个拿来形成skybox是足够了,因为看起来还真像那么回事,但是拿来做全局光照就不够了。这是因为,光源有强度的概念,如果限制在256档位显然是不足以模拟光源的(还记得我们将之前的4个点光源的颜色设置到了300.0f不?)因此,我们不能使用之前的UNSIGNED_BYTE格式的纹理来实现全局光照纹理了。这就需要引出一个很重要的纹理——HDR格式的纹理,是一种记录FLOAT格式的纹理。能够记录float的纹理不止.hdr一种,还有其他的,但是我们这里就用.hdr啦。
  • .hdr并没有直接记录三个浮点数通道,而是使用RGBA四个通道,并将第四个通道记录指数。
  • 同样的,我们要读取.hdr文件,还是得仰仗stb_image.h大哥。在读取方面与之前的稍有不同:我们使用stbi_loadf函数,函数的参数没有变化,但是返回值不再是unsigned char,而是float,这是很自然的结论。另外一个不算是变化的变化就是申请空间时使用GL_RGB16F,注意哦没有转换到sRGB空间。
void IBL::GetHDR(string path)
{
	stbi_set_flip_vertically_on_load(true);
	int width, height, nrChannels;
	float* data = stbi_loadf(path.c_str(), &width, &height, &nrChannels, 0);
	if (data)
	{
		glGenTextures(1, &HDR);
		glBindTexture(GL_TEXTURE_2D, HDR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data);
	}
	else
	{
		std::cout << "Failed to load HDR " + path << std::endl;
	}
	stbi_image_free(data);
}
  • 我们拿到了HDR纹理,把他画在一个立方体上看一看喽。咦?一个2D纹理要怎么画到box上呢?思路很简单,我们只需要使用纹理坐标的变换就可以了。HDR的2D纹理是等距柱状贴图,也就是使用球坐标中的theta和phi作为uv坐标的,我们要进行映射只需要将3D的xyz坐标映射到球坐标上就可以啦。
const float PI = 3.14159265359;

vec2 SampleSphericalMap(vec3 v)
{
	vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
	uv /= vec2(2 * PI, PI);
	uv += 0.5;
	return uv;
}

帅不喽!

  • 但是,我们希望的不只是如此,我们需要把等距柱状贴图转换成立方体贴图方便我们操作。方法很简单,我们只需要申请一个帧缓冲和一个立方体贴图,然后渲染贴图的六个面,并使用上面的纹理坐标映射来绘制颜色。渲染的思路很简单,我们只需要在一个立方体的中心渲染他就可以了。在前面我们使用了几何着色器的gl_Layer来实现立方体纹理的渲染,在这里我们再使用另一个方法试试:我们没有把立方体贴图使用glFramebufferTexture来将立方体贴图附加到帧缓冲中,而是分六次渲染,并在每次使用glFramebufferTexture2D将立方体贴图的一个面作为2D纹理附加到上面。在上面你可能看到了背景中的skybox是我们的汪洋大海环山绕,我很喜欢这张图,但是在这里也只能让他退休啦。我们把新的HDR纹理画成skybox。
void IBL::GetCubeMap()
{
	glGenFramebuffers(1, &envCubeFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, envCubeFBO);

	glGenRenderbuffers(1, &envCubeRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, envCubeRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, envCubeRBO);

	glGenTextures(1, &envCubeMap);
	glBindTexture(GL_TEXTURE_CUBE_MAP, envCubeMap);
	for (unsigned int i = 0; i != 6; i++)
	{
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, NULL);
	}
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	glm::mat4 projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
	glm::mat4 views[] =
	{
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))

	};

	envCubeShader.use();
	envCubeShader.setMat4("projection", glm::value_ptr(projection));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, HDR);
	envCubeShader.setInt("equirectangularMap", 0);

	glViewport(0, 0, 512, 512);
	glEnable(GL_DEPTH_TEST);

	for (unsigned int i = 0; i != 6; i++)
	{
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubeMap, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		envCubeShader.setMat4("view", glm::value_ptr(views[i]));
	
		glBindVertexArray(cubeVAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(i * 6 * sizeof(unsigned int)));
	}
}
  • 到这里我们就拿到了全局光照的立方体贴图啦,我们可以利用它做很多有趣的事情了!但是,在此之前,我们先来理解一下这个全局光照贴图:作为一个立方体贴图或者等距柱状贴图,显然我们是需要使用着色点的入射光的lightDir方向向量来查询光照的,那么就像skybox一样,这个光照对于所有的着色点而言是没有位移差别的,这很不正常,但是我们先不管。

2. 辐照度图

我们有了全局光照立方体贴图,那么就有资格计算irradiance了。很显然的是纹理查询的结果正是radiance(我们认为全局光照贴图的radiance是可以直接用的,不需要做距离衰减,毕竟这就是在camera处观察到的已经衰减了的光照)。那么,我们现在的问题变成,如何计算积分呢?

\[L_o(p,\omega_o)=(k_d\frac{c}{\pi})\int_\Omega L_i(p,\omega_i)n\cdot \omega_i d\omega_i \]

  • 很自然的,我们使用采样。你应该很熟悉蒙特卡洛积分了吧,这里我们进行半球面均匀采样,也就是黎曼和。要注意的是,**我们不会真的使用立体角去做积分哒,而是会转换成球坐标,即

\[\int_\Omega d\omega = \int_0^{2\pi}\int_0^{0.5\pi}sin\theta d\theta d\phi \]

进一步的,我们得到:

\[L_o(p,\omega_o)=(k_d\frac{c}{\pi})\int_0^{2\pi}\int_0^{0.5\pi}L_i(p,\theta,\phi)cos\theta sin\theta d\theta d\phi \]

当我们进行离散计算时,就需要变成:

\[L_o(p,\omega_o)=(k_d\frac{c}{\pi})\times(\sum_0^{2\pi}\sum_0^{0.5\pi}L_i(p,\theta,\phi)cos\theta sin\theta) \times (\frac{\pi^2}{nrSamples}) \]

细心的你也许注意到了,我们的公式与原文并不一样,那么谁是对的呢,当然是我对啦,😀,信我没错。

  • 那么思路就很简单了,我们在半球面内遍历phi从0到\(2\pi\),遍历theta从0到\(0.5\pi\),并取合适的间隔,对于结果我们去转化到xyz坐标中,这三个坐标值很显然就是\(cos\theta\)\(sin\theta sin\phi\)\(sin\theta cos\phi\)需要注意的是,我们对这三个坐标的对应不是强制要求的,但是要注意\(cos\theta\)对应半球面采样的采样方向也就是法线方向,并外两个是切线方向;那么很自然地会注意到这个坐标是切线空间坐标,如果我们要对应一致便于阅读的话最后选择z坐标为法线方向,最后不可避免地我们需要进行TBN变换,其中N坐标取着色点法线就可以了,无论是顶点插值得到的还是来自法线纹理,但是要注意的是,对于TB方向的获取不必费劲进行坐标映射了,因为我们不追求切向坐标的严格标准,毕竟是在遍历所有方向,所以只要保证是在法线的垂面上就可以了,思路很简单,两次叉积就行啦。拿到采样的方向后,对HDR立方体纹理查找就可以得到radiance了。按照上面的公式,我们还需要乘上两个余弦项再去累加。
  • 进一步的,我们需要注意到原文中这样一个计算公式irradiance = PI * irradiance * (1.0 / float(nrSamples));,这样做是没有错的,但是写的很丑,不如按照我的写法,irradiance = irradiance * (PI * PI) / nrSamples;,你注意到差别了没?原文中少了一个\(\pi\),你也许会疑惑这个pi去哪了,没错,聪明的你发现了这是原文提前计算了BRDF中的1.0/pi。我把他们给分开了,把pi拿到了后续其他shader的ambient的diffuse计算中了,这样会多了一些没用的计算,但是可读性好一些了。
#version 330 core

in vec3 localPos;
out vec4 fragColor;

uniform samplerCube envCubeMap;

const float PI = 3.14159265359;

void main()
{
	vec3 normal = normalize(localPos);
	vec3 up = vec3(0.0, 1.0, 0.0);
	vec3 right = cross(up, normal);
	up = cross(normal, right);
	mat3 RUN = mat3(right, up, normal);

	vec3 irradiance = vec3(0.0);

	float sampleDelta = 0.025;
	float nrSamples = 0.0;

	for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
	{
		for (float phi = 0.0; phi < 2 * PI; phi += sampleDelta)
		{
			vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
			vec3 sampleVec = RUN * tangentSample;

			irradiance += texture(envCubeMap, sampleVec).rgb * cos(theta) * sin(theta);

			nrSamples++;
		}
	}

	irradiance = irradiance * (PI * PI) / nrSamples;

	fragColor = vec4(irradiance, 1.0);
}

  • 现在我们就拿到辐照度图了,我们把它画成天空盒看看:

    再把它画到Object上看看,完美!

3. IBL的Ambient漫反射计算

废了不多的功夫,我们已经拿到了辐照度图,接下来直接就可以在之前的shader中采样作为全局光照diffuse部分的irradiance了。关于\(\frac{c}{\pi}\)对你来说应该不是问题,只需要着色点的纹理采样就可以了,之前已经实现了。

\[L_o(p,\omega_o)=(k_d\frac{c}{\pi})\times(\sum_0^{2\pi}\sum_0^{0.5\pi}L_i(p,\theta,\phi)cos\theta sin\theta) \times (\frac{\pi^2}{nrSamples}) \]

但是,要注意的是,菲涅尔项的计算。我们之前计算菲涅尔项是使用半程向量和观察向量,但是在这里我们的光照来自半球面,并没有明确的半程向量,因此采用法向量与观察方向。然而,问题在于,当视线掠过平面时,也就是观察Object的边缘时,NV夹角是90.0,但是我们的光照采样的HV夹角几乎全部都是小于90度的,这就会使得菲涅尔项比合理的结果大的很多,结果就是边缘的Ambient diffuse会很亮(相对而言,哈哈),因此我们需要改进菲涅尔项的计算。思路很简单,我们只需要减小菲涅尔项的插值最大值就可以了,原本是1.0,我们在这里使用max(1.0-roughness,F0),将其变成从1.0按照光滑程度逐渐降低到F0的结果。

vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
	return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
	//ambient
	vec3 F0 = vec3(0.04);
	F0 = mix(F0, albedo, metalness);
	float NdotV = max(dot(N, V), 0.0);
	vec3 kS = FresnelSchlickRoughness(NdotV, F0, roughness);
	vec3 kD = 1 - kS;
	kD *= metalness;
	vec3 irradiance = texture(irradianceMap, N).rgb;
	vec3 diffuse = kD * (albedo / PI) * irradiance;
	vec3 ambient = diffuse * ao;

现在,我们就得到了ambient的diffuse的全局光照实现啦!

四、Ambient(IBL)——镜面反射

我们已经拿到了全局光照中的漫反射成分了,下面我们就再拿到镜面反射部分,让美美的。还记得我们将漫反射部分和镜面反射部分,从反射方程中很合理地拆开了么,下面就去处理镜面反射部分。

\[L_o(p,\omega_o)=\int_\Omega\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)}L_i(p,\omega_i)n\cdot \omega_i d\omega_i \]

  • 首先,我们应该注意到,漫反射部分的积分只是对于入射方向的积分,因此我们不需要在积分时考虑出射方向会是什么,只需要按照着色点法向(或者说对入射的全局光照的采样方向),就很容易地使用采样来实现了,来获得一个关于出射方向的纹理,即辐照度图。但是镜面反射部分的积分既与入射方向有关,还与出射方向有关,这让我们难以处理。更直观的说,我们需要对入射光进行BRDF,然后积分获得出射光,但是当观察方向变化之后BRDF也会不同,即各向异性,因此我们无法对一个着色点按照法向来采样半球面,并对所有出射方向保持一致了,进一步的即便是我们把这个采样积分过的纹理提取出来之后再按照观察方向后处理也是比较丑陋的,因为对于不同的入射方向在积分是就存在着BRDF差异了,这样看来,如果要获得一个好的效果,我们在积分时就需要设计一个同时考虑入射和观察方向的纹理,这显然很困难。
  • 但是,一个有趣的地方在于,我们的积分结果,即纹理,显然是一个使用方向矢量来查询的纹理,一般来说是一个立方体贴图,因此我们很难实现一个使用两个方向矢量来查询的纹理;但是,之所以纹理要使用方向矢量来查询,是因为一个纹理像素就是对光照按着某个方向进行波瓣采样的结果,所以很显然的是对全局光照的采样造成了方向矢量查询纹理的局面;到这里就可以看出一个非常有意思的现象了,也就是对光照采样只需要考虑入射方向,而对于出射方向是没有需求的,只是因为BRDF中存在着出射方向才使得我们需要同时考虑入射和出射方向;那么,一个非常有趣的结论就诞生了,如果我们能够把光照辐射radiance与BRDF的积分拆开,那么我们对于光照是不是就只需要一个简单的立方体贴图就可以了呢。
  • 那么,我们怎么把他们拆开呢?一个很有趣的思路是,如果我们假设反射方程中的radiance是一个常数,那么我们是否就可以把这一个radiance提取出来了呢?但是,一个需要对半球面积分的radiance怎么可能是常数呢?但是,如果这个常数radiance已经是综合了各个可见方向的radiance之后综合属性呢。对光线而言,所谓综合,就是积分。当然一个需要注意的是,光照积分的结果是irradiance,但是我们希望他保持为一个radiance(正如前面所说),所以需要做归一化,实则上就是基于体积角求均值,但是这只是公式化的表达,在实际实现时并不是这样做的。这样,我们就把积分拆成了光照和BRDF两个部分,跟漫反射部分大同小异了。

\[L_o(p,\omega_o)=\frac{\int_\Omega L_i(p,\omega_i)d\omega_i}{\int_\Omega d\omega_i}\int_\Omega\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)}n\cdot \omega_i d\omega_i \]

1. 预滤波环境贴图

\[\frac{\int_\Omega L_i(p,\omega_i)d\omega_i}{\int_\Omega d\omega_i} \]

我们把第一项称为预滤波环境贴图,这个名字很直观,我们对radiance求积分再取均值不正是在滤波么。

  • 首先,我们要如何采样呢?要注意的是,虽然第一项并没有强调任何的采样倾向,但是我们记得第一项实则是镜面反射光照效果的综合radiance,因此我们需要给他一个采样分布。很显然的是,镜面反射的入射光应当是一个波瓣,而这个波瓣的分布是依赖于入射光的半程向量的分布,也就是法线分布函数NDF。因此,我们使用NDF来采样环境光。
  • 思路自然是使用NDF来采样半程向量,并进一步获得光线方向啦。那么,如何实现随机采样呢?首先,我们可以使用伪随机函数,但是这里我们采用低差异序列,来获得一个均匀分布的随机数。
float RadicalInverse_VdC(uint bits)
{
	bits = (bits << 16u) | (bits >> 16u);
	bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
	bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
	bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
	bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
	return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
  • 进一步的,我们使用一个均匀分布的随机数来获得其他形式的随机分布。思路很简单,我们只需要找到任何分布都存在的一个均匀分布特征,是啥呢?自然就是概率分布函数啦。因此,我们要实现一种随机分布,只需要这样做:找到该分布的概率密度函数pdf,这显然是一个积分为1的函数,然后获得这个pdf的概率分布函数P,这显然是一个0-1的函数,然后使用P表示我们要实现的随机变量,在这里就是光照矢量的坐标了,更进一步说是球坐标中的\(\theta\),对于\(\phi\)我们直接使用均匀采样,而且是不随机的等距采样,因为这没有什么影响。那么,我们的任务就是,小小的研究一下概率密度函数。

  • 要注意的是,虽然NDF是一个分布,但是他并不是概率密度函数,因为他积分不为1。但是NDF在设计时,被要求如下:

\[\int_\Omega D(h)(n\cdot h)dh=1 \]

也就是说,对于球坐标系中\((\theta,\phi)\)而言,概率密度函数是:

\[pdf(\theta,\phi)=D(n\cdot h)=\frac{\alpha^2}{\pi(cos^2\theta_h(\alpha^2-1)+1)^2}cos\theta_hsin\theta_h \]

相应的,对于\(\theta\)而言的pdf,只需要对\(\phi\)做积分既可以了。这二者一个意思,只是提前积分了一次罢了。

\[pdf(\theta)=\int_0^{2\pi}pdf(\theta,\phi)d\phi=D(n\cdot h)=\frac{2\alpha^2}{(cos^2\theta_h(\alpha^2-1)+1)^2}cos\theta_hsin\theta_h \]

然后,我们对pdf做全面积分,获得P。

\[P(\theta)=\int_0^{\theta_h}pdf(\theta_h)d\theta=2\alpha^2(\frac{1}{(2\alpha^4-4\alpha^2+2)cos^2\theta+2\alpha^2-2}-\frac{1}{2\alpha^4-2\alpha^2}) \]

最后,我们表示出来\(cos\theta\)

\[cos\theta=\sqrt\frac{1-P}{(\alpha^2-1)P+1} \]

  • 最后,我们就可以使用0-1的均匀分布随机数来模拟这个分布了;同时,我们对于phi项使用0-2pi的等距采样。
vec2 Hammersley(uint i, uint N)
{
	return vec2(float(i) / float(N), RadicalInverse_VdC(i));
}
\\...
	float a = roughness * roughness;

	//Sampling H vector
	float phi = Xi.x * 2 * PI;
	float cosTheta = sqrt((1.0 - Xi.y) / ((a * a - 1.0) * Xi.y + 1.0));
	float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
  • 拿到了球坐标,再转化成xyz坐标。最后使用TBN变化,变成世界坐标系的坐标;至于TBN的设置不必考虑严格的标准。到这里,我们就可以获得H向量了。
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
	float a = roughness * roughness;

	//Sampling H vector
	float phi = Xi.x * 2 * PI;
	float cosTheta = sqrt((1.0 - Xi.y) / ((a * a - 1.0) * Xi.y + 1.0));
	float sinTheta = sqrt(1.0 - cosTheta * cosTheta);

	vec3 H;
	H.x = sinTheta * cos(phi);
	H.y = sinTheta * sin(phi);
	H.z = cosTheta;

	//TBN convert
	vec3 normal = normalize(N);
	vec3 tangent = normal.z > 0.001 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, 1.0);
	vec3 bitangent = cross(normal, tangent);
	tangent = cross(bitangent, normal);
	mat3 TBN = mat3(tangent, bitangent, normal);
	vec3 sampleVec = normalize(TBN * H);

	return sampleVec;
}
  • 拿到半程向量H的采样之后,就可以获得光照方向L的采样了。在此之前,我们实际上并不知道观察方向V,在这里,我们直接假设V=N的,这很不体面,但是经得起用。
	vec3 N = normalize(localPos);
	vec3 R = N;
	vec3 V = R;
		vec2 Xi = Hammersley(i, SAMPLE_COUNT);
		vec3 H = ImportanceSampleGGX(Xi, N, roughness);
		vec3 L = normalize(2 * dot(V, H) * H - V);
  • 最后一步,就是对光照方向进行采样,然后取均值就可以了,在这里使用LdotN来做加权平均实现均值。
	float totalWeight = 0.0;
	vec3 prefilterColor = vec3(0.0);

	for (uint i = 0u; i != SAMPLE_COUNT; i++)
	{
		vec2 Xi = Hammersley(i, SAMPLE_COUNT);
		vec3 H = ImportanceSampleGGX(Xi, N, roughness);
		vec3 L = normalize(2 * dot(V, H) * H - V);

		float NdotH = max(dot(N, H), 0.0);
		float mipLevel = GetMipLevel(NdotH, roughness);

		float NdotL_origon = dot(N, L);
		float NdotL = max(NdotL_origon, 0.0);
		if (NdotL_origon > 0.0)
		{
			prefilterColor += textureLod(envCubeMap, L, mipLevel).rgb * NdotL;
			totalWeight += NdotL;
		}
	}

	prefilterColor /= totalWeight;
  • 这样我们就完成了预滤波环境贴图的shader了,下面我们就需要做一些准备工作:我们需要实现一个帧缓冲,并为其附加一个立方体贴图,然后分别对六个面渲染预滤波环境贴图,这个流程跟我们的辐照度图一样。但是有一处不同,而这里我们在前面还没有提到,那就是我们的预滤波环境贴图是基于NDF采样得到的,而NDF是roughness的函数,那么显然的不同roughness,我们的预滤波环境贴图是不一样的,roughness越大,我们的贴图会越模糊。因此,我们需要一系列的预滤波环境贴图,我们是通过MIPMAP来实现的。当然了,我们不是在roughness=0.0的贴图上进行mipmap,而是根据0-1的roughness取渲染mip=0-k的mipmap,显然的mipLevel和roughness之间有一个简单的线性映射。
  • 因此,首先,我们在申请帧缓冲时使用glGenerateMipmap(GL_TEXTURE_CUBE_MAP)来分配mipmap空间,然后,我们在渲染时使用两层循环,第一层指定mipLevel,第二层逐面渲染,这里要注意的是我们是在glFramebufferTexture2D的最后一个level参数中指定mipLevel的。当然,我们的roughness会根据mipLevel线性映射求出并使用uniform传入shader中。此外,要注意的是,不同mipLevel的颜色缓冲的大小是不一样的,宽高都是成2的幂次关系的,因此我们还需要基于mipLevel求出宽高,并设置glViewPort、渲染缓冲的空间分配glRenderbufferStorage。此外,要注意开启GL_LINEAR_MIPMAP_LINEAR。
void IBL::GetPrefilterMap()
{
	glGenFramebuffers(1, &prefilterFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, prefilterFBO);

	glGenRenderbuffers(1, &prefilterRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, prefilterRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 128, 128);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, prefilterRBO);

	glGenTextures(1, &prefilterMap);
	glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap);
	for (unsigned int i = 0; i != 6; i++)
	{
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 0, GL_RGB, GL_FLOAT, NULL);
	}
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

	glm::mat4 projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
	glm::mat4 views[] =
	{
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))

	};

	prefilterShader.use();
	prefilterShader.setMat4("projection", glm::value_ptr(projection));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, envCubeMap);
	prefilterShader.setInt("encCubeMap", 0);

	const unsigned int maxMipLevels = 5;
	for (unsigned int mip = 0; mip != maxMipLevels; mip++)
	{
		unsigned int mipWidth = 128 * pow(0.5, mip);
		unsigned int mipHeight = 128 * pow(0.5, mip);

		glBindRenderbuffer(GL_RENDERBUFFER, prefilterRBO);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, prefilterRBO);

		glViewport(0, 0, mipWidth, mipHeight);
		glEnable(GL_DEPTH_TEST);

		float roughness = (float)mip / (float)(maxMipLevels - 1);
		prefilterShader.setFloat("roughness", roughness);

		for (unsigned int i = 0; i != 6; i++)
		{
			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip);
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			prefilterShader.setMat4("view", glm::value_ptr(views[i]));

			glBindVertexArray(cubeVAO);
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(i * 6 * sizeof(unsigned int)));
		}
	}
	glBindFramebuffer(GL_FRAMEBUFFER,0);
}
  • 这样,我们就拿到了贴图,我们来看一下不同粗糙度的情况,可以看出效果不错。


  • 但是存在着两个问题,第一个是在两个立方体面的夹缝非常明显的界限,这是因为两个面之间没有线性插值,解决这个问题很简单:
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);

  • 第二个问题是,在比较亮的物体周围,存在着很多的光斑,这在mip比较高的情况下更是明显。这是因为,我们使用采样,自然就会造成噪声,这里有部分采样点采样到了亮出,并且这种采样在相邻像素之间是不均匀的,那么随着mip层级变高,宽高变小,纹理像素数量变少,那么像素之间的差异就会被更加明显的看出来。对于这个,我们根据采样点的pdf,来对环境贴图的mipmap进行采样,这样如果pdf比较小,说明随机性比较高,更应该mip比较高的模糊纹理上面采样,这样一来就减小了不同像素之间在随机性上的差异,使得过渡更加平滑。我们的思路是这样的:
  • 首先,我们明确一下pdf,对半程向量的pdf我们已经谈过了,但是要注意的是,我们最终实现的采样是光线方向的采样。很显然的是,相对于半程向量的波瓣,光线方向的波瓣一般是宽的多的,这就意味着如果我们仍然使用半程向量的pdf作为光线的pdf,那么pdf的积分就会大于1,因此我们要做调整,一般是按照下面这样子做的。

\[pdf_{L}=\frac{pdf_{H}}{4(l,h)}=\frac{pdf_{H}}{4(v,h)}=\frac{D(n\cdot h)}{4(v\cdot h)} \]

为什么会这样处理呢?我想的不是很清楚,但是可以拿一个特例来解释一下。假设观察方向处于法线方向(事实上,采样时正是如此),然后假设粗糙度\(\alpha=1.0\),那么入射方向与法线的夹角\(\theta_l\)就是半程向量与法线夹角\(\theta_h\)的2倍,那么我们就会有(这里我们对立体角做积分,所以没有正弦项):

\[pdf(\omega)=\frac{\alpha^2}{\pi(cos^2\theta_h(\alpha^2-1)+1)^2}cos\theta_h =\frac{1}{\pi}cos\theta \]

\[pdf(\theta_h)=\frac{2\alpha^2}{(cos^2\theta_h(\alpha^2-1)+1)^2}cos\theta_hsin\theta_h =2cos\theta_hsin\theta_h \]

显然,积分结果是等于1没问题的。但是如果是光线入射方向分布的话:

\[\int_0^{\pi}pdf(\theta_l/2)d\theta_l=\int_0^{\pi}2cos\frac{\theta_l}{2}sin\frac{\theta_l}{2}d\theta_l=2.0 \]

显然,积分结果大于1,至于为什么积分范围选在了全方位呢,因为虽然这些光照不进来,但是该采还是得采。然后,我们对pdf做一下上述的变换,需要注意到在我们的假设条件下,(h,n)==(h,v):

\[pdf_l(\theta_h)=\frac{pdf_h(\theta_h)}{4(h,v)}=\frac{sin\theta_h}{2} \]

此时,我们的积分结果是:

\[\int_0^{\pi}pdf_l(\theta_l/2)d\theta_l=\int_0^{\pi}0.5sin\frac{\theta_l}{2}d\theta_l=1.0 \]

这样看来,我们的pdf的变换是在特殊条件下合理的。

  • 接下来,我们要讨论的是,如何按照pdf选择合适的mipmap层级呢?显然的是,pdf越小的采样位置,对噪声的效果越强,我们越是希望他采到更模糊的mipmap图。我们希望是这样子的:按照某个pdf值采到的采样点,我们希望它在某一个级别的map上,对于该mipmap的一个像素而言,该像素对应的立体角积分离散采样值,恰好是单位立体角;也就是说,我们总是期待每一次采样都能够采到单位立体角,这样就可以减少噪声。即:
    首先,mip为0的图每个采样像素的立体角是,

\[saTexel=\frac{4\pi}{6\times resolution^2} \]

考虑到mipmap之后,mip级的一个像素的立体角是:

\[2^{2\times mip}\times saTexel \]

然后,某一个pdf对应到的蒙特卡洛采样的系数,换言之,一次采样的立体角的蒙特卡洛离散值是

\[saSample = \frac{1}{SampleCount\times pdf} \]

那么可有:

\[2^{2\times mip}\times saTexel=saSample \]

最终得到:

\[mip=\frac{1}{2}log_2(\frac{saSample}{saTexel}) \]

  • 到这里,我们就拿到了如何按照pdf获取对应的mip啦!
float GetMipLevel(float NdotH, float roughness)
{
	float HdotV = NdotH;
	float pdf = DistributionGGX(NdotH, roughness) * NdotH / (4.0 * HdotV) + 0.0001;

	float resolution = 512.0;
	float saTexel = 4.0 * PI / (6 * resolution * resolution);
	float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);

	float mip = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel);
	return mip;
}
  • 画个图看一下,果然没有光斑了,完美!
#version 330 core

in vec3 localPos;
out vec4 fragColor;

uniform samplerCube envCubeMap;
uniform float roughness;

const float PI = 3.14159265359;
const uint SAMPLE_COUNT = 1024u;

float RadicalInverse_VdC(uint bits);
vec2 Hammersley(uint i, uint N);
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);
float DistributionGGX(float NdotH, float roughness);
float GetMipLevel(float NdotH, float roughness);

void main()
{
	vec3 N = normalize(localPos);
	vec3 R = N;
	vec3 V = R;

	float totalWeight = 0.0;
	vec3 prefilterColor = vec3(0.0);

	for (uint i = 0u; i != SAMPLE_COUNT; i++)
	{
		vec2 Xi = Hammersley(i, SAMPLE_COUNT);
		vec3 H = ImportanceSampleGGX(Xi, N, roughness);
		vec3 L = normalize(2 * dot(V, H) * H - V);

		float NdotH = max(dot(N, H), 0.0);
		float mipLevel = GetMipLevel(NdotH, roughness);

		float NdotL_origon = dot(N, L);
		float NdotL = max(NdotL_origon, 0.0);
		if (NdotL_origon > 0.0)
		{
			prefilterColor += textureLod(envCubeMap, L, mipLevel).rgb * NdotL;
			totalWeight += NdotL;
		}
	}

	prefilterColor /= totalWeight;

	fragColor = vec4(prefilterColor, 1.0);
}


float RadicalInverse_VdC(uint bits)
{
	bits = (bits << 16u) | (bits >> 16u);
	bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
	bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
	bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
	bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
	return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}

vec2 Hammersley(uint i, uint N)
{
	return vec2(float(i) / float(N), RadicalInverse_VdC(i));
}

vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
	float a = roughness * roughness;

	//Sampling H vector
	float phi = Xi.x * 2 * PI;
	float cosTheta = sqrt((1.0 - Xi.y) / ((a * a - 1.0) * Xi.y + 1.0));
	float sinTheta = sqrt(1.0 - cosTheta * cosTheta);

	vec3 H;
	H.x = sinTheta * cos(phi);
	H.y = sinTheta * sin(phi);
	H.z = cosTheta;

	//TBN convert
	vec3 normal = normalize(N);
	vec3 tangent = normal.z > 0.001 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, 1.0);
	vec3 bitangent = cross(normal, tangent);
	tangent = cross(bitangent, normal);
	mat3 TBN = mat3(tangent, bitangent, normal);
	vec3 sampleVec = normalize(TBN * H);

	return sampleVec;
}

float DistributionGGX(float NdotH, float roughness)
{
	float a = roughness * roughness;
	float a2 = a * a;

	float NdotH2 = NdotH * NdotH;

	float nom = a2;
	float denom = NdotH2 * (a2 - 1.0) + 1.0;
	denom = PI * denom * denom;

	return nom / denom;
}

float GetMipLevel(float NdotH, float roughness)
{
	float HdotV = NdotH;
	float pdf = DistributionGGX(NdotH, roughness) * NdotH / (4.0 * HdotV) + 0.0001;

	float resolution = 512.0;
	float saTexel = 4.0 * PI / (6 * resolution * resolution);
	float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);

	float mip = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel);
	return mip;
}

2. BRDF积分贴图

我们已经拿到预滤波环境贴图啦,下面只需要计算BRDF积分贴图就可以实现PBR了。

\[\int_\Omega\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)}n\cdot \omega_i d\omega_i \]

我们首先需要整理一下这个式子,还记得我们的DFG表达式不:

\[D=\frac{\alpha^2}{\pi(cos^2\theta_h(\alpha^2-1)+1)^2} \]

\[F=F_0+(1-F_0)(1-(h\cdot v))^5 \]

\[G=\frac{k}{(n\cdot v)(1-k)+k} \]

可以看出来,我们的BRDF积分跟roughnes、F0、ndotv、hdotv、hdotn等有关,其中有关h的后两者是积分的内部变量,因此,一个BRDF积分实则是roughnes、F0、ndotv三个参数的查值表。然而,实际上不止三个参数,因为F0是三维的,所以这并不是一个容易表示的纹理,因此我们将F0拆出去,将其变成roughness和ndotv的纹理,一个很有趣的事情是这两个参数都是0-1的,因此直接拿来做纹理坐标就很合适了。

\[\int_\Omega\frac{DFG}{4(n\cdot \omega_o)(n\cdot \omega_i)}n\cdot \omega_i d\omega_i = \int_\Omega\frac{DFG}{4(n\cdot \omega_o)}d\omega_i =F_0\int_\Omega\frac{DG}{4(n\cdot \omega_o)}(1-(1-(h\cdot v))^5)d\omega_i+\int_\Omega\frac{DG}{4(n\cdot \omega_o)}(1-(h\cdot v))^5d\omega_i \]

可见,我们只需要将两部分积分变成两个纹理值,分别是F0的系数和另一个偏置项,然后放在一张图上就可以了。下面,我们重点关注积分中BRDF还剩下的部分:

\[\int_\Omega\frac{DG}{4(n\cdot \omega_o)}d\omega_i \]

还记得我们光照方向采样的pdf吗:

\[pdf_l=\frac{D(n\cdot h)}{4(h\cdot v)} \]

这样一来,我们就获得了蒙特卡洛积分的离散表达式了:

\[\frac{1}{nrSamples}\sum \frac{\frac{DG}{4(n\cdot v)}}{\frac{D(n\cdot h)}{4(h\cdot v)}}=\frac{1}{nrSamples}\sum \frac{G (h\cdot v)}{(n\cdot v)(n\cdot h)} \]

很自然地,两个值就是:

\[\frac{1}{nrSamples}\sum \frac{G (h\cdot v)}{(n\cdot v)(n\cdot h)}(1-(1-(h\cdot v))^5) \]

\[\frac{1}{nrSamples}\sum \frac{G (h\cdot v)}{(n\cdot v)(n\cdot h)}(1-(h\cdot v))^5 \]

  • 实现方法也很简单,我们去绘制一个四边形,并给他四个顶点的纹理坐标分别是2D纹理的四个顶点坐标,然后拿着纹理坐标作为roghness和ndotv两个参数作为片段的属性,然后在片段着色器中使用之前的重要性采样方法获得半程向量,当然我们需要根据ndotv来反向解出一个v矢量,以便计算vdoth,这样我们的计算所需就实现了。
#version 330 core

in vec2 texCoord;
out vec2 fragColor;

float RadicalInverse_VdC(uint bits);
vec2 Hammersley(uint i, uint N);
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec2 IntegrateBRDF(float NdotV, float roughness);

const float PI = 3.14159265359;

void main()
{
	vec2 integrateBRDF = IntegrateBRDF(texCoord.x, texCoord.y);
	fragColor = integrateBRDF;
}

float RadicalInverse_VdC(uint bits)
{
	bits = (bits << 16u) | (bits >> 16u);
	bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
	bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
	bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
	bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
	return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}

vec2 Hammersley(uint i, uint N)
{
	return vec2(float(i) / float(N), RadicalInverse_VdC(i));
}

vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
	float a = roughness * roughness;

	//Sampling H vector
	float phi = Xi.x * 2 * PI;
	float cosTheta = sqrt((1.0 - Xi.y) / ((a * a - 1.0) * Xi.y + 1.0));
	float sinTheta = sqrt(1.0 - cosTheta * cosTheta);

	vec3 H;
	H.x = sinTheta * cos(phi);
	H.y = sinTheta * sin(phi);
	H.z = cosTheta;

	//TBN convert
	vec3 normal = normalize(N);
	vec3 tangent = normal.z > 0.001 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, 1.0);
	vec3 bitangent = cross(normal, tangent);
	tangent = cross(bitangent, normal);
	mat3 TBN = mat3(tangent, bitangent, normal);
	vec3 sampleVec = normalize(TBN * H);

	return sampleVec;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
	float r = roughness;
	float k = r * r / 2.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;
}

vec2 IntegrateBRDF(float NdotV, float roughness)
{
	vec3 N = vec3(0.0, 0.0, 1.0);
	vec3 V;
	V.x = sqrt(1.0 - NdotV * NdotV);
	V.y = 0.0;
	V.z = NdotV;

	float A = 0.0;
	float B = 0.0;
	const uint SAMPLE_COUNT = 1024u;
	for (uint i = 0u; i != SAMPLE_COUNT; i++)
	{
		vec2 Xi = Hammersley(i, SAMPLE_COUNT);
		vec3 H = ImportanceSampleGGX(Xi, N, roughness);
		vec3 L = normalize(2.0 * dot(V, H) * H - V);

		float HdotV = max(dot(H, V), 0.0);
		float NdotH = max(H.z, 0.0);

		float NdotL = L.z;
		if (NdotL > 0.0)
		{
			float G = GeometrySmith(N, V, L, roughness);
			float G_vis = G * HdotV / (NdotV * NdotH);
			float Fc = pow(1.0 - HdotV, 5.0);
			A += G_vis * (1.0 - Fc);
			B += G_vis * Fc;
		}
	}

	A /= float(SAMPLE_COUNT);
	B /= float(SAMPLE_COUNT);

	return vec2(A, B);
}
  • 帧缓冲部分并无新意,只是我们需要一个GL_RG16F的纹理格式
void IBL::GetPrebrdfMap()
{
	glGenFramebuffers(1, &prebrdfFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, prebrdfFBO);

	glGenRenderbuffers(1, &prebrdfRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, prebrdfRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, prebrdfRBO);

	glGenTextures(1, &prebrdfMap);
	glBindTexture(GL_TEXTURE_2D, prebrdfMap);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, prebrdfMap, 0);
	
	glViewport(0, 0, 512, 512);
	glEnable(GL_DEPTH_TEST);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	prebrdfShader.use();

	glBindVertexArray(frameVAO);
	glDrawArrays(GL_TRIANGLES, 0, 6);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
  • 我们把纹理画出来,完美!

3. 最后一步,我们的IBL全局光照!

除了之前的diffuse部分之外,我们还需要从预滤波环境贴图和BRDF积分贴图中采样,并按照之前的拆分了F0的公式来计算镜面反射光照。有几点需要注意:

  • 首先,我们将公式中的F0使用考虑了粗糙度之后计算得到的F来代替;
  • 然后,与漫反射的辐照度图不同,预滤波环境贴图并不是使用法向矢量来查询,而是使用观察方向reflect得到的反射方向R来查值,这个思路很清晰,因为漫反射是各向同性的,所以他的光照来源是沿法向且没有进行重要性采样的,但是镜面反射是各向异性的,半程向量是有波瓣分布的,那么对于一个观察方向而言,其入射光的采样显然是沿着R反射方向的一个波瓣分布。
  • 其次,我们需要按照粗糙度来线性映射mipLevel,这个也是很熟悉的了,只要设置我们在之前渲染预滤波环境贴图的最大mipLevel就可以反向获得这个线性映射关系了。
  • 最后,我们查值BRDF积分贴图,然后计算就可以了。
	//ambient
	vec3 F0 = vec3(0.04);
	F0 = mix(F0, albedo, metalness);
	float NdotV = max(dot(N, V), 0.0);
	vec3 F = FresnelSchlickRoughness(NdotV, F0, roughness);

	const float MAX_REFLECTION_LOD = 4.0;
	vec3 R = reflect(-V, N);
	vec3 preFilterColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
	vec2 preBRDF = texture(prebrdfMap, vec2(NdotV, roughness)).rg;
	vec3 specular = preFilterColor * (F * preBRDF.x + preBRDF.y);

	vec3 kS = F;
	vec3 kD = 1 - kS;
	kD *= 1.0 - metalness;
	vec3 irradiance = texture(irradianceMap, N).rgb;
	vec3 diffuse = kD * (albedo / PI) * irradiance;

	vec3 ambient = (diffuse + specular) * ao;

完美!

  • 同样的,我们把IBL集合称为一个类:
#ifndef IBL_H
#define IBL_H

#include <glad/glad.h> 
#include "shader.h"
#include "image.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <string>
using std::string;

class IBL {
public:
	IBL() = default;
	IBL(string path);
	unsigned int HDRMap()
	{
		return HDR;
	}
	unsigned int CubeMap()
	{
		return envCubeMap;
	}
	unsigned int IrradianceMap()
	{
		return irradianceMap;
	}
	unsigned int PrefilterMap()
	{
		return prefilterMap;
	}
	unsigned int PrebrdfMap()
	{
		return prebrdfMap;
	}
private:
	Shader envCubeShader = Shader("res/shader/envCubeVertex.shader", "res/shader/envCubeFragment.shader");
	Shader irradianceShader = Shader("res/shader/irradianceVertex.shader", "res/shader/irradianceFragment.shader");
	Shader prefilterShader = Shader("res/shader/prefilterVertex.shader", "res/shader/prefilterFragment.shader");
	Shader prebrdfShader = Shader("res/shader/prebrdfVertex.shader", "res/shader/prebrdfFragment.shader");
	void GetVertexArray_Frame();
	void GetVertexArray_Cube();
	unsigned int cubeVAO, cubeVBO, cubeEBO;
	unsigned int frameVAO, frameVBO;
	void GetVertexArray();
	unsigned int HDR;
	void GetHDR(string path);
	unsigned int envCubeFBO, envCubeRBO, envCubeMap;
	void GetCubeMap();
	unsigned int irradianceFBO, irradianceRBO, irradianceMap;
	void GetIrradianceMap();
	unsigned int prefilterFBO, prefilterRBO, prefilterMap;
	void GetPrefilterMap();
	unsigned int prebrdfFBO, prebrdfRBO, prebrdfMap;
	void GetPrebrdfMap();
};

IBL::IBL(string path)
{
	GetVertexArray();
	GetHDR(path);
	GetCubeMap();
	GetIrradianceMap();
	GetPrefilterMap();
	GetPrebrdfMap();
}

void IBL::GetHDR(string path)
{
	stbi_set_flip_vertically_on_load(true);
	int width, height, nrChannels;
	float* data = stbi_loadf(path.c_str(), &width, &height, &nrChannels, 0);
	if (data)
	{
		glGenTextures(1, &HDR);
		glBindTexture(GL_TEXTURE_2D, HDR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data);
	}
	else
	{
		std::cout << "Failed to load HDR " + path << std::endl;
	}
	stbi_image_free(data);
}

void IBL::GetCubeMap()
{
	glGenFramebuffers(1, &envCubeFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, envCubeFBO);

	glGenRenderbuffers(1, &envCubeRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, envCubeRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, envCubeRBO);

	glGenTextures(1, &envCubeMap);
	glBindTexture(GL_TEXTURE_CUBE_MAP, envCubeMap);
	for (unsigned int i = 0; i != 6; i++)
	{
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, NULL);
	}
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


	glm::mat4 projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
	glm::mat4 views[] =
	{
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))

	};

	envCubeShader.use();
	envCubeShader.setMat4("projection", glm::value_ptr(projection));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, HDR);
	envCubeShader.setInt("equirectangularMap", 0);

	glViewport(0, 0, 512, 512);
	glEnable(GL_DEPTH_TEST);

	for (unsigned int i = 0; i != 6; i++)
	{
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubeMap, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		envCubeShader.setMat4("view", glm::value_ptr(views[i]));
	
		glBindVertexArray(cubeVAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(i * 6 * sizeof(unsigned int)));
	}

	glBindTexture(GL_TEXTURE_CUBE_MAP, envCubeMap);
	glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}


void IBL::GetIrradianceMap()
{
	glGenFramebuffers(1, &irradianceFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, irradianceFBO);

	glGenRenderbuffers(1, &irradianceRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, irradianceRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, irradianceFBO);

	glGenTextures(1, &irradianceMap);
	glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap);
	for (unsigned int i = 0; i != 6; i++)
	{
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, NULL);
	}
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	glm::mat4 projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
	glm::mat4 views[] =
	{
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))

	};

	irradianceShader.use();
	irradianceShader.setMat4("projection", glm::value_ptr(projection));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, envCubeMap);
	irradianceShader.setInt("encCubeMap", 0);

	glViewport(0, 0, 32, 32);
	glEnable(GL_DEPTH_TEST);

	for (unsigned int i = 0; i != 6; i++)
	{
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		irradianceShader.setMat4("view", glm::value_ptr(views[i]));

		glBindVertexArray(cubeVAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(i * 6 * sizeof(unsigned int)));
	}

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void IBL::GetPrefilterMap()
{
	glGenFramebuffers(1, &prefilterFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, prefilterFBO);

	glGenRenderbuffers(1, &prefilterRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, prefilterRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 128, 128);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, prefilterRBO);

	glGenTextures(1, &prefilterMap);
	glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap);
	for (unsigned int i = 0; i != 6; i++)
	{
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 0, GL_RGB, GL_FLOAT, NULL);
	}
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

	glm::mat4 projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
	glm::mat4 views[] =
	{
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
		   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))

	};

	prefilterShader.use();
	prefilterShader.setMat4("projection", glm::value_ptr(projection));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, envCubeMap);
	prefilterShader.setInt("encCubeMap", 0);

	const unsigned int maxMipLevels = 5;
	for (unsigned int mip = 0; mip != maxMipLevels; mip++)
	{
		unsigned int mipWidth = 128 * pow(0.5, mip);
		unsigned int mipHeight = 128 * pow(0.5, mip);

		glBindRenderbuffer(GL_RENDERBUFFER, prefilterRBO);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, prefilterRBO);

		glViewport(0, 0, mipWidth, mipHeight);
		glEnable(GL_DEPTH_TEST);

		float roughness = (float)mip / (float)(maxMipLevels - 1);
		prefilterShader.setFloat("roughness", roughness);

		for (unsigned int i = 0; i != 6; i++)
		{
			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip);
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			prefilterShader.setMat4("view", glm::value_ptr(views[i]));

			glBindVertexArray(cubeVAO);
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(i * 6 * sizeof(unsigned int)));
		}
	}
	glBindFramebuffer(GL_FRAMEBUFFER,0);
}

void IBL::GetPrebrdfMap()
{
	glGenFramebuffers(1, &prebrdfFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, prebrdfFBO);

	glGenRenderbuffers(1, &prebrdfRBO);
	glBindRenderbuffer(GL_RENDERBUFFER, prebrdfRBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, prebrdfRBO);

	glGenTextures(1, &prebrdfMap);
	glBindTexture(GL_TEXTURE_2D, prebrdfMap);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, prebrdfMap, 0);
	
	glViewport(0, 0, 512, 512);
	glEnable(GL_DEPTH_TEST);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	prebrdfShader.use();

	glBindVertexArray(frameVAO);
	glDrawArrays(GL_TRIANGLES, 0, 6);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void IBL::GetVertexArray()
{
	GetVertexArray_Cube();
	GetVertexArray_Frame();
}

void IBL::GetVertexArray_Cube()
{
	float vertices[] = {
			0.5f, -0.5f, -0.5f,
			0.5f, 0.5f, -0.5f,
			 0.5f, 0.5f,  0.5f,
			 0.5f, -0.5f,  0.5f,

			-0.5f, -0.5f, -0.5f,
			-0.5f, 0.5f, -0.5f,
			 -0.5f, 0.5f,  0.5f,
			 -0.5f, -0.5f,  0.5f,

			-0.5f, 0.5f,-0.5f,
			-0.5f, 0.5f, 0.5f,
			 0.5f, 0.5f, 0.5f,
			0.5f, 0.5f, -0.5f,

			-0.5f, -0.5f,-0.5f,
			-0.5f, -0.5f, 0.5f,
			 0.5f, -0.5f, 0.5f,
			 0.5f, -0.5f, -0.5f,

			 -0.5f, -0.5f, 0.5f,
			 0.5f, -0.5f, 0.5f, 
			 0.5f,  0.5f, 0.5f, 
			-0.5f,  0.5f, 0.5f, 

			-0.5f, -0.5f, -0.5f,
			0.5f, -0.5f, -0.5f, 
			 0.5f,  0.5f, -0.5f,
			-0.5f,  0.5f, -0.5f
	};
	unsigned int indices[] = {
		0, 1, 2, // 第一个三角形
		2, 3, 0,  // 第二个三角形

		6, 5, 4,
		4, 7, 6,

		8, 9, 10,
		10, 11, 8,

		14 ,13, 12,
		12, 15, 14,

		16, 17, 18,
		18, 19, 16,

		22, 21, 20,
		20, 23, 22
	};

	glGenBuffers(1, &cubeVBO);
	glGenBuffers(1, &cubeEBO);
	glGenVertexArrays(1, &cubeVAO);

	glBindVertexArray(cubeVAO);
	glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeEBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	glBindVertexArray(0);

}

void IBL::GetVertexArray_Frame()
{
	float frameVertices[] =
	{
		-1.0, -1.0,  0.0, 0.0,
		 1.0, -1.0,  1.0, 0.0,
		 1.0,  1.0,  1.0, 1.0,
		 1.0,  1.0,  1.0, 1.0,
		-1.0,  1.0,  0.0, 1.0,
		-1.0, -1.0,  0.0, 0.0
	};

	glGenBuffers(1, &frameVBO);
	glGenVertexArrays(1, &frameVAO);

	glBindVertexArray(frameVAO);
	glBindBuffer(GL_ARRAY_BUFFER, frameVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(frameVertices), frameVertices, GL_STATIC_DRAW);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
	glEnableVertexAttribArray(1);
	glBindVertexArray(0);
}
#endif

最后,给大家一张合影,可以看出,在摄影方面我是有点水准的。

posted @ 2023-05-26 22:44  ETHERovo  阅读(45)  评论(0编辑  收藏  举报