相关资料

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. Spark在处理数据的时候,会将数据都加载到内存再做处理吗?

    对于Spark的初学者,往往会有一个疑问:Spark(如SparkRDD.SparkSQL)在处理数据的时候,会将数据都加载到内存再做处理吗? 很显然,答案是否定的! 对该问题产生疑问的根源还是对Sp ...

  2. Mybites学习

    参考链接:https://www.cnblogs.com/dongying/p/4073259.html <select <!-- 1. id (必须配置) id是命名空间中的唯一标识符, ...

  3. MYSQL 悲观锁和乐观锁简单介绍及实现

    1:悲观锁 1.1 特点: 每次查询都会进行锁行,怕"其他人"进行数据的修改. 1.2 实现步骤: 步骤1:开启事务test1,并对id=2的记录进行查询,并加锁,如:   步骤2 ...

  4. 痞子衡嵌入式:盘点国内RISC-V内核MCU厂商

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是国内RISC-V内核MCU厂商. 虽然RISC-V风潮已经吹了好几年,但2019年才是其真正进入主流市场的元年,最近国内大量芯片公司崛起 ...

  5. 使用Maven新建SpringBoot工程

    最近用IDEA插件创建Springboot项目,总是403,估计被墙了! 那么这里在提供两种方法 1.从官网下载模板,导入IDEA内 2.使用Maven创建 方法一:打开 https://start. ...

  6. 微信小程序折线图表折线图加区域图

    1.先来个效果图 这里我用的是插件@antv/f2-canvas(安装的方法如下) npm init 此处如果直接使用官方npm install 可能会出现没有node_modules错误 npm i ...

  7. CSDN博客转MD格式

    基于大神作品修改原文,使用了一下发现有一些小问题,爬取的博客标题如果含有字符是Windows不支持的命名格式,会卡在界面,进行了一下优化,加了一些字符过滤处理,但是tomd模块对html的处理还是不是 ...

  8. golang——win10环境protobuf的使用

    1.protobuf配置 (1)https://github.com/protocolbuffers/protobuf/releases (2)选择适合的版本:protoc-3.8.0-win64.z ...

  9. golang 并发运算时主线程先运行完,子线程运行没结束的问题记录

    代码如下: blocks,err:= mgo.FindBlocks(batch) //获得当前批次下的矿体信息 cubes:= BlockCutting(blocks[0],x,y,z,nest) f ...

  10. mysql 单机多实例重启数据库服务

    1.# cat db.txtbackend 3310base 3320storage 3330payment 3340promotion 3350 2.# cat restart_mysql_slav ...