文章目录
- 预滤波
- generate Mipmap
- 获取每一层级的预滤波图
- prefilterMap Shader
- 重要性采样
- 效果展示
- 预过滤卷积的亮点
- 解决方法
- 代码解析
- 首先得确保我们被采样的环境贴图有mipmap贴图
- 通过计算决定使用那一层mipmap值
- 效果
- 预计算BRFD
- 生成LUT图
- IBL Shading
- 渲染结果
- 与教材的不同
- 最终结果展示
预滤波
generate Mipmap
Cubemap增加是否生成mipmap选项
if(!mipmap)
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
else
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
if(mipmap) glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
获取每一层级的预滤波图
void CubeMap::getIBLprefilterMapFromEnvCubeMap(unsigned int CubeMap,unsigned int maxMipLevels)
{
//第一步:编译链接预滤波Shader
QOpenGLShaderProgram prefilterShader;
prefilterShader.addShaderFromSourceFile(QOpenGLShader::Vertex,":/cubemap.vert");
prefilterShader.addShaderFromSourceFile(QOpenGLShader::Fragment,":/prefilterMap.frag");
prefilterShader.link();
//第二步:FBO 创建帧缓存、绑定深度缓存和模板缓存
unsigned int captureFBO, captureRBO;
glGenFramebuffers(1, &captureFBO);
glGenRenderbuffers(1, &captureRBO);
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, CubeSize, CubeSize);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
//第三步:输入Uniform参数,并渲染到当前Cubemap
prefilterShader.bind();
prefilterShader.setUniformValue("projection", captureProjection); //vert
prefilterShader.setUniformValue("environmentMap", 0); //frag
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, CubeMap);//此处输入CubeMap
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
for (unsigned int mip = 0; mip < maxMipLevels; ++mip)
{
// 1.计算第i层的mipmap大小
unsigned int mipi = CubeSize >> mip;
//mipmap第i层的长宽为 第0层大小 * 0.5^mip
// == CubeSize * pow(0.5, mip)
// == CubeSize >> mip
// 2.设置该mip层的渲染窗体大小
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipi, mipi);
glViewport(0, 0, mipi, mipi);
// 3.根据mipmap的层级选择预滤波的滤波模糊度
float roughness = (float)mip / (float)(maxMipLevels - 1);
prefilterShader.setUniformValue("roughness", roughness); //frag
// 4.渲染得到该层级的预滤波立方体贴图
for (unsigned int i = 0; i < 6; ++i)
{
prefilterShader.setUniformValue("view", lookatMatrix[i]); //vert
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, mip);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderCube();
}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
prefilterMap Shader
#version 450 core
//输入:预滤波方向
in vec3 WorldPos;
out vec4 FragColor;
//主要参数:
uniform samplerCube environmentMap;//环境立方体贴图
uniform float roughness;//控制预滤波的模糊程度
//辅助参数:
uniform uint sample_count;//每方向采样数
uniform bool enMapHasMipmap;//是否有mipmap
uniform int environmentMapSize;//环境立方体贴图大小
const float PI = 3.1415926535;
void main(void)
{
vec3 N = normalize(WorldPos);
//采样数可设为Uniform
//const uint SAMPLE_COUNT = 1024u;
const uint SAMPLE_COUNT = sample_count;
//采样结果保存
vec3 prefilteredColor = vec3(0.0);
float totalWeight = 0.0;
//重要性采样
for(uint i = 0;i<SAMPLE_COUNT;++i){
//得到采样方向
vec3 L = N;
//采样(roughness 0-1 属于 mipmap 0 - 7)
prefilteredColor += texture(environmentMap, L).rgb;
totalWeight += 1.0f;
}
prefilteredColor = prefilteredColor / totalWeight;
FragColor = vec4(prefilteredColor, 1.0);
}
其中获取采样方向,以及确定采样层级是较为关键的部分。
重要性采样
#version 450 core
//输入:预滤波方向
in vec3 WorldPos;
out vec4 FragColor;
//主要参数:
uniform samplerCube environmentMap;//环境立方体贴图
uniform float roughness;//控制预滤波的模糊程度
//辅助参数:
uniform uint sample_count;//每方向采样数
//uniform bool enMapHasMipmap;//是否有mipmap
//uniform int environmentMapSize;//环境立方体贴图大小
//辅助函数
float RadicalInverse_VdC(uint bits);
vec2 Hammersley(uint i, uint N);
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);
float DistributionGGX(vec3 N, vec3 H, float roughness);
//const
const float PI = 3.1415926535;
void main(void)
{
vec3 N = normalize(WorldPos);
//采样数可设为Uniform
//const uint SAMPLE_COUNT = 1024u;
const uint SAMPLE_COUNT = 1024u;
//采样结果保存
vec3 prefilteredColor = vec3(0.0);
float totalWeight = 0.0;
//重要性采样
for(uint i = 0;i<SAMPLE_COUNT;++i){
//得到采样方向
vec2 randomVec2 = Hammersley(i,SAMPLE_COUNT);
vec3 H = ImportanceSampleGGX(randomVec2,N,roughness);
vec3 L = normalize(2.0 * dot(N, H) * H - N);
//问题:该反射方向可能会向物体背部反射,所以要去除背向光线
//那为何不直接使用H作为反射方向,这样可以避免光线的失效,增加采样数,减少L的计算时间
float NdotL = max(dot(N,L),0.0f);
if(NdotL > 0.0f){
//采样(roughness 0-1)
//float D = DistributionGGX(N,H,roughness);
prefilteredColor += texture(environmentMap, L).rgb;
totalWeight += 1.0f;
}
}
prefilteredColor = prefilteredColor / totalWeight;
FragColor = vec4(prefilteredColor, 1.0);
}
效果展示
mipmap 0
mipmap 1
mipmap 2
mipmap 3
mipmap 4
mipmap 5
--全黑(就不截图了)
mipmap 4.9
可以看出没有被渲染的mipmap层级存在(不会报错),但值为纯黑。
因此如果渲染中间层,会将前一层与该黑色层混合,而不报错。
另外可以看到立方体贴图的贴图之间并未进行滤波。
OpenGL 可以启用 GL_TEXTURE_CUBE_MAP_SEAMLESS
,以为我们提供在立方体贴图的面之间进行正确过滤的选项:
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
如下:可以看到边角位置不再有明显边界
mipmap 3.6
mipmap 4.9
注意:开启该选项后,并不是对纹理做模糊,而是当指向像素边界时,会根据边界临近的纹理像素做插值。
因此,如果只是在预滤波中开启GL_TEXTURE_CUBE_MAP_SEAMLESS
,而在显示预滤波图时不开启GL_TEXTURE_CUBE_MAP_SEAMLESS
,我们看到的边界结果才是纹理中真正存储的像素值。
纹理真正记录的数据如下,GL_TEXTURE_CUBE_MAP_SEAMLESS
相当于边界处的 glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
函数。
预过滤卷积的亮点
解决方法
在较高采样率区域使用较低mipmap
(更清晰)级别的纹理进行采样。
在较低采样率区域使用较高mipmap
(更模糊)级别的纹理进行采样。
代码解析
首先得确保我们被采样的环境贴图有mipmap贴图
glBindTexture(GL_TEXTURE_CUBE_MAP,envCubemap);
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
环境贴图mipmap
mipmap 1
mipmap 2
mipmap 3
mipmap 4
mipmap 7
通过计算决定使用那一层mipmap值
mipmap每加1层,纹理大小就缩小一半,每四个像素合成一个像素。
因此,
如果一个采样点覆盖率为4个像素,则应该在第1层mipmap上采样;
如果一个采样点覆盖率为16个像素,则应该在第2层mipmap上采样;
如果一个采样点覆盖率为8个像素,则应该在
l
o
g
2
(
8
1
2
)
=
0.5
∗
l
o
g
2
(
8
)
log_2(8^\frac{1}{2}) = 0.5 * log_2(8)
log2(821)=0.5∗log2(8)层mipmap采样。
现在问题转移到了一个采样点覆盖的像素数为多少。
一个采样点覆盖的像素数
=
该方向像素数
该方向采样数
一个采样点覆盖的像素数 = \frac{该方向像素数}{该方向采样数}
一个采样点覆盖的像素数=该方向采样数该方向像素数
已知,球面坐标上的WorldPos
指向立方体贴图,每个单位立体角向量指向的贴图像素数也是不同的。
向量指向立方体贴图边角位置,该方向的像素数会偏多。
而指向立方体一面中心位置,像素数就会偏少。
这里做平均处理,将该方向像素数平均。
该方向像素数
=
立方体像素数
球面积分
=
6
∗
分辨
率
2
4
π
该方向像素数 = \frac{立方体像素数}{球面积分} = \frac{6 * 分辨率^2}{4\pi}
该方向像素数=球面积分立方体像素数=4π6∗分辨率2
该方向采样数 = 总采样数 ∗ 该方向采样概率 = S A M P L E _ C O U N T ∗ p d f 该方向采样数 = 总采样数 * 该方向采样概率 \\= SAMPLE\_COUNT * pdf 该方向采样数=总采样数∗该方向采样概率=SAMPLE_COUNT∗pdf
综上:
一个采样点覆盖的像素数
=
6
∗
r
e
s
o
l
u
t
i
o
n
2
4
π
∗
总采样数
∗
p
d
f
一个采样点覆盖的像素数 = \frac{6 * resolution^ 2}{4\pi * 总采样数 * pdf }
一个采样点覆盖的像素数=4π∗总采样数∗pdf6∗resolution2
问题又来了:pdf怎么计算?
p
d
f
pdf
pdf是 由ImportanceSampleGGX
函数生成的H
向量计算得到 的 L
的分布
而H
的向量分布为均匀分布的伪随机数。
这里 Chetan Jags 做了近似,将 p d f pdf pdf 近似为法线分布函数计算得到的值。
p
d
f
=
D
i
s
t
r
i
b
u
t
i
o
n
G
G
X
(
N
⋅
H
,
r
o
u
g
h
n
e
s
s
)
∗
(
N
⋅
H
)
4
∗
(
H
⋅
V
)
pdf = \frac{DistributionGGX(N\cdot H, roughness) * (N \cdot H) }{4 * (H \cdot V)}
pdf=4∗(H⋅V)DistributionGGX(N⋅H,roughness)∗(N⋅H)
因为
N
=
=
V
N==V
N==V,所以简化为
p
d
f
=
D
i
s
t
r
i
b
u
t
i
o
n
G
G
X
(
N
⋅
H
,
r
o
u
g
h
n
e
s
s
)
4
pdf = \frac{DistributionGGX(N\cdot H, roughness) }{4 }
pdf=4DistributionGGX(N⋅H,roughness)
综上,代码为:
//重要性采样
for(uint i = 0;i<SAMPLE_COUNT;++i){
//得到采样方向
vec2 randomVec2 = Hammersley(i,SAMPLE_COUNT);
vec3 H = ImportanceSampleGGX(randomVec2,N,roughness);
vec3 L = normalize(2.0 * dot(N, H) * H - N);
//问题:该反射方向可能会向物体背部反射,所以要去除背向光线
//那为何不直接使用H作为反射方向,这样可以避免光线的失效,增加采样数,减少L的计算时间
float NdotL = max(dot(N,L),0.0f);
if(NdotL > 0.0f){
//计算采样mipmap
float D = DistributionGGX(N,H,roughness);
float pdf = D / 4.0 + 0.0001;
//一个采样点对应四个采样像素,mipmap=1;
//mipmap级别 = 0.5 * log_2(一个采样点采样的像素数) ;
//一个采样点采样的像素数 = 每方向像素数 / (采样数量 * 该方向采样概率)
// = 6 * res * res / (4 * PI * SAMPLE_COUNT * pdf);
//每像素平均立体角,当前采样方向的概率
float resolution = 512.0; // 原空间盒清晰度 (per face)
float TexPerSample = 4 * resolution * resolution / (6 * PI * SAMPLE_COUNT * pdf);
float mipLevel = ( roughness == 0.0 ? 0.0 : 0.5 * log2(TexPerSample) );
//加权
prefilteredColor += textureLod(environmentMap, L, mipLevel).rgb * NdotL;
totalWeight += NdotL;
}
}
最后加权NdotL
是为了减少较大倾角对像素点的权值。
平均
prefilteredColor = prefilteredColor / totalWeight;
FragColor = vec4(prefilteredColor, 1.0);
效果
上方为解决两点的显示,下方为之前的显示。
mipmap 3
使用预过滤环境贴图,LINEAR
,使用加权NdotL
使用预过滤环境贴图,NEAREST
,使用加权NdotL
未使用预过滤环境贴图,LINEAR
,未使用加权 NdotL
未使用预过滤环境贴图,LINEAR
,未使用加权 NdotL
未使用预过滤环境贴图,NEAREST
,使用加权 NdotL
mipmap 4
综上:在一般情况下使用预过滤环境贴图,效果不大,但使用加权 NdotL
可以很大程度上改变效果,将更多的采样权值放在中心采样区。
预计算BRFD
生成LUT图
已知视口方向与法线的夹角(
N
⋅
V
N \cdot V
N⋅V),粗糙度(
r
o
u
g
h
n
e
s
s
roughness
roughness)
得到与
F
0
F_0
F0 无关的两个参数。
LUT图纹理的坐标:
横坐标:视口方向与法线的夹角(
N
⋅
V
N \cdot V
N⋅V)
纵坐标:粗糙度(
r
o
u
g
h
n
e
s
s
roughness
roughness)
纹理存储的值:
R:
F
0
F_0
F0 的比例
G:
F
0
F_0
F0 的偏差
注:LUT图与材质无关(粗糙度,金属度),与环境贴图无关,与视口法线无关。因此所有的IBL镜面反射只需要一个LUT图(所有材质的BRDF都是基于金属度,粗糙度,给定的 F 0 F_0 F0,而且反射方式相同的情况下)。
IBL Shading
输入Uniform
增加如下参数
// IBL
uniform samplerCube irradianceMap;
uniform samplerCube prefilterMap;
uniform int maxMipmapLevels;
uniform sampler2D LUT;
环境光反射计算
//环境光镜面反射 specular
vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0);
vec3 prefilterColor = textureCubeLod(prefilterMap,R,roughness * maxMipmapLevels).rgb;
vec2 brdf = texture2D( LUT, vec2( max(dot(N,V),0.0) , roughness )).rg;
vec3 specular = prefilterColor * (kS * brdf.x + brdf.y);
合入渲染结果
//合计最终值
vec3 color = ambient + specular + Lo;
渲染结果
与教材的不同
教材中 k S k_S kS 的计算使用了如下方程
vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
//调用如下方程计算kS
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);
}
渲染效果如下
差别,使用fresnelSchlickRoughness
(教材)函数计算的
F
0
F_0
F0 相比于fresnelSchlick
(个人)函数会更小一点。也就是说反射量会小一点。
但比较两图,肉眼上并无差别。