QT with OpenGL(IBL-镜面反射)

文章详细介绍了OpenGL中预滤波的过程,包括generateMipmap的使用,获取不同层级的预滤波图,以及预滤波Shader的重要性和采样效果。同时,文章探讨了预计算BRDF生成LUT图的方法,以及IBLShading中的环境光反射计算,展示了不同计算方式对渲染结果的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

预滤波

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.5log2(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_COUNTpdf

综上:
一个采样点覆盖的像素数 = 6 ∗ r e s o l u t i o n 2 4 π ∗ 总采样数 ∗ p d f 一个采样点覆盖的像素数 = \frac{6 * resolution^ 2}{4\pi * 总采样数 * pdf } 一个采样点覆盖的像素数=4π总采样数pdf6resolution2

问题又来了:pdf怎么计算?
p d f pdf pdfImportanceSampleGGX函数生成的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(HV)DistributionGGX(NH,roughness)(NH)
因为 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(NH,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 NV),粗糙度 r o u g h n e s s roughness roughness
得到与 F 0 F_0 F0 无关的两个参数。

LUT图纹理的坐标:
横坐标:视口方向与法线的夹角( N ⋅ V N \cdot V NV
纵坐标:粗糙度( 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(个人)函数会更小一点。也就是说反射量会小一点。

但比较两图,肉眼上并无差别。
在这里插入图片描述
在这里插入图片描述

最终结果展示

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elsa的迷弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值