相关资料

https://www.cnblogs.com/dojo-lzz/p/13237686.html

文档:PBR学习笔记.note
对于之前的这篇文章中,基本了解了PBR分解后的各个子项意思,但是对于最后一个IBL的解释实际上还是有些牵强。这几天了解到了蒙特卡洛积分以及基于重要性采样的蒙特卡洛几分才算是对这部分有个比较透彻的了解。
参考资料先存下来:
 

理论

PBR是基于物理的渲染,核心是从能量守恒角度将各个方向的光源进行积分。BRDF就是根据物体的各种性质经过一系列实验得到的一个双向反射分布函数来进行模拟。在之前那篇文章中,首先对于环境中已有的点光源和方向光源分别进行caculatorFinalColor处理根据diffuse和specular;
vec3 calculateFinalColor(PBRInfo pbrInputs, vec3 lightColor) {
// Calculate the shading terms for the microfacet specular shading model
vec3 F = specularReflection(pbrInputs);
float G = geometricOcclusion(pbrInputs);
float D = microfacetDistribution(pbrInputs); // Calculation of analytical lighting contribution
vec3 diffuseContrib = (1.0 - F) * diffuse(pbrInputs);
vec3 specContrib = F * G * D / (4.0 * pbrInputs.NdotL * pbrInputs.NdotV);
// Obtain final intensity as reflectance (BRDF) scaled by the energy of the light (cosine law)
return pbrInputs.NdotL * lightColor * (diffuseContrib + specContrib);
}
对于环境中光源的处理已经完成了,但是前面说到PBR是对各个方向的光进行积分,即对环境中各个方向能够反射进入人眼中光都需要处理。
 
首先光怎么来,可以认为是从环境贴图中来,我们就认为环境贴图中每个像素颜色代表代表一个微分光源,也就是说要对环境贴图中所有纹理进行遍历求和,这个过程显然对于实时渲染时不可接受的,这么这个时候就出现了蒙特卡洛积分。蒙特卡洛积分的思想是在整个积分区间内,随机的进行有限个采样,通过采样点的均值来进行近似。
想具体了解下蒙特卡洛积分的,可以看这篇文章,这是迄今我见过最通俗易懂的文章:https://blog.csdn.net/i_dovelemon/article/details/76286192
但是呢光有基础蒙特卡罗并不行,因为我们是随机采样,每个采样点实际上对于整体的贡献度是不相同的,所以我们还需要计算每个采样点对于整体的权重情况,那么这个计算重要性的过程与蒙特卡洛结合就称为基于重要性的蒙特卡洛积分https://blog.csdn.net/i_dovelemon/article/details/76786741
 
现在整个公式变成这个样子了
再来现在采样有了,权重有了,还有一个问题要解决就是采样的分布情况。如果都集中在高权重或低权重对整体结果的影响是很大的,所以图形学在这个过程中有专门的一个采样序列的问题。通过一个有特点的采样函数来生成一些采样点,这里使用的是Hammersley采样序列算法https://blog.csdn.net/i_dovelemon/article/details/76599923
好了,下面来看下我们汇总后的代码(我们对于环境光来说漫反射部分实际上各个方向都是一样的,所以这里重点看镜面反射部分):
float3 SpecularIBL( float3 SpecularColor , float Roughness, float3 N, float3 V )
{
float3 SpecularLighting = 0;
const uint NumSamples = 1024; // 使用了1024个采样点
for( uint i = 0; i < NumSamples; i++ )
{
float2 Xi = Hammersley( i, NumSamples ); // 计算一个随机采样序列 float3 H = ImportanceSampleGGX( Xi, Roughness, N ); // 将一个二维采样序列转换成三维空间中的采样方向
// 下面是计算法线、采样方向、视线等各种方向的一堆夹角
// 看上图L是从一个随机采样方向计算出得到的环境入射光源的反方向
float3 L = 2 * dot( V, H ) * H - V;
float NoV = saturate( dot( N, V ) );
float NoL = saturate( dot( N, L ) );
float NoH = saturate( dot( N, H ) );
float VoH = saturate( dot( V, H ) );
if( NoL > 0 )
{
// 计算环境光源颜色,envMap很可能是立方体贴图
float3 SampleColor = EnvMap.SampleLevel( EnvMapSampler , L, 0 ).rgb;
// 下面是计算BRDF的specular部分的G和F,这里并没有计算D,因为在BRDF/pdf过程中,D被消除掉了。
float G = G_Smith( Roughness, NoV, NoL );
float Fc = pow( 1 - VoH, 5 );
float3 F = (1 - Fc) * SpecularColor + Fc;
// Incident light = SampleColor * NoL
// Microfacet specular = D*G*F / (4*NoL*NoV)
// pdf = D * NoH / (4 * VoH)
// 上面pdf公式可以看出重要性跟粗糙度、法线与采样方向、视线与采样方向相关的。粗糙度是一个经过物理实验测量的值
SpecularLighting += SampleColor * F * G * VoH / (NoH * NoV);
}
}
return SpecularLighting / NumSamples; // 求和再取均值就是蒙特卡罗积分的体现
}
上文中讲到了先有采样序列,然后是将一个二维随机数映射为采样方向,下面就来看下这个过程:
要理解这个,得要有立体角的概念,这个概念如果不明白可以搜一搜,讲的挺多的。
 
 

