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−(1−a2)v1−v
θ、ϕ\\theta 、 \\phiθ、ϕ 角如下图所示
3.2 将半球向量坐标转化为笛卡尔坐标
// from spherical coordinates to cartesian coordinates
vec3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
注意根据公式:
cos2θ=1−v1−(1−a2)vcos^2\\theta = \\frac{1-v}{1-(1-a^2)v}cos2θ=1−(1−a2)v1−v
有
zmax=lima→0cosθ=1zmin=lima→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=a→0limcosθ=1zmin=a→1limcosθ=(1−v)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)
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是视口方向
也就是说:如果我们看一个球体,我们看到的球体表面的反射光来自球体的法相方向。如下图所示。这样确实得到了一定程度的近似,尤其在正对物体法线时的高光效果会接近真实。
但是,当我们在有较大角度观察类似于镜子,湖面的平面时,反射往往得不到令人满意的预期。
下面这图我真心觉得不太能表达上面说的东西,我刚看到时候琢磨的半天也不知道要表达什么
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 级别上可能仍然不够,导致明亮区域周围出现点状图案
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。
关于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半层的方法
- 设置CubeMap的参数时,将
MIN_FILTER
过滤器设置为三线性插值GL_LINEAR_MIPMAP_LINEAR
。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);
- 在Shader中使用时,使用
textureLod(纹理, 采样方向,mipmap级数)
vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb;