> 文章列表 > IBL-镜面反射(预滤波篇)

IBL-镜面反射(预滤波篇)

IBL-镜面反射(预滤波篇)

文章目录

  • 1低差异序列
  • 2重要性采样
  • 3 GGX重要性采样
    • 3.1 将uv坐标转化为半球向量坐标
    • 3.2 将半球向量坐标转化为笛卡尔坐标
    • 3.3 将切线坐标转化为世界坐标
    • 3.4 完整代码
  • 4.预滤波器卷积着色器
    • 4.1 近似
    • 4.2 获取采样方向并采样
    • 4.3 完整代码(待优化,因为N = V)
    • 4.4 mipmap问题
      • 4.4.1 立方体的面和面之间的滤波的问题
      • 4.4.2 预过滤卷积的亮点
        • 4.4.2.解决方案
  • 5. 将得到的卷积图像保存在mipmap中
    • 5.1 每渲染一个mipmap,都需要重新设置一次FrameBuffer,和Viewport
    • 5.2 将roughness0~1的范围,对应到mipmap最低层到最高层
    • 5.3 将mip层绑定到FrameBuffer并渲染
    • 补充:获取mipmap半层的方法

1低差异序列

低差异序列最直接简单直观的解释就是:将小数点左边的数翻转到小数点右边。

如果为2进制:

1	1	         --------->	          0.1		0.5
2	10	         --------->	          0.01		0.25
3	11	         --------->	          0.11		0.75
4	100	         --------->	          0.001		0.125
5	101	         --------->	          0.101		0.625
6	110	         --------->	          0.011		0.375
7	111	         --------->	          0.111		0.875
8	1000         --------->	          0.0001	1/16
9	1001         --------->	          0.1001	1/2 + 1/16
10	1010         --------->	          0.0101	1/4 + 1/16

观察1-7的分布,可以看到为0.125 0.25 0.375 0.5 0.625 0.75 0.875,分布均匀。
数字代号分别为4 2 6 1 5 3 7,看起来像不像二叉树的中序遍历

如果为3进制:

1	1	         --------->	          0.1		1/3
2	2	         --------->	          0.2		2/3
3	10	         --------->	          0.01		1/9
4	11	         --------->	          0.11		1/3 + 1/9
5	12	         --------->	          0.21		2/3 + 1/9
6	20	         --------->	          0.02		2/9
7	21	         --------->	          0.12		1/3 + 2/9
8	22         --------->	          0.22		2/3 + 2/9
9	100         --------->	          0.001		1/27 
10	101         --------->	          0.101		1/3 + 1/27

观察1-8的分布 ,数字代号为3 6 1 4 7 2 5 8

2重要性采样

参照 重要性采样

3 GGX重要性采样

3.1 将uv坐标转化为半球向量坐标

函数中:
Xi代表低差异性采样的uv坐标(个人决定用vec2 uv会更好)。
N代表宏观法线
roughness为粗糙度

float a = roughness*roughness;float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta*cosTheta);

根据迪士尼对 PBR 的研究,Epic Games(虚幻引擎公司) 使用了平方粗糙度以获得更好的视觉效果

Xi.x决定了方位角(u)
Xi.y决定了半球向量与法线的夹角(v)
ϕ=2πucos2θ=1−v1−(1−a2)v\\phi = 2\\pi u\\\\ \\quad \\\\ cos^2\\theta = \\frac{1-v}{1-(1-a^2)v}ϕ=2πucos2θ=1(1a2)v1v

θ、ϕ\\theta 、 \\phiθϕ 角如下图所示
IBL-镜面反射(预滤波篇)

3.2 将半球向量坐标转化为笛卡尔坐标

// from spherical coordinates to cartesian coordinates
vec3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;

IBL-镜面反射(预滤波篇)
注意根据公式:
cos2θ=1−v1−(1−a2)vcos^2\\theta = \\frac{1-v}{1-(1-a^2)v}cos2θ=1(1a2)v1v

zmax=lim⁡a→0cosθ=1zmin=lim⁡a→1cosθ=(1−v)x2+y2max=vx2+y2min=0z_{max} =\\lim_{a \\to 0}cos\\theta = 1 \\\\ z_{min} =\\lim_{a \\to 1}cos\\theta = \\sqrt{(1-v)} \\\\ \\quad\\\\ \\sqrt{x^2 + y^2}_{max} = v\\\\ \\sqrt{x^2 + y^2}_{min} = 0zmax=a0limcosθ=1zmin=a1limcosθ=(1v)x2+y2max=vx2+y2min=0
也就是说,
aaa决定了采样是否更接近法线,aaa越大,采样范围越分散;反之。

下图中: sin2θ=x2+y2,横坐标为v(图中画错了,画成了u)sin^2\\theta = x^2+y^2 , 横坐标为v(图中画错了,画成了u)sin2θ=x2+y2,横坐标为v(图中画错了,画成了u
IBL-镜面反射(预滤波篇)
Excal杀我啊!!

3.3 将切线坐标转化为世界坐标

注:up值的选定只是为了生成转换坐标系,只要up值与N不在同一直线就可。

// from tangent-space vector to world-space sample vector
vec3 up        = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
vec3 tangent   = normalize(cross(up, N));
vec3 bitangent = cross(N, tangent);
vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;return normalize(sampleVec);

3.4 完整代码

vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{float a = roughness*roughness;float phi = 2.0 * PI * Xi.x;float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));float sinTheta = sqrt(1.0 - cosTheta*cosTheta);// from spherical coordinates to cartesian coordinatesvec3 H;H.x = cos(phi) * sinTheta;H.y = sin(phi) * sinTheta;H.z = cosTheta;// from tangent-space vector to world-space sample vectorvec3 up        = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);vec3 tangent   = normalize(cross(up, N));vec3 bitangent = cross(N, tangent);vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;return normalize(sampleVec);
}

4.预滤波器卷积着色器

4.1 近似

由于我们在卷积环境贴图时事先不知道视角方向,因此 Epic Games 假设视角方向——也就是镜面反射方向——总是等于输出采样方向ωo,以作进一步近似。翻译成代码如下:

vec3 N = normalize(localPos);    
vec3 V = N;//V是视口方向

也就是说:如果我们看一个球体,我们看到的球体表面的反射光来自球体的法相方向。如下图所示。这样确实得到了一定程度的近似,尤其在正对物体法线时的高光效果会接近真实。
IBL-镜面反射(预滤波篇)
但是,当我们在有较大角度观察类似于镜子湖面的平面时,反射往往得不到令人满意的预期。

下面这图我真心觉得不太能表达上面说的东西,我刚看到时候琢磨的半天也不知道要表达什么
IBL-镜面反射(预滤波篇)

4.2 获取采样方向并采样

vec2 Xi = Hammersley(i, SAMPLE_COUNT);
vec3 H  = ImportanceSampleGGX(Xi, N, roughness);
vec3 L  = normalize(2.0 * dot(V, H) * H - V);prefilteredColor += texture(environmentMap, L).rgb * NdotL;

Xi为低差异序列值
H为低差异序列生成的微表面法线
L为微表面法线的反射方向

4.3 完整代码(待优化,因为N = V)

vec3 N = normalize(localPos);    
vec3 V = N;//V是视口方向const uint SAMPLE_COUNT = 1024u;
float totalWeight = 0.0;   
vec3 prefilteredColor = 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.0 * dot(V, H) * H - V);float NdotL = max(dot(N, L), 0.0);if(NdotL > 0.0){prefilteredColor += texture(environmentMap, L).rgb * NdotL;totalWeight      += NdotL;}
}
prefilteredColor = prefilteredColor / totalWeight;FragColor = vec4(prefilteredColor, 1.0);

4.4 mipmap问题

4.4.1 立方体的面和面之间的滤波的问题

OpenGL 可以启用 GL_TEXTURE_CUBE_MAP_SEAMLESS,以为我们提供在立方体贴图的面之间进行正确过滤的选项

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);   

4.4.2 预过滤卷积的亮点

我们已经进行了大量的采样,但是在某些环境下,在某些较粗糙的 mip 级别上可能仍然不够,导致明亮区域周围出现点状图案
IBL-镜面反射(预滤波篇)

4.4.2.解决方案

我们可以在预过滤卷积时,不直接采样环境贴图,而是基于积分的 PDF粗糙度采样环境贴图的 mipmap ,以减少伪像。

// 计算法线分布概率,通过法线分布概率得出pdf
float D   = DistributionGGX(NdotH, roughness);
float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001; //通过立方体贴图像素值,采样数等确定mipmap级别。
float resolution = 512.0; // resolution of source cubemap (per face)
float saTexel  = 4.0 * PI / (6.0 * resolution * resolution);
float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);
float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); //累计卷积颜色值。
prefilteredColor += textureLod(environmentMap, L, mipLevel).rgb * NdotL;
totalWeight      += NdotL;

根据上下文可知N==V,所以pdf=D4+0.0001pdf = \\frac{D}{4} + 0.0001pdf=4D+0.0001
IBL-镜面反射(预滤波篇)
关于GGX法线分布的pdf积分求解参照brdf重要性采样

至于作者为什么要这么去做,可以看一下出处 Chetan Jags ,还没看,不太懂它这样做的意义。

5. 将得到的卷积图像保存在mipmap中

5.1 每渲染一个mipmap,都需要重新设置一次FrameBuffer,和Viewport

unsigned int mipWidth  = 128 * std::pow(0.5, mip);
unsigned int mipHeight = 128 * std::pow(0.5, mip);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
glViewport(0, 0, mipWidth, mipHeight);

5.2 将roughness0~1的范围,对应到mipmap最低层到最高层

即:
roughness = 0 对应mipmap第0层
roughness = 1 对应mipmap设定的最高层
中间的mipmap层数由插值的roughness作为参数输入。

 float roughness = (float)mip / (float)(maxMipLevels - 1);

5.3 将mip层绑定到FrameBuffer并渲染

for (unsigned int i = 0; i < 6; ++i)
{prefilterShader.setMat4("view", captureViews[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);renderCube();
}

补充:获取mipmap半层的方法

  1. 设置CubeMap的参数时,将MIN_FILTER过滤器设置为三线性插值GL_LINEAR_MIPMAP_LINEAR
  2. glGenerateMipmap(GL_TEXTURE_CUBE_MAP)
glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap); 
...
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);  
... 
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
  1. 在Shader中使用时,使用textureLod(纹理, 采样方向,mipmap级数)
vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb;