反射方程的分解、预计算BRDF

news2024/12/28 23:45:09

反射方程

 仔细研究反射方程可以发现BRDF的漫反射kd和镜面反射ks是相互独立的,所以可以将方程分解为两部分:

 通过分别积分两部分再求即可得到最终的反射结果。


漫反射部分

 仔细观察漫反射积分,我们发现漫反射兰伯特项是一个常数项(颜色 c 、折射率 kd 和 π 在整个积分是常数),不依赖于任何积分变量。基于此,我们可以将常数项移出漫反射积分:

 这给了我们一个只依赖于 wi的积分(假设 p 位于环境贴图的中心)。有了这些知识,我们就可以计算或预计算一个新的立方体贴图,它在每个采样方向——也就是纹素——中存储漫反射积分的结果,这些结果是通过卷积计算出来的。

        卷积的特性是,对数据集中的一个条目做一些计算时,要考虑到数据集中的所有其他条目。这里的数据集就是场景的辐射度或环境贴图。因此,要对立方体贴图中的每个采样方向做计算,我们都会考虑半球 Ω 上的所有其他采样方向

        为了对环境贴图进行卷积,我们通过对半球 Ω上的大量方向进行离散采样并对其辐射度取平均值,来计算每个输出采样方向 wo的积分。用来采样方向 wi的半球,要面向卷积的输出采样方向 wo。

 这个预计算的立方体贴图,在每个采样方向 wo上存储其积分结果,可以理解为场景中所有能够击中面向 wo 的表面的间接漫反射光的预计算总和。这样的立方体贴图被称为辐照度图,因为经过卷积计算的立方体贴图能让我们从任何方向有效地直接采样场景(预计算好的)辐照度。

下面是一个环境立方体贴图及其生成的辐照度图的示例(由 Wave 引擎提供),每个向wo 的场景辐射度取平均值。

由于立方体贴图每个纹素中存储了( wo 方向的)卷积结果,辐照度图看起来有点像环境的平均颜色或光照图。使用任何一个向量对立方体贴图进行采样,就可以获取该方向上的场景辐照度。

 然而,计算上又不可能从 Ω 的每个可能的方向采样环境光照,理论上可能的方向数量是无限的。不过我们可以对有限数量的方向采样以近似求解,在半球内均匀间隔或随机取方向可以获得一个相当精确的辐照度近似值,从而离散地计算积分 ∫。

然而,对于每个片段实时执行此操作仍然太昂贵,因为仍然需要非常大的样本数量才能获得不错的结果,因此我们希望可以预计算。既然半球的朝向决定了我们捕捉辐照度的位置,我们可以预先计算每个可能的半球朝向的辐照度,这些半球朝向涵盖了所有可能的出射方向 wo :

 给定任何方向向量 wi ,我们可以对预计算的辐照度图采样以获取方向 wi的总漫反射辐照度。为了确定片段上间接漫反射光的数量(辐照度),我们获取以表面法线为中心的半球的总辐照度。获取场景辐照度的方法就简化为:

vec3 irradiance = texture(irradianceMap, N);

现在,为了生成辐照度贴图,我们需要将环境光照求卷积,转换为立方体贴图。假设对于每个片段,表面的半球朝向法向量 N ,对立方体贴图进行卷积等于计算朝向 N的半球 Ω中每个方向 wi的总平均辐射率。


镜面反射部分

 由于与辐射卷积相同的原因,无法以合理的性能实时地求解积分的镜面反射部分。所以最好可以预计算这个积分,以得到IBL贴图这样的东西,用片段的法线对这张图进行采样并计算。但是从这个积分中可以看出该积分与漫反射部分不同,它不仅仅依赖于wi 还依赖于 wo所以无法使用两个方向向量采样预计算的立方体图。 Epic Games 的分割求和近似法将预计算分成两个单独的部分求解,再将两部分组合起来得到后文给出的预计算结果。分割求和近似法将镜面反射积分拆成两个独立的积分:

卷积的第一部分被称为预滤波环境贴图,它类似于辐照度图,是预先计算的环境卷积贴图,但这次考虑了粗糙度。因为随着粗糙度的增加,参与环境贴图卷积的采样向量会更分散,导致反射更模糊,所以对于卷积的每个粗糙度级别,我们将按顺序把模糊后的结果存储在预滤波贴图的 mipmap 中。例如,预过滤的环境贴图在其 5 个 mipmap 级别中存储 5 个不同粗糙度值的预卷积结果,如下图所示:

我们使用 Cook-Torrance BRDF 的法线分布函数(NDF)生成采样向量及其散射强度,该函数将法线和视角方向作为输入。由于我们在卷积环境贴图时事先不知道视角方向,因此 Epic Games 假设视角方向——也就是镜面反射方向——总是等于输出采样方向ωo,以作进一步近似。翻译成代码如下:

vec3 N = normalize(w_o);
vec3 R = N;
vec3 V = R;

这样,预过滤的环境卷积就不需要关心视角方向了。这意味着当从如下图的角度观察表面的镜面反射时,得到的掠角镜面反射效果不是很好(图片来自文章《Moving Frostbite to PBR》)。然而,通常可以认为这是一个体面的妥协:

等式的第二部分等于镜面反射积分的 BRDF 部分。如果我们假设每个方向的入射辐射度都是白色的(因此L(p,x)=1.0),就可以在给定粗糙度、光线 ωi法线 n夹角 n⋅ωi 的情况下,预计算 BRDF 的响应结果。Epic Games 将预计算好的 BRDF 对每个粗糙度和入射角的组合的响应结果存储在一张 2D 查找纹理(LUT)上,称为BRDF积分贴图。2D 查找纹理存储是菲涅耳响应的系数(R 通道)和偏差值(G 通道),它为我们提供了分割版镜面反射积分的第二个部分:

 

 生成查找纹理的时候,我们以 BRDF 的输入n⋅ωi(范围在 0.0 和 1.0 之间)作为横坐标,以粗糙度作为纵坐标。有了此 BRDF 积分贴图和预过滤的环境贴图,我们就可以将两者结合起来,以获得镜面反射积分的结果:

float lod             = getMipLevelFromRoughness(roughness);
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
vec2 envBRDF          = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy;
vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 


预计算BRDF(镜面部分)

回顾一下镜面部分的分割求和近似法:

我们已经在预过滤贴图的各个粗糙度级别上预计算了分割求和近似的左半部分。右半部分要求我们在 n⋅ωo、表面粗糙度、菲涅尔系数 F0上计算 BRDF 方程的卷积。这等同于在纯白的环境光或者辐射度恒定为 Li=1.0 的设置下,对镜面 BRDF 求积分。对3个变量做卷积有点复杂,不过我们可以把 F0 移出镜面 BRDF 方程:

F为菲涅耳方程。将菲涅耳分母移到 BRDF 下面可以得到如下等式:

 

用 Fresnel-Schlick 近似公式替换右边的 F 可以得到:

 让我们用 α 替换 (1-\omega _{o}\cdot h)^{5}以便更轻松地求解 F0:

 然后将菲涅耳函数 F分拆到两个积分里:

 这样,F0在整个积分上是恒定的,我们可以从积分中提取出F0。接下来,我们将α替换回其原始形式,从而得到最终分割求和的 BRDF 方程:

 公式中的两个积分分别表示 F0 的比例偏差。注意,由于 f(p,ωi,ωo)已经包含 F 项,它们被约分了,这里的 f 中不计算 F 项。

和之前卷积环境贴图类似,可以对 BRDF 方程求卷积,其输入是 n 和 ωo的夹角,以及粗糙度,并将卷积的结果存储在纹理中。我们将卷积后的结果存储在 2D 查找纹理(Look Up Texture, LUT)中,这张纹理被称为 BRDF 积分贴图,稍后会将其用于 PBR 光照着色器中,以获得间接镜面反射的最终卷积结果。

vec2 IntegrateBRDF(float NdotV, float roughness)
{
    vec3 V;
    V.x = sqrt(1.0 - NdotV*NdotV);
    V.y = 0.0;
    V.z = NdotV;

    float A = 0.0;
    float B = 0.0;

    vec3 N = vec3(0.0, 0.0, 1.0);

    const uint SAMPLE_COUNT = 1024u;
    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(L.z, 0.0);
        float NdotH = max(H.z, 0.0);
        float VdotH = max(dot(V, H), 0.0);

        if(NdotL > 0.0)
        {
            float G = GeometrySmith(N, V, L, roughness);
            float G_Vis = (G * VdotH) / (NdotH * NdotV);
            float Fc = pow(1.0 - VdotH, 5.0);

            A += (1.0 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }
    A /= float(SAMPLE_COUNT);
    B /= float(SAMPLE_COUNT);
    return vec2(A, B);
}
// ----------------------------------------------------------------------------
void main() 
{
    vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);
    FragColor = integratedBRDF;
}

BRDF 卷积部分是从数学到代码的直接转换。我们将角度 θ 粗糙度作为输入,以重要性采样产生采样向量,在整个几何体上结合 BRDF 的菲涅耳项对向量进行处理,然后输出每个样上 F0 的系数和偏差,最后取平均值。

细节:与 IBL 一起使用时,BRDF 的几何项略有不同,因为 k变量的含义稍有不同:

由于 BRDF 卷积是镜面 IBL 积分的一部分,因此我们要在 Schlick-GGX 几何函数中使用 k_{IBL}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float a = roughness;
    float k = (a * a) / 2.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}  

最后,为了存储 BRDF 卷积结果,需要生成一张 512 × 512 分辨率的 2D 纹理。

补充:

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 coordinates
    vec3 H;
    H.x = cos(phi) * sinTheta;
    H.y = sin(phi) * sinTheta;
    H.z = cosTheta;

    // 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);
}
float RadicalInverse_VdC(uint bits) 
{
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
    return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/137262.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Hadoop YARN

Hadoop YARNYARN的组成YARN3大件的作用MR提交计算程序的过程Scheduler调度策略FIFO Scheduler&#xff08;先进先出调度&#xff09;Capacity Scheduler&#xff08;容量调度&#xff09;Fair Scheduler(公平调度)Hadoop YARN是一个是一个通用资源管理系统和调度平台&#xff0…

Ambire Wallet 2022 年度回顾

在推出的第一年&#xff0c;Ambire Wallet 确立了自己作为 EVM 领域顶级智能合约钱包解决方案之一的地位&#xff1a;拥有近 10 万个账户&#xff0c;它通过其 NPM sig lib 和多样化的沟通努力为类别应用扫清了道路。 回顾过去一年我们最大的亮点 是的&#xff0c;朋友们&#…

vue路由的介绍和使用(包括前端导航、导航守卫)

文章目录路由基本使用实现简易的前端路由安装和配置vue-router路由安装vue-router包创建路由模块导入并挂载路由模块声明路由链接和占位符使用router-link替代a连接redirect重定向嵌套路由声明子级路由链接和占位符声明嵌套路由的规则动态路由动态路由匹配(基本用法)需求:在Mov…

Java源码篇之容器类——HashMap

以下是基于jdk17 Java源码篇之容器类——HashMapconstructorput()hash()putVal()resize()treeifyBin()treeify()tieBreakOrder()balanceInsertion()moveRootToFront()checkInvariants()constructor // 无参构造 public HashMap() {this.loadFactor DEFAULT_LOAD_FACTOR; // a…

多线程并发检测触发器触发算法优化,附详细代码 - 定时执行专家

目录 ◆ V6.5版之前的并行检测方案 ◆ V6.5版之前的并行检测方案存在的问题 ◆ V6.5版本的并行检测方案 ◆ 定时执行专家 - 简介 ◆ 定时执行专家 - 最新版下载 一些用户说任务数量可能达到200个&#xff0c;让我比较惊讶&#xff0c;这个软件的设计之初并没有考虑这么多的…

MySQL调优-SQL底层执行原理

目录 MySQL调优-SQL底层执行原理 MySQL内部组件结构 Server层 Store层 连接器 客户端连接mysql数据库 创建新用户并且修改用户密码&#xff1a; show processlist 查看用户状态 客户端自动断开时间 长连接和短连接 查询缓存 常见的一些命令操作 大多数情况查询缓存…

Linux使用gdb定位Qt程序崩溃位置(systemd-coredump)

Linux提供了systemd-coredump服务&#xff0c;可以配合gdb来定位到程序崩溃位置&#xff0c;下面介绍它们的用法。 1. systemd-coredump systemd-coredump的简单介绍&#xff1a; systemd-coredump能从操作系统内核中获取内存转储&#xff0c;并能对获取到的数据进行各种处理…

Polygon zkEVM测试集——创建合约交易

1. 引言 可通过https://www.evm.codes/playground&#xff0c;来深入理解EVM各opcode中stack、memory、storage之间的关系&#xff0c;可输入任意的opcode来观察变化。 很赞的资料集&#xff1a; 深入理解合约升级(2) - Solidity 内存布局深入理解 EVM&#xff08;一&#xf…

力扣:两数之和与n数之和的(Map)与(排序+双指针)解法 【三刷终于明白HashMap求和的去重问题】

啃一本算法书啃了快一年了&#xff0c;用嘴想一想都该只剩渣了&#xff0c;脑子是怎么想的&#xff1f;&#xff1f;&#xff1f; 真希望有一天“爷啃完了&#xff0c;爷不要你了&#xff0c;爷换一本啃”&#xff0c;&#xff0c;欸欸欸&#xff1f;&#xff1f;罪过罪过&…

Python基础(一)

Python 的种类 CpythonPython的官方版本&#xff0c;使用C语言实现&#xff0c;使用最为广泛&#xff0c;CPython实现会将源文件&#xff08;py文件&#xff09;转换成字节码文件&#xff08;pyc文件&#xff09;&#xff0c;然后运行在Python虚拟机上。 JyhtonPython的Java实…

IDEA初始化git+代码提交

IDEA初始化git 当你在代码仓库如&#xff1a;github or gitee 上建立好了仓库&#xff0c;我们在idea中该如何初始化你的git又怎么样把你的代码push到代码仓库上呢&#xff1f; 第一步&#xff1a;初始化idea中的git 在idea中的setting中搜索git&#xff0c;将你的git.exe路…

JVM垃圾回收器-评估GC的性能指标

文章目录学习资料垃圾回收器概述评估GC的性能指标吞吐量&#xff08;throughput&#xff09;暂停时间&#xff08;pause time&#xff09;吞吐量VS暂停时间学习资料 【尚硅谷宋红康JVM全套教程&#xff08;详解java虚拟机&#xff09;】 【阿里巴巴Java开发手册】https://www.…

机器学习 加利福尼亚房价预测

学习目标&#xff1a; 提示&#xff1a;导入包 例如&#xff1a; import pandas as pd import numpy as npfrom sklearn.datasets import fetch_california_housing from sklearn.model_selection import KFold, train_test_split from sklearn.metrics import mean_squared…

【阶段二】Python数据分析Pandas工具使用02篇:数据读取:文本文件读取、电子表格读取与数据预处理:数据概览与清洗

本篇的思维导图: 数据读取:文本文件读取 对于csv后缀的文本文件,可以使用pandas模块中的read_csv函数进行读取。 所需要的数据文件如下百度云盘链接: 链接:https://pan.baidu.com/s/1Zj-uTt_wdRcmDt3aumZ2nA 提取码:z2e8 代码

CSRF攻防基础讲解

CSRF攻击 Cross-site request forgery跨站请求伪造 场景模拟 在用户登录某个网站后&#xff0c;看到某篇文章高兴之余&#xff0c;挥手打字&#xff0c;突然有人发来一个链接&#xff0c;登录者打开链接后什么都没有操作或者只是好奇的点击了某个按钮&#xff0c;在原登录网…

猿客栈后台管理系统日志记录

目录 一、用户权限设置 前端逻辑 后端逻辑 二、登录界面逻辑 1、账号密码登录实现 前端逻辑 在Cookie中存储token的方法 在Cookie中存储和获取的token方法 后端逻辑 生成token工具类 2、手机号登录 前台逻辑 后台逻辑 补充&#xff1a;实现点击发送验证码120s倒计…

ThinkPHP 之 SQLI审计分析(二)

说明 该文章来源于同事lu2ker转载至此处&#xff0c;更多文章可参考&#xff1a;https://github.com/lu2ker/ 文章目录说明0x00 测试代码做了什么&#xff1f;0x01 分析调用0x02 漏洞点的发现、构造、利用0x03 总结Time&#xff1a;9-3影响版本&#xff1a;ThinkPHP5.0.10 Pa…

详细介绍chrony服务器

chrony服务器 硬件时间&#xff1a;BIOS里面&#xff1b;关机后依然运行&#xff0c;主板电池为它供电&#xff1b;RTC时钟 系统时间&#xff1a;开机后&#xff0c;软件启动读取硬件时钟&#xff0c;之后独立运行 Chrony 的配置文件是/etc/chrony.conf chronyd服务器端 ch…

【目标检测】Mask rcnn代码实现Pytorch版,适用30系列显卡!(测试版)

目录&#xff1a;Mask rcnn代码实现Pytorch版一、环境二、mmdetection环境搭建三、测试四、结果展示为什么选择使用Pytorch版本&#xff1f;因为本人换电脑了&#xff0c;显卡升级为30系列&#xff0c;而30系列显卡的 CUDA 版本要求是 11.x。一、环境 cudatoolkit …

MYSQL之两阶段提交和组提交(数据一致性)

我们在MySQL 的日志中详细的介绍了undo log、redo log、binlog这三个日志和所用到的一些缓存知识&#xff0c;那么下面我们分析一下更新语句执行过程&#xff0c;它们是怎么变化的呢&#xff1f;下面我们直接给答案吧。假如我们修改一行主键索引&#xff08;id&#xff09;为1的…