• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Learn OpenGL 笔记7.4 PBR-Specular IBLImage based lighting-特殊的图像的照明

武飞扬头像
姜姜的奇妙冒险[Unity游戏前端]
帮助1

在上一章中,我们通过预先计算辐照度贴图作为照明的间接漫反射部分,将 PBR 与基于图像的照明相结合。 在本章中,我们将关注反射方程的specular part镜面反射部分:

学新通

您会注意到 Cook-Torrance 镜面反射部分(乘以 ks)在积分上不是恒定的,它取决于入射光方向,还取决于入射视图方向。尝试求解所有入射光方向(包括所有可能的视图方向)的积分是一种combinatorial overload组合过载,并且对于实时计算而言过于昂贵。 Epic Games 提出了一种解决方案,他们能够为pre-convolute the specular part for real time purposes实时目的对镜面反射部分进行预卷积,但需要做出一些妥协,称为split sum approximation拆分和近似

split sum approximation拆分和近似将反射方程的镜面反射部分拆分为两个独立的部分,我们可以分别对它们进行卷积,然后在 PBR 着色器中组合这些部分以用于基于镜面间接图像的照明。与我们对辐照度图进行预卷积的方式类似,拆分和近似需要一个 HDR 环境图作为其卷积输入。为了理解拆分和近似,我们将再次查看反射方程,但这次关注镜面反射部分:

学新通

 出于与 irradiance convolution辐照度卷积相同(性能)的原因,我们无法实时解决积分的镜面反射部分并期望得到合理的性能。 所以最好我们预先计算这个积分以获得类似镜面反射 IBL 贴图的东西,用片段的法线对这张贴图进行采样,然后完成它。 但是,这有点棘手。 我们能够预先计算辐照度图,因为积分仅取决于 ωi,我们可以move the constant diffuse albedo terms out of the integral将恒定漫反射反照率项移出积分。 这一次,积分不仅仅取决于 ωi,从 BRDF 可以看出:

学新通

积分也取决于 wo,我们不能真正对具有两个方向向量的预先计算的立方体贴图进行采样。 如前一章所述,位置 p 在这里无关紧要。 为 ωi 和 ωo 的每个可能组合预先计算这个积分在实时设置中是不切实际的。

Epic Games 的拆分和近似通过将预计算拆分为 2 个单独的部分来解决该问题,我们稍后可以将它们组合起来以获得我们所追求的预计算结果。 拆分和近似将镜面反射积分拆分为两个单独的积分:(简单的平移变换)

 学新通

1.pre-filtered environment map:

 第一部分(卷积时)称为pre-filtered environment map预过滤环境图,它(类似于 irradiance map辐照度图)是预先计算的environment convolution map环境卷积图,但这次考虑了粗糙度。 为了增加粗糙度,环境贴图与更多分散的样本向量进行卷积,产生更模糊的反射。 对于我们卷积的每个粗糙度级别,我们将顺序模糊的结果存储在预过滤贴图的 mipmap 级别中。 例如,在其 5 个 mipmap 级别中存储 5 个不同roughness values粗糙度值的预卷积结果的预过滤环境贴图

学新通

 我们使用 Cook-Torrance BRDF 的正态分布函数 (NDF,normal distribution function,DFG中的D) 生成样本向量及其散射量,该函数将法线和视图方向作为输入。 由于我们在对环境贴图进行卷积时事先不知道view观察者视图方向,因此 Epic Games 通过 assuming the view direction (and thus the specular reflection direction) to be equal to the output sample direction ωo,假设观察者view视图方向(以及镜面反射方向)等于输出样本方向 ωo 来进一步近似。 这会将其自身转换为以下代码:

  1.  
    vec3 N = normalize(w_o);
  2.  
    vec3 R = N;
  3.  
    vec3 V = R;

这样,预过滤的环境卷积不需要知道视图方向。 这确实意味着当从下图所示的角度观察镜面反射时,我们不会得到很好的grazing specular reflections掠射镜面反射(courtesy of the Moving Frostbite to PBR article); 然而,这通常被认为是可接受的折衷方案: 

学新通

 2. BRDF integration map

拆分和方程的第二部分等于镜面积分的 BRDF 部分。 如果我们假设每个方向的入射辐射都是完全白色的(因此 L(p,x)=1.0),我们可以在给定输入粗糙度和法线 n 与光方向 ωi 或 n 之间的输入角度的情况下预先计算 BRDF 的响应 ⋅ωiEpic Games 将pre-computed BRDF's response to each normal and light direction combination将预先计算的BRDF 对每个法线和光方向组合的响应存储在称为  BRDF integration map 集成图2D lookup texture  (LUT) 中的不同粗糙度值上2D lookup texture scale(红色)bias value(绿色)输出到表面的菲涅耳响应,从而为我们提供分割镜面反射积分的第二部分:

学新通

 (这个图,横坐标是,normal 和light direction的结合,而从坐标是粗糙度,查找pre-computed BRDF's response的信息)

我们通过将平面的水平纹理坐标(范围在 0.0 和 1.0 之间)作为 BRDF 的输入 n⋅ωi 并将其垂直纹理坐标作为输入粗糙度值来生成查找纹理。 有了这个 BRDF integration map and the pre-filtered environment map,我们可以将两者结合起来得到镜面积分的结果

  1.  
    //根据粗糙度,获取纵坐标
  2.  
    float lod = getMipLevelFromRoughness(roughness);
  3.  
    //根据反射角度,和粗糙度,获取prefiltered图中的颜色
  4.  
    vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
  5.  
    // 根据 normal 和light direction的结合 以及 粗糙度,获取Fresnel response的第二部分
  6.  
    vec2 envBRDF = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy;
  7.  
    //套公式
  8.  
    vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x envBRDF.y)

这应该让您大致了解 Epic Games 的拆分和近似如何大致接近反射率方程的间接镜面反射部分。 现在让我们尝试自己构建预卷积部分。

3.Pre-filtering an HDR environment map (预过滤 一张HDR 环境贴图)

Pre-filtering an environment map预过滤环境贴图与我们convoluted an irradiance map卷积辐照度贴图的方式非常相似。 不同之处在于我们现在考虑了粗糙度并在预过滤贴图的 mip 级别中顺序存储更粗糙的反射。

首先,我们需要生成一个新的立方体贴图来保存预过滤的环境贴图数据。 为了确保我们为其 mip 级别分配足够的内存,我们调用 glGenerateMipmap 作为分配所需内存量的简单方法:

  1.  
    unsigned int prefilterMap;
  2.  
    glGenTextures(1, &prefilterMap);
  3.  
    glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap);
  4.  
    for (unsigned int i = 0; i < 6; i)
  5.  
    {
  6.  
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X i, 0, GL_RGB16F, 128, 128, 0, GL_RGB, GL_FLOAT, nullptr);
  7.  
    }
  8.  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  9.  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  10.  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
  11.  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  12.  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  13.  
     
  14.  
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

请注意,因为我们计划对 prefilterMap 的 mipmap 进行采样,您需要确保将其缩小过滤器设置为 GL_LINEAR_MIPMAP_LINEAR 以启用trilinear filtering。 我们将预过滤的镜面反射存储在其基本 mip 级别的 128 x 128 的每面分辨率中。 这对于大多数反射来说可能已经足够了,但是如果您有大量光滑的材质(想想汽车反射),您可能需要提高分辨率。

在上一章中,我们通过使用球坐标生成均匀分布在半球 Ω 上的样本向量来对环境图进行卷积。 虽然这对于辐照度来说效果很好,但对于镜面反射来说效率较低。 When it comes to specular reflections, based on the roughness of a surface, the light reflects closely or roughly around a reflection vector r over a normal n, but (unless the surface is extremely rough) around the reflection vector nonetheless当涉及镜面反射时,基于表面的粗糙度,光线在法线 n 上的反射矢量 r 附近或粗略地反射,但(除非表面非常粗糙)仍然围绕反射矢量发散

学新通

可能的出射光反射的一般形状称为specular lobe镜面波瓣。 随着粗糙度的增加,specular lobe镜面波瓣镜面波瓣的尺寸增加; 并且镜面反射瓣的形状会随着入射光方向的变化而变化。 因此,镜面波瓣的形状高度依赖于材料。

当涉及到微表面模型时,我们可以将镜面反射瓣想象为在给定一些入射光方向的情况下围绕微平面中间向量的反射方向。 鉴于大多数光线最终在微平面中间向量周围反射的镜面波瓣中结束,因此以类似的方式生成样本向量是有意义的,否则大多数会被浪费。 这个过程(以specular lobe生成样本)称为importance sampling重要性抽样

3.1 Monte Carlo integration and importance sampling (蒙特卡洛积分和重要性抽样)

为了完全掌握importance sampling重要性抽样,我们首先深入研究称为Monte Carlo integration蒙特卡洛积分的数学结构。蒙特卡洛积分主要围绕统计和概率论的结合。蒙特卡罗帮助我们离散地解决计算人口的一些统计数据或价值的问题,而不必考虑所有人口。

例如,假设您要计算一个国家所有公民的平均身高。为了得到你的结果,你可以测量每个公民并平均他们的身高,这将为你提供你正在寻找的确切答案。然而,由于大多数国家都有相当多的人口,这不是一个现实的方法:这将花费太多的精力和时间。

另一种方法是从该人群中选择一个更小的完全随机(无偏)子集,测量他们的身高,然后平均结果。这个人口可能只有 100 人。虽然不如确切答案准确,但您会得到一个相对接近基本事实的答案。这被称为大数定律。这个想法是,如果你从总人口中测量一组较小的真正随机样本的 N 组,结果将相对接近真实答案,并随着样本数量 N 的增加而变得更接近。

蒙特卡洛积分建立在law of large numbers大数定律的基础上,并采用相同的方法求解积分。与其求解所有可能的(理论上无限的)样本值 x 的积分,不如简单地生成从总人口和平均值中随机挑选的 N 个样本值。随着 N 的增加,我们保证得到更接近积分的确切答案的结果:

(就是池塘抓鱼做记号放回去,再抓,用来估算池塘鱼数量的那个方法)

学新通

为了求解积分,我们在总体 a 到 b 上抽取 N 个随机样本,将它们加在一起,然后除以样本总数来平均它们。 pdf 代表probability density function概率密度函数,它告诉我们特定样本在整个样本集中出现的概率。 例如,人口高度的 pdf 看起来有点像这样:

 (如果我们要估算人口数量,那么我专门找1.65米的人来进行池塘算法就行,速度最快,效率最高,1.65重要性最高)学新通

从这张图中我们可以看到,如果我们从人口中随机抽取样本,那么选择身高 1.70 的人的样本的可能性更高,而样本身高 1.50 的概率较低。

例如,我们可以对称为 low-discrepancy sequences低差异序列的东西进行蒙特卡洛积分,它仍然会生成随机样本,但每个样本分布更均匀(图片由 James Heald 提供): 
学新通

pseudorandom:伪随机    low-discrepancy sequences低差异序列

当使用low-discrepancy sequences低差异序列生成蒙特卡洛样本向量时,该过程称为Quasi-Monte Carlo integration准蒙特卡洛积分。 Quasi-Monte Carlo 方法具有更快的rate of convergence收敛速度,这使得它们对性能要求高的应用很感兴趣。

鉴于我们新获得的关于蒙特卡洛和准蒙特卡洛积分的知识,我们可以使用一个有趣的特性来实现更快的收敛速度,称为 importance sampling重要性采样。我们在本章之前已经提到过,但是当涉及到光的镜面反射时,反射光矢量被限制在镜面叶中,其大小由表面的粗糙度决定。将任何(准)随机生成的样本视为镜面叶外的任何(准)随机生成的样本与镜面积分无关,因此将样本生成集中在镜面叶内(重要性更高)是有意义的,但代价是使蒙特卡洛估计器有偏差。

这实质上就是importance sampling重要性采样的意义所在:在某些区域中生成样本向量,该区域受围绕microfacet's halfway vector微平面的中途向量的粗糙度约束。通过将准蒙特卡罗采样与低差异序列相结合,并使用重要性采样对样本向量进行偏置,我们获得了很高的convergence收敛速度。因为我们以更快的速度获得解决方案,所以我们将需要更少的样本来达到足够的近似值。

 (百度的重要性采样,就是蒙特卡洛函数,增加一个重要性函数,根据重要性,来进行采样)

3.2 A low-discrepancy sequence 低差异序列 

在本章中,我们将pre-compute the specular portion of the indirect reflectance equation using importance sampling given a random low-discrepancy sequence based on the Quasi-Monte Carlo method使用基于准蒙特卡罗方法的随机低差异序列,使用重要性采样预先计算间接反射率方程的镜面部分。 我们将使用的序列被称为Hammersley Sequence序列,正如 Holger Dammertz.仔细描述的那样。 Hammersley 序列基于 Van Der Corput 序列,该序列在其小数点周围镜像十进制二进制表示。

给定一些巧妙的技巧,我们可以在着色器程序中非常有效地生成 Van Der Corput 序列,我们将使用该程序在 N 个总样本上获得 Hammersley 序列样本 i:

  1.  
    float RadicalInverse_VdC(uint bits)
  2.  
    {
  3.  
    bits = (bits << 16u) | (bits >> 16u);
  4.  
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
  5.  
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
  6.  
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
  7.  
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
  8.  
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
  9.  
    }
  10.  
    // ----------------------------------------------------------------------------
  11.  
    vec2 Hammersley(uint i, uint N)
  12.  
    {
  13.  
    return vec2(float(i)/float(N), RadicalInverse_VdC(i));
  14.  
    }

(根据 i和N生成一个low-discrepancy sequence 低差异化序列)

学新通

GLSL Hammersley 函数为我们提供了大小为 N 的总采样集,采样为 i。

没有位运算符支持的 Hammersley 序列:

  1.  
    //没有位运算符支持的 Hammersley 序列
  2.  
    //并非所有与 OpenGL 相关的驱动程序都支持位运算符(例如 WebGL 和 OpenGL ES 2.0),在这种情况下,您可能需要使用不依赖位运算符的 Van Der Corput 序列的替代版本:
  3.  
     
  4.  
    float VanDerCorput(uint n, uint base)
  5.  
    {
  6.  
    float invBase = 1.0 / float(base);
  7.  
    float denom = 1.0;
  8.  
    float result = 0.0;
  9.  
     
  10.  
    for(uint i = 0u; i < 32u; i)
  11.  
    {
  12.  
    if(n > 0u)
  13.  
    {
  14.  
    denom = mod(float(n), 2.0);
  15.  
    result = denom * invBase;
  16.  
    invBase = invBase / 2.0;
  17.  
    n = uint(float(n) / 2.0);
  18.  
    }
  19.  
    }
  20.  
     
  21.  
    return result;
  22.  
    }
  23.  
    // ----------------------------------------------------------------------------
  24.  
    vec2 HammersleyNoBitOps(uint i, uint N)
  25.  
    {
  26.  
    return vec2(float(i)/float(N), VanDerCorput(i, 2u));
  27.  
    }
  28.  
     
  29.  
    //请注意,由于旧硬件中的 GLSL 循环限制,序列会在所有可能的 32 位上循环。 此版本的性能较低,但如果您发现自己没有位运算符,则可以在所有硬件上工作。
学新通

3.3 GGX Importance sampling GGX 重要性抽样

我们不会在积分半球 Ω 上均匀或随机(蒙特卡洛)生成样本向量,而是根据表面粗糙度生成偏向microsurface halfway vector微表面中间向量的一般反射方向的样本向量。 采样过程将和我们之前看到的类似:开始一个大循环,生成一个随机(低差异)序列值,取序列值在切线空间生成样本向量,变换到世界空间,然后采样 场景的光芒。 不同的是,我们现在use a low-discrepancy sequence value as input to generate a sample vector使用低差异序列值作为输入来生成样本向量

(根据粗糙度,就是重要性,来随机生成一个接近halfway的vector,越粗糙,这个vector离halfway越远,但是有得有规律的远,得符合蒙特卡洛采样,10000个蒙卡一下,会形成一个花瓣形状符合现实)

  1.  
    const uint SAMPLE_COUNT = 4096u;
  2.  
    for(uint i = 0u; i < SAMPLE_COUNT; i)
  3.  
    {
  4.  
    // 生成随机(低差异)序列值
  5.  
    vec2 Xi = Hammersley(i, SAMPLE_COUNT);

此外,为了建立一个样本向量,我们需要一些方法来将样本向量定向和偏置到一些表面粗糙度的镜面波瓣。 我们可以采用理论章节中描述的 NDF,并在 Epic Games 描述的球形样本向量过程中结合 GGX NDF:

  1.  
    //重要性采样
  2.  
    vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
  3.  
    {
  4.  
    float a = roughness*roughness;
  5.  
     
  6.  
    float phi = 2.0 * PI * Xi.x;
  7.  
    float cosTheta = sqrt((1.0 - Xi.y) / (1.0 (a*a - 1.0) * Xi.y));
  8.  
    float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
  9.  
     
  10.  
    // from spherical coordinates to cartesian coordinates
  11.  
    // 从球坐标到笛卡尔坐标
  12.  
    vec3 H;
  13.  
    H.x = cos(phi) * sinTheta;
  14.  
    H.y = sin(phi) * sinTheta;
  15.  
    H.z = cosTheta;
  16.  
     
  17.  
    // from tangent-space vector to world-space sample vector
  18.  
    //从切线空间向量到世界空间样本向量
  19.  
    vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
  20.  
    vec3 tangent = normalize(cross(up, N));
  21.  
    vec3 bitangent = cross(N, tangent);
  22.  
     
  23.  
    vec3 sampleVec = tangent * H.x bitangent * H.y N * H.z;
  24.  
    return normalize(sampleVec);
  25.  
    }
学新通

这为我们提供了一个sample vector样本向量,该样本向量基于一些输入粗糙度和低差异序列值 Xi,somewhat oriented around the expected microsurface's halfway vector。 请注意,Epic Games 基于Disney's最初的 PBR 研究使用平方粗糙度来获得更好的视觉效果。

通过定义低差异 Hammersley 序列和样本生成,我们可以最终确定pre-filter convolution shader预过滤卷积着色器

  1.  
    #version 330 core
  2.  
    out vec4 FragColor;
  3.  
    in vec3 localPos;
  4.  
     
  5.  
    uniform samplerCube environmentMap;
  6.  
    uniform float roughness;
  7.  
     
  8.  
    const float PI = 3.14159265359;
  9.  
     
  10.  
    float RadicalInverse_VdC(uint bits);
  11.  
    vec2 Hammersley(uint i, uint N);
  12.  
    vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);
  13.  
     
  14.  
    void main()
  15.  
    {
  16.  
    vec3 N = normalize(localPos);
  17.  
    vec3 R = N;
  18.  
    vec3 V = R;
  19.  
     
  20.  
    const uint SAMPLE_COUNT = 1024u;
  21.  
    float totalWeight = 0.0;
  22.  
    vec3 prefilteredColor = vec3(0.0);
  23.  
    for(uint i = 0u; i < SAMPLE_COUNT; i)
  24.  
    {
  25.  
    //生成低差异化序列(随机序列)
  26.  
    vec2 Xi = Hammersley(i, SAMPLE_COUNT);
  27.  
    //生成重要性采样,根据粗糙度生成一个接近halfway的vector
  28.  
    vec3 H = ImportanceSampleGGX(Xi, N, roughness);
  29.  
    vec3 L = normalize(2.0 * dot(V, H) * H - V);
  30.  
     
  31.  
    float NdotL = max(dot(N, L), 0.0);
  32.  
    if(NdotL > 0.0)
  33.  
    {
  34.  
    //对prefilteredColor进行采样,这个图是镜面积分的第一张图,第一个部分
  35.  
    prefilteredColor = texture(environmentMap, L).rgb * NdotL;
  36.  
    totalWeight = NdotL;
  37.  
    }
  38.  
    }
  39.  
    prefilteredColor = prefilteredColor / totalWeight;
  40.  
     
  41.  
    FragColor = vec4(prefilteredColor, 1.0);
  42.  
    }
学新通

我们基于在预过滤立方体贴图的每个 mipmap 级别(从 0.0 到 1.0)上变化的一些输入粗糙度对环境进行预过滤,并将结果存储在 prefilteredColor 中。 得到的 prefilteredColor 除以总样本权重,其中对最终结果影响较小的样本(对于较小的 NdotL)对最终权重的贡献较小。

3.4 Capturing pre-filter mipmap levels (捕获预过滤器 mipmap 级别)

剩下要做的就是让 OpenGL 在多个 mipmap 级别上使用不同的粗糙度值对环境贴图进行预过滤。 使用辐照度章节的原始设置实际上很容易做到这一点:

  1.  
    prefilterShader.use();
  2.  
    prefilterShader.setInt("environmentMap", 0);
  3.  
    prefilterShader.setMat4("projection", captureProjection);
  4.  
    glActiveTexture(GL_TEXTURE0);
  5.  
    glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
  6.  
     
  7.  
    glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
  8.  
    unsigned int maxMipLevels = 5;
  9.  
    for (unsigned int mip = 0; mip < maxMipLevels; mip)
  10.  
    {
  11.  
    // reisze framebuffer according to mip-level size.
  12.  
    unsigned int mipWidth = 128 * std::pow(0.5, mip);
  13.  
    unsigned int mipHeight = 128 * std::pow(0.5, mip);
  14.  
    glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
  15.  
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
  16.  
    glViewport(0, 0, mipWidth, mipHeight);
  17.  
     
  18.  
    float roughness = (float)mip / (float)(maxMipLevels - 1);
  19.  
    prefilterShader.setFloat("roughness", roughness);
  20.  
    for (unsigned int i = 0; i < 6; i)
  21.  
    {
  22.  
    prefilterShader.setMat4("view", captureViews[i]);
  23.  
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
  24.  
    GL_TEXTURE_CUBE_MAP_POSITIVE_X i, prefilterMap, mip);
  25.  
     
  26.  
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  27.  
    renderCube();
  28.  
    }
  29.  
    }
  30.  
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
学新通

该过程类似于辐照度图卷积,但这次我们将帧缓冲区的尺寸缩放到适当的 mipmap 比例,每个 mip 级别将尺寸减少 2。此外,我们在 glFramebufferTexture2D 中指定要渲染的 mip 级别 最后一个参数并将我们预过滤的roughness 粗糙度传递给pre-filter shader预过滤着色器

应该为我们提供一个经过适当预过滤的环境贴图它返回的反射越模糊,我们访问它的 mip 级别越高(mip级别越高,图片越小)。 如果我们在天空盒着色器中使用预过滤的环境立方体贴图并在其第一个 mip 级别之上强制采样,如下所示:

vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb; 

学新通

 如果它看起来有点相似,那么您已经成功地预过滤了 HDR 环境贴图。 玩转不同的 mipmap 级别,以查看预过滤器贴图随着 mip 级别的增加逐渐从锐利反射变为模糊反射(mip级别越高,图片变得越小)

4.Pre-filter convolution artifacts 预过滤卷积伪影

虽然当前的预过滤器贴图适用于大多数用途,但迟早您会遇到几个与预过滤器卷积直接相关的渲染伪影。 我将在这里列出最常见的,包括如何修复它们。

4.1 Cubemap seams at high roughness 高粗糙度的立方体贴图接缝

在具有粗糙表面的表面上对预过滤器贴图进行采样意味着在其某些较低的 mip 级别上对预过滤器贴图进行采样。 在对立方体贴图进行采样时,OpenGL 默认不会在立方体贴图面上进行线性插值。 因为较低的 mip 级别具有较低的分辨率,并且预过滤器映射与更大的样本波瓣卷积,所以the lack of between-cube-face filtering 立方体面与面间缺乏过渡,变得非常明显

学新通

 幸运的是,OpenGL 通过启用 GL_TEXTURE_CUBE_MAP_SEAMLESS 为我们提供了正确过滤立方体贴图面的选项:

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);  

只需在应用程序开始的某个地方启用此属性,接缝就会消失。

4.2 Bright dots in the pre-filter convolution 预过滤卷积中的亮点

由于镜面反射中的高频细节和变化很大的光强度,对镜面反射进行卷积需要大量样本才能正确解释 HDR 环境反射变化很大的性质。 我们已经采集了大量样本,但在某些环境中,在某些较粗糙的 mip 级别可能仍然不够,在这种情况下,您将开始看到在明亮区域周围出现虚线图案:

学新通 一种选择是进一步增加样本数,但这对所有环境来说都不够。 正如 Chetan Jags 所述,我们可以通过(在预过滤卷积期间)不直接采样环境图,而是根据积分的 PDF(probability density function概率密度函数)和粗糙度,来决定环境图的 mip 级别来减少此伪影:

  1.  
    float D = DistributionGGX(NdotH, roughness);
  2.  
    float pdf = (D * NdotH / (4.0 * HdotV)) 0.0001;
  3.  
     
  4.  
    float resolution = 512.0; // resolution of source cubemap (per face)
  5.  
    float saTexel = 4.0 * PI / (6.0 * resolution * resolution);
  6.  
    float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf 0.0001);
  7.  
     
  8.  
    //根据采样结果,pdf重要性来配置mipLevel
  9.  
    float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel);

不要忘记在要从中采样其 mip 级别的环境贴图上启用trilinear filtering:(三线性过滤以双线性过滤为基础。会对pixel大小与texel大小最接近的两层Mipmap level分别进行双线性过滤,然后再对两层得到的结果进生线性插值。)

  1.  
    glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
  2.  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

并让 OpenGL 在设置立方体贴图的基础纹理后生成 mipmap:

  1.  
    // convert HDR equirectangular environment map to cubemap equivalent
  2.  
    [...]
  3.  
    // then generate mipmaps
  4.  
    glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
  5.  
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

这效果出奇的好,应该可以去除粗糙表面上预过滤贴图中的大部分(如果不是全部)点。

5. Pre-computing the BRDF (预计算 BRDF)

随着预过滤环境的启动和运行,我们可以专注于second part of the split-sum approximation拆分和近似的第二部分:BRDF。 让我们再次简要回顾一下specular split sum approximation镜面分割和近似

学新通

 我们已经在不同的粗糙度级别上预先计算了预滤波器映射中分割和近似的左侧部分。 右侧要求我们在角度 n⋅ωo、表面粗糙度和菲涅耳 F0 上对 BRDF 方程进行卷积。 这类似于将镜面反射 BRDF 与纯白色环境或 1.0 的恒定辐射度 Li 相结合。 用 3 个变量对 BRDF 进行卷积有点多,但我们可以尝试将 F0 移出镜面 BRDF 方程

学新通

 F是菲涅耳方程。 将菲涅耳分母移到 BRDF 可以得到以下等价方程:

学新通

 用 Fresnel-Schlick 近似代替最右边的 F 可以得到:

学新通

 让我们将 (1−ωo⋅h)5 替换为 α,以便更容易求解 F0:

学新通

 然后我们将菲涅耳函数 F 拆分为两个积分:

学新通

 这样,F0 在积分上是常数,我们可以从积分中取出 F0。 接下来,我们将 α 代回其原始形式,得到最终的拆分和 BRDF 方程:

学新通

得到的两个积分分别代表 F0 的a scale and a bias 尺度和偏差。 请注意,由于 fr(p,ωi,ωo) 已经包含 F函数的项,它们都抵消了,从 fr 中删除了 F函数

与早期的卷积环境贴图类似,我们可以对 BRDF 方程的输入进行卷积:n 和 ωo 之间的角度,以及粗糙度。 我们将convoluted result卷积结果存储在称为 BRDF integration map的 2D 查找纹理 (LUT) 中,稍后我们在 PBR 光照着色器中使用它来获得convoluted indirect specular result最终的卷积间接镜面反射结果。

BRDF 卷积着色器在 2D 平面上运行,使用其 2D 纹理坐标直接作为 BRDF 卷积(NdotV and roughness)的输入。 卷积代码在很大程度上类似于 pre-filter convolution预过滤卷积,只是它现在根据我们的 BRDF 几何函数和 Fresnel-Schlick 近似处理样本向量:

  1.  
    // 整合BRDF ,根据n 和 ωo 之间的角度,以及粗糙度。计算最终镜面反射结果
  2.  
    vec2 IntegrateBRDF(float NdotV, float roughness)
  3.  
    {
  4.  
    vec3 V;
  5.  
    V.x = sqrt(1.0 - NdotV*NdotV);
  6.  
    V.y = 0.0;
  7.  
    V.z = NdotV;
  8.  
     
  9.  
    float A = 0.0;
  10.  
    float B = 0.0;
  11.  
     
  12.  
    vec3 N = vec3(0.0, 0.0, 1.0);
  13.  
     
  14.  
    const uint SAMPLE_COUNT = 1024u;
  15.  
    for(uint i = 0u; i < SAMPLE_COUNT; i)
  16.  
    {
  17.  
    //低离散,假随机,围绕halfway来偏移vector
  18.  
    vec2 Xi = Hammersley(i, SAMPLE_COUNT);
  19.  
    vec3 H = ImportanceSampleGGX(Xi, N, roughness);
  20.  
    vec3 L = normalize(2.0 * dot(V, H) * H - V);
  21.  
     
  22.  
    float NdotL = max(L.z, 0.0);
  23.  
    float NdotH = max(H.z, 0.0);
  24.  
    float VdotH = max(dot(V, H), 0.0);
  25.  
     
  26.  
    if(NdotL > 0.0)
  27.  
    {
  28.  
    //套公式算镜面反射
  29.  
    float G = GeometrySmith(N, V, L, roughness);
  30.  
    float G_Vis = (G * VdotH) / (NdotH * NdotV);
  31.  
    float Fc = pow(1.0 - VdotH, 5.0);
  32.  
     
  33.  
    A = (1.0 - Fc) * G_Vis;
  34.  
    B = Fc * G_Vis;
  35.  
    }
  36.  
    }
  37.  
    A /= float(SAMPLE_COUNT);
  38.  
    B /= float(SAMPLE_COUNT);
  39.  
    return vec2(A, B);
  40.  
    }
  41.  
    // ----------------------------------------------------------------------------
  42.  
    void main()
  43.  
    {
  44.  
    vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);
  45.  
    FragColor = integratedBRDF;
  46.  
    }
学新通

如您所见,BRDF 卷积是从数学到代码的直接转换。 我们将角度 θ 和粗糙度都作为输入,生成具有重要性采样的样本向量,在the geometry and the derived Fresnel term of the BRDF几何和 BRDF 的派生菲涅耳项上对其进行处理,and output both a scale and a bias to F0 for each sample, averaging them in the end.并为每个样本输出一个尺度和一个到 F0 的偏差,平均它们 到底。

您可能从理论章节中回忆起,当与 IBL 一起使用时,BRDF 的几何术语略有不同,因为它的 k 变量的解释略有不同:

学新通

由于 BRDF 卷积是镜面反射 IBL (image base light)积分的一部分,我们将学新通用于 Schlick-GGX 几何函数:

 计算G的部分:

  1.  
    //G的小步骤
  2.  
    float GeometrySchlickGGX(float NdotV, float roughness)
  3.  
    {
  4.  
    float a = roughness;
  5.  
    float k = (a * a) / 2.0;
  6.  
     
  7.  
    float nom = NdotV;
  8.  
    float denom = NdotV * (1.0 - k) k;
  9.  
     
  10.  
    return nom / denom;
  11.  
    }
  12.  
    // ----------------------------------------------------------------------------
  13.  
    //G的大步骤
  14.  
    float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
  15.  
    {
  16.  
    float NdotV = max(dot(N, V), 0.0);
  17.  
    float NdotL = max(dot(N, L), 0.0);
  18.  
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
  19.  
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);
  20.  
     
  21.  
    return ggx1 * ggx2;
  22.  
    }
学新通

请注意,虽然 k 将 a 作为其参数,但我们并没有像我们最初对 a 的其他解释那样将粗糙度平方。 可能因为 a 已经在这里平方了。 我不确定这是否与 Epic Games 的部分或原始迪士尼论文不一致,但直接将粗糙度转换为 a 会给出与 Epic Games 版本相同的 BRDF 集成图。

最后,为了存储 BRDF convolution result卷积结果,我们将生成 512 x 512 分辨率的 2D 纹理:

  1.  
    unsigned int brdfLUTTexture;
  2.  
    glGenTextures(1, &brdfLUTTexture);
  3.  
     
  4.  
    // pre-allocate enough memory for the LUT texture.
  5.  
    glBindTexture(GL_TEXTURE_2D, brdfLUTTexture);
  6.  
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0);
  7.  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  8.  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  9.  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  10.  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

 请注意,我们使用 Epic Games 推荐的 16 位精度浮点格式。 请务必将环绕模式设置为 GL_CLAMP_TO_EDGE 以防止边缘采样伪影。

然后,我们重新使用相同的帧缓冲区对象并在 NDC 屏幕空间四边形上运行此着色器:

  1.  
    glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
  2.  
    glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
  3.  
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
  4.  
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0);
  5.  
     
  6.  
    glViewport(0, 0, 512, 512);
  7.  
    brdfShader.use();
  8.  
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  9.  
    RenderQuad();
  10.  
     
  11.  
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

拆分和积分的卷积 BRDF 部分应为您提供以下结果:

学新通

 使用预过滤的环境贴图和 BRDF 2D LUT,we can re-construct the indirect specular integral according to the split sum approximation. The combined result then acts as the indirect or ambient specular light.我们可以根据拆分和近似重建间接镜面反射积分。 然后,组合结果充当6间接或环境镜面反射光。

6.Completing the IBL reflectance 完成 IBL 反射率

为了使反射方程的间接镜面反射部分启动并运行,我们需要将stitch both parts of the split sum approximation together拆分和近似的两个部分缝合在一起。 让我们首先将预先计算的光照数据添加到 PBR 着色器的顶部:

  1.  
    uniform samplerCube prefilterMap;
  2.  
    uniform sampler2D brdfLUT;

首先,我们通过使用反射向量对pre-filtered environment map预过滤的环境贴图进行采样来获得表面的间接镜面反射。 请注意,我们根据表面粗糙度对适当的 mip 级别进行采样,从而使更粗糙的表面具有更模糊的镜面反射:

  1.  
    void main()
  2.  
    {
  3.  
    [...]
  4.  
    vec3 R = reflect(-V, N);
  5.  
     
  6.  
    const float MAX_REFLECTION_LOD = 4.0;
  7.  
    vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
  8.  
    [...]
  9.  
    }

在预过滤步骤中,我们仅将环境映射卷积到最多 5 个 mip 级别(0 到 4),我们在此将其表示为 MAX_REFLECTION_LOD,以确保我们不会在没有(相关)数据的情况下对 mip 级别进行采样。

然后我们从 BRDF lookup texture (BRDF 查找纹理)中采样指定材质的粗糙度以及法线和视图向量之间的角度

  1.  
    vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
  2.  
    vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
  3.  
    vec3 specular = prefilteredColor * (F * envBRDF.x envBRDF.y);

Given the scale and bias to F0 (here we're directly using the indirect Fresnel result F) from the BRDF lookup texture,我们将其与 IBL 反射方程的左预过滤部分结合起来,并将近似积分结果重新构造为 specular镜面反射

这给了我们反射方程的间接镜面反射部分。 现在,将它与上一章中反射方程的漫反射 IBL 部分结合起来,我们得到完整的 PBR IBL 结果:

  1.  
    vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
  2.  
     
  3.  
    vec3 kS = F;
  4.  
    vec3 kD = 1.0 - kS;
  5.  
    kD *= 1.0 - metallic;
  6.  
     
  7.  
    vec3 irradiance = texture(irradianceMap, N).rgb;
  8.  
    vec3 diffuse = irradiance * albedo;
  9.  
     
  10.  
    const float MAX_REFLECTION_LOD = 4.0;
  11.  
    //镜面反射第一部分
  12.  
    vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
  13.  
    //镜面反射第二部分
  14.  
    vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
  15.  
    // 二者套公式结合
  16.  
    vec3 specular = prefilteredColor * (F * envBRDF.x envBRDF.y);
  17.  
     
  18.  
    //环境光
  19.  
    vec3 ambient = (kD * diffuse specular) * ao;
学新通

Note that we don't multiply specular by ks as we already have a Fresnel multiplication in there.

现在,在粗糙度和金属属性不同的一系列球体上运行这个精确的代码,我们终于可以在最终的 PBR 渲染器中看到它们的真实颜色:

学新通

 We could even go wild, and use some cool textured PBR materials:

学新通Or load this awesome free 3D PBR model by Andrew Maximov:

学新通

 我相信我们都同意我们的照明现在看起来更有说服力了。 更好的是,无论我们使用哪种环境贴图,我们的光照看起来都是正确的。 下面您将看到几个不同的预计算 HDR 贴图,它们完全改变了光照动态,但在不改变单个光照变量的情况下仍然看起来物理上正确!

学新通

 好吧,这次 PBR 冒险原来是一段漫长的旅程。 有很多步骤,因此可能会出错,因此如果您遇到困难,请仔细检查球体场景或纹理场景代码示例(包括所有着色器),或者在评论中查看并询问。

通常只需将环境贴图预先计算为辐照度和预过滤器贴图,然后将其存储在磁盘上(请注意,BRDF 集成贴图不依赖于环境贴图,因此您只需要计算或加载一次)。这确实意味着您需要提供一种自定义图像格式来存储 HDR 立方体贴图,包括它们的 mip 级别。或者,您可以将其存储(并加载)为一种可用格式(例如支持存储 mip 级别的 .dds)。

也可通过使用 cmftStudio 或 IBLBaker 等几个出色的工具为您生成这些预先计算的地图。

我们跳过的一点是预先计算的立方体贴图的reflection probes反射探针 cubemap interpolation and parallax correction立方体贴图插值和视差校正。这是在场景中放置多个反射探测器的过程,这些探测器在该特定位置拍摄场景的立方体贴图快照,然后我们可以将其卷积为该部分场景的 IBL 数据。通过基于相机附近的几个探针之间的插值,我们可以实现基于局部高细节图像的照明,这仅受我们愿意放置的反射探针数量的限制。 

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgafece
系列文章
更多 icon
同类精品
更多 icon
继续加载