对于一个微分立体角来说,要确定一个向量只需要有两个量,phi和theta;这两个量就是通过上文中的采样序列生成的。下面要做的就是把这微分立体角坐标转换成三维空间坐标。(注意:图片中说的镜面反射方向应该是法向量方向,这里可能是GPU GEM中文作者翻译错误了)
float3 ImportanceSampleGGX( float2 Xi, float Roughness, float3 N )
{
float a = Roughness * Roughness;
float Phi = 2 * PI * Xi.x; // 水平方向的phi
// theta不知道是怎么计算出来的,可能也是根据一个数学理论来计算的,这里可以看到有将粗糙度考虑进去
float CosTheta = sqrt( (1 - Xi.y) / ( 1 + (a*a - 1) * Xi.y ) );
float SinTheta = sqrt( 1 - CosTheta * CosTheta );
float3 H;
// 根据微分立体角坐标求得以该表面为原点,镜面反射方向为微分球的局部三维坐标系
H.x = SinTheta * cos( Phi );
H.y = SinTheta * sin( Phi );
H.z = CosTheta;
// 求表面切空间的的基底,切空间基底向量坐标系为世界坐标系
float3 UpVector = abs(N.z) < 0.999 ? float3(0,0,1) : float3(1,0,0);
float3 TangentX = normalize( cross( UpVector, N ) );
float3 TangentY = cross( N, TangentX );
// Tangent to world space
// 下面这个操作的前提是需要微分球的纵轴方向要与该点法向量的轴重合才行,而不是图片中说的镜面反射方向,这里可能是GPU GEM中文作者翻译错误了
// 因为微分球转换的三维坐标与且空间重合,所以这里是将微分三维坐标进行向量分解,最终得到一个三维空间坐标下的单位向量
return TangentX * H.x + TangentY * H.y + N * H.z;
}
可以看到这是在实时渲染情况下的计算过程,根据GPU 精粹3中第20章的结果来看,并不用1024次采样只需要40几次即可。
这种实时渲染的优点真的实时渲染,不需要提前生成BRDF 的LUT查找表,但问题也是每帧都这么计算还是很耗费性能,所以后来的大牛各种研究,就是我们在PBR学习笔记中看到的那部分,在IBL这部分直接读取纹理。
 
 
其实在图形学继续深入的研究方向比如仿真、光线追踪,大部分处理过程都是一个近似过程,这也是为什么像GPU精粹、GPU Pro、GPU Zen中经常开头一复杂积分,到了最后代码过程其实还是简单的加减乘除和循环。所以在整个仿真的过程中,首先通过物理理论研究完成完整计算公式,后续进行一步步近似和简化,逐渐转化成GPU可执行的代码程序。这也是图形学奇高的门槛,一个数学公式让人望而生畏,转头就跑。好了下面说一下怎么进行进一步的近似处理。
 
经过一系列数学研究之后拆成了两个公式。拆成这两部分的优势是,两部分都可以做预处理,通过程序提前写入纹理中,实时渲染只需要去纹理中去查询即可,做简单的加减乘除。
前一部分可以预处理成跟roughness和cos(v)相关的光照颜色,原理跟上面实时渲染差不多,取了采样了一批环境纹理的颜色,取均值。

float3 PrefilterEnvMap( float Roughness, float3 R )
{
float3 N = R;
float3 V = R;
float3 PrefilteredColor = 0;
const uint NumSamples = 1024;
for( uint i = 0; i < NumSamples; i++ )
{
float2 Xi = Hammersley( i, NumSamples );
float3 H = ImportanceSampleGGX( Xi, Roughness, N );
float3 L = 2 * dot( V, H ) * H - V;
float NoL = saturate( dot( N, L ) );
if( NoL > 0 )
{
PrefilteredColor += EnvMap.SampleLevel( EnvMapSampler , L, 0 ).rgb * NoL;
TotalWeight += NoL;
}
}
return PrefilteredColor / TotalWeight;
}
第二部分,最终而后半部分可以转化为一个kx+b的线性函数,这里,只有x未知(可以认为这里的x是物体的specularColor),而k,b只跟roughness和cos(v)有关。而且roughness和cos(v)都在[0,1]之间,所以,我们可以生产一张如下的纹理,建立一张映射表,通过roughness和cos直接找到对应的k,b值,省去中间大量的采样计算。当然,纹理越大,越精确,但毕竟只是[0,1]之间的插值,所以是近似值。
 
虽然大部分地方显示的是上面的图,但是在这个库中https://github.com/KhronosGroup/glTF-Sample-Viewer/tree/glTF-WebGL-PBR,使用的是下面的图,看到y轴刚好反了下:https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/assets/images/lut_ggx.png
 
而在上一期文章中,获取brdf的代码中给纹理的坐标也确实做了1减的处理
好,回过头来,根据UE4的方案代码如下:
float2 IntegrateBRDF( float Roughness, float NoV )
{
float3 V;
V.x = sqrt( 1.0f - NoV * NoV ); // sin
V.y = 0;
V.z = NoV; // cos
float A = 0;
float B = 0;
const uint NumSamples = 1024;
for( uint i = 0; i < NumSamples; i++ )
{
float2 Xi = Hammersley( i, NumSamples );
float3 H = ImportanceSampleGGX( Xi, Roughness, N );
float3 L = 2 * dot( V, H ) * H - V;
float NoL = saturate( L.z );
float NoH = saturate( H.z );
float VoH = saturate( dot( V, H ) );
if( NoL > 0 )
{
float G = G_Smith( Roughness, NoV, NoL );
float G_Vis = G * VoH / (NoH * NoV);
float Fc = pow( 1 - VoH, 5 );
A += (1 - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
return float2( A, B ) / NumSamples;
}
这里有个库,可以用来生成这种查找表:https://github.com/HectorMF/BRDFGenerator
那么按照UE4的论文,根据近似公式之后,这部分IBL的着色器代码变为:

float3 ApproximateSpecularIBL( float3 SpecularColor , float Roughness, float3 N, float3 V )
{
float NoV = saturate( dot( N, V ) );
float3 R = 2 * dot( V, N ) * N - V;
float3 PrefilteredColor = PrefilterEnvMap( Roughness, R );
float2 EnvBRDF = IntegrateBRDF( Roughness, NoV );
return PrefilteredColor * ( SpecularColor * EnvBRDF.x + EnvBRDF.y );
}
可以看到完美对应近似公式,上面说的第二部分拆成了kx+b的形式
 
在PBR学习笔记1文章中,可以看到那里的处理方式,除了specular外还考虑了diffuseLight,也就是需要烘焙一张diffuseLight的纹理,这个纹理只跟法向量有关
 

那么IBL这部分基本介绍完了,另外需要注意的是,BRDF有很多种实现方式,生成LUT也不一样最常见的是上面那种红绿的形式,另外还有:
有这个模样的,
 
 
以及这个模样的:
可以在这个库里看到:
对于BRDF中的FDG几个方面也有不同的实现,比如考虑了各向异性情况的,都在上面那个链接中,可以看一下。
 
如果有耐心看完,并把PBR研究透,基本也是开始摸到了光线追踪的门槛
 
 

PBR(基于物理的渲染)学习笔记2的更多相关文章

  1. PBR(基于物理的渲染)学习笔记

    PBR基本介绍 PBR代表基于物理的渲染,本质上还是 gl_FragColor = Emssive + Ambient + Diffuse + Specular 可能高级一些在考虑下AO也就是环境光遮 ...

  2. PBR:基于物理的渲染(Physically Based Rendering)+理论相关

    一: 关于能量守恒 出射光线的能量永远不能超过入射光线的能量(发光面除外).如图示我们可以看到,随着粗糙度的上升镜面反射区域的会增加,但是镜面反射的亮度却会下降.如果不管反射轮廓的大小而让每个像素的镜 ...

  3. Canvas 数学、物理、动画学习笔记一

    Canvas 第五章 数学.物理和运动学习笔记让人映像深刻的运动,需要我们不只是简单的知道如何移动对象,还需要知道怎么按用户期望看到的方式去移动它们.这些需要基于数学知识的基本算法和物理学作用.基于点 ...

  4. 基于物理的渲染——间接光照

    在前面的文章中我们已经给出了基于物理的渲染方程: 并介绍了直接光照的实现.然而在自然界中,一个物体不会单独存在,光源会照射到其他的物体上,反射的光会有一部分反射到物体上.为了模拟这种环境光照的形式,我 ...

  5. 离屏渲染学习笔记 /iOS圆角性能问题

    离屏渲染学习笔记 一.概念理解 OpenGL中,GPU屏幕渲染有以下两种方式: On-Screen Rendering 意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行. O ...

  6. MVC中使用Entity Framework 基于方法的查询学习笔记 (三)

    紧接上文,我们已经学习了MVC数据上下文中两个常用的类,这两个类承载着利用函数方式进行数据查询的全部内容,我们既然已经了解了DbSet<TEntity> 是一个泛型集合,并且实现了一些接口 ...

  7. MVC中使用Entity Framework 基于方法的查询学习笔记 (一)

    EF中基于方法的查询方式不同于LINQ和以往的ADO.NET,正因为如此,有必要深入学习一下啦.闲话不多说,现在开始一个MVC项目,在项目中临床学习. 创建MVC项目 1.“文件”--“新建项目”-- ...

  8. 基于PHP的AJAX学习笔记(教程)

    本文转载自:http://www.softeng.cn/?p=107 这是本人在学习ajax过程所做的笔记,通过本笔记的学习,可以完成ajax的快速入门.本笔记前端分别使用原生态的javascript ...

  9. 基于python的接口测试学习笔记一(初出茅庐)

    第一次写博客笔记,讲一下近来学习的接口自动化测试.网上查阅了相关资料,最后决定使用python语言写接口测试,使用的是python的第三方库requests.虽然python本身标准库中的 urlli ...

随机推荐

  1. js中this指向的问题与联系

    前言 JavaScript 中最大的一个安全问题,也是最令人困惑的一个问题,就是在某些情况下this的值是如何确定的.有js基础的同学面对这个问题基本可以想到:this的指向和函数调用的方式相关.这当 ...

  2. 6. vue组件详解(一)

    主要内容: 1. 组件的基本使用 2. 全局组件和局部组件 3. 父组件和子组件 4. 组件语法糖的写法 5. 组件data关联的写法 6. 父子组件的通信 组件系统是 Vue 的一个重要概念,因为它 ...

  3. Spring注解@PropertySource加载配置文件和SpringBoot注解@Value、@ConfigurationProperties进行属性映射

    SpringBoot的配置文件 位置:resources目录下 配置文件的作用: (1).SpringBoot是基于约定的,所以很多配置都有默认值,但如果想使用自己的配置替换默认配置的话,就可以使用a ...

  4. pandas的数据筛选之isin和str.contains函数

    筛选是在平时的工作中使用非常频繁的功能,前文介绍了loc和iloc的筛选方法,现在继续介绍一些筛选的方法.   DataFrame列表 以>,<,==,>=,<=来进行选择(& ...

  5. Newbe.Claptrap 框架入门,第二步 —— 创建项目

    接上一篇 Newbe.Claptrap 框架入门,第一步 -- 开发环境准备 ,我们继续了解如何创建一个 Newbe.Claptrap 项目. Newbe.Claptrap 是一个用于轻松应对并发问题 ...

  6. mysql 单表下的字段操作

    如下只介绍单表的添加.更新.删除.查询表结构操作,查询数据操作范围太大用单独的篇幅来讲解: 查看表结构 desc test_tb; Insert 插入数据 插入 = 添加 为表中指定的字段插入数据 C ...

  7. mysql查询较长的执行进程及创建权限账号

    A:对于死锁,进程的操作 1.查找当前活跃事务 SELECT * from information_schema.INNODB_TRX 根据trx_started等判断事务是否异常锁定 2.杀死线程 ...

  8. 【译】Visual Studio 的 Razor 编辑器的改进

    自从我们在一个通用的 Razor 语言服务器上发布了 Visual Studio 的一个新的实验性 Razor 编辑器的第一个预览版以来,已经过去了6个月,现在是时候更新一下我们的进展了.团队一直在努 ...

  9. 混合编程:如何用python11调用C++

    摘要:在实际开发过程中,免不了涉及到混合编程,比如,对于python这种脚本语言,性能还是有限的,在一些对性能要求高的情景下面,还是需要使用c/c++来完成. 那怎样做呢?我们能使用pybind11作 ...

  10. 2.pandas常用读取

    一.文本读写 名称 接收 代表(含义) 默认 filepath string 文件路径 无 sep string 分割符 ',' header Int/sequence 某行做列名 infer自动寻找 ...