bsdf = brdf + btdf
brdf:用于描述光(辐射能量)在两个物体交界处怎么反射、怎么折射
这个定律的前提是两个表面是光学光滑的,而现实世界中基本不存在这样的表面,所以又搞出来个微表面理论
蒙特卡洛路径追踪:就是计算漫反射的。
pbr就是物理光照建模,然后合理的数学解法
D为法线分布函数(NDF)
F为反射函数(Fresnel 函数)
G为阴影遮罩函数(几何函数),未被shadow或mask的比例
Unity采用pbr材质其实有多种选择的;
1.Standard Shader:建议只作为参考,实际用效率不高,pass太多、变体有三万多、内存占用太大、编译打包时间长、pbr部分可选的宏开关不多,主要是BRDF1-3,得大改才行。
2.shaderforge:pbr写法倒是简单的多,F是5次方的schlick近似、D是GGX,G是SmithJointGGXVisibilityTerm,而漫反射是disney diffuse,也支持lightmap和光泽反射,当然shaderforge本身也有多余代码,要手动处理。
3.Alloy:是一整套pbr框架,brdf部分采用的公式也都是优化的,比如基于LH优化GGX(参考http://filmicworlds.com/blog/optimizing-ggx-shaders-with-dotlh/)、Sebastien Lagarde's cheap Environment BRDF、Brent Burley's diffuse scattering BRDF等,接口方式有点像jquery(比喻不一定恰当),一个函数用完传到下一个函数,使用也挺方便的。
4.Uber shader:基于Unity standard修改的,尤其以POM、散射、glitter等为卖点,比如雪、冰块、蜡烛、皮肤等在自带的demo中看起来是不错的。
5.Amplify shader Editor:这其实和shaderforge一样是个shader编辑器,不同之处主要生成surface shader,而不是vertex shader、pixel shader,用于pbr也很方便,还有它是开放源码的,编辑器很容易扩展,发展挺快,Unity大会都推荐了,而且价格比shaderforge便宜多了。另外这家开发商的其他插件也不错,比如用于扫描材质的Amplify texture、用于色彩分离的Amplify Color、用于运动模糊的Amplify Motion等。
https://juejin.im/post/5e55f0436fb9a07cc747408d
基于精确的计算公式,使得pbr材质在任何光照下都是结果正确的
遵循各种物理原理
能量守恒(也就是物体反射出去的光量不可能超过所接收的光量):反射光 + 折射光 ≤ 入射光
Fresnel反射(所有表面反射在掠射角(grazing angles)处更加强烈)
物体表面是如何自我遮挡等原理
1、金属工作流:(不易出错,省内存,但是白边现象更明显),
1.1 Base Color :( RGB-sRGB ) 包含信息:绝缘体,反射颜色。金属,反射率。最亮色不应超越 240RGB ;最暗色应在 30-50RBG 之间;Base Color 不要包含其他光照信息,比如 Amblient Occlusion ;金属反射率应该在 70%-100% 之间。也就是RGB 范围在 180-255 。
1.2 Matallic Map :( Grayscale-Linear ):描述哪些区域是金属,那些是绝缘体。1 表示纯金属,0 表示绝缘体。如果Matalic Map 的灰度值低于 235 ,那么对应的 BaseColor 的反射率也应该降低一些。
1.3 Roughness Map :( Grayscale-Linear ):描述表面引起光纤漫反射的不规则程度。这将改变光纤的方向,但是不会改变光线的强度。光滑表面会有小而亮的高光,而粗糙表面将会出现大而暗淡的高光。在这个灰度图里面, 0 代表光滑表面,而 1 代表粗糙表面。Roughness Map 是一个“有故事”的贴图,它会告诉你这个表面经历过那些严酷(或者平和)的环境。而公用的贴图为环境光吸收,法线和高度贴图。
2、Specular工作流
2.1 Diffuse :( RGB-sRGB ):包含 Albedo Color ,但是不包含任何反射率信息。纯金属为黑色,因为纯金属没有颜色,油漆和锈迹是需要带有颜色的。基础颜色应不包含光照信息,除了 Micro-Occlusion。黑色不应低于 30-50sRGB ,亮色不应高于 240sRGB。
2.2 Specular:( RGB-sRGB ):灰度值,表示非金属 F0 。RGB 值,表示金属吸收的不同波长的光线这种贴图允许使用不同的 F0 值来表现非金属。
2.3 Glossiness:与 Roughness 相反, 0 代表粗糙表面,而 1 代表光滑表面
3、Ambient Occlusion :表示有多少环境光,可以被表面上的一个点吸收。AO 应该只影响 Diffuse 部分,而不应该阻塞Specular部分。
4、Height Map:用于视差映射,通过增加可见的深度感来提高渲染的真实性。
5、Normal Map:不论PBR还是非PBR,NormalMap都是用来模拟表面细节,同时还会影响Roughness和Glossiness映射。
七个PBR相关CG头文件
UnityStandardBRDF.cginc头文件 – 用于存放标准着色器处理BRDF材质属性相关的函数与宏
UnityStandardConfig.cginc头文件 – 用于存放标准着色器配置相关的代码(其实里面就几个宏)
UnityStandardCore.cginc头文件 – 用于存放标准着色器的主要代码(如顶点着色函数、片段着色函数等相关函数)
UnityStandardInput.cginc头文件 – 用于存放标准着色器输入结构相关的工具函数与宏
UnityStandardMeta.cginc头文件 – 用于存放标准着色器meta通道中会用到的工具函数与宏
UnityStandardShadow.cginc头文件 – 用于存放标准着色器阴影贴图采样相关的工具函数与宏
UnityStandardUtils.cginc头文件 – 用于存放标准着色器共用的一些工具函数
StandardShaderGUI.cs脚本文件 – 定义了特定的自定义编辑器UI界面
https://blog.csdn.net/Neil3D/article/details/83783638
https://blog.csdn.net/Neil3D/article/details/84112481
https://neil3d.github.io/assets/pdf/cookpaper.pdf
反射率方程
原始的反射率方程是在半球域上对立体角(Solid Angle)进行积分
辐射率(Radiance)
可错略理解为物体表面一点向指定方向的光能辐射强度
BRDF(Bidirectional Reflective Distribution Function),中文译为双向反射分布函数。
计算结果是[0,1]的值,对于一个完美的镜面反射来说,我们可以按照几何光学来计算任何一个入射光
的反射方向,那么 BRDF 应该只在这个反射方向 上返回 1.0,其他任何方向上都应该得到 0.0 的结果
立体角(Solid Angle)
立体角并不是一个角度,而是一个面积。它是单位球面上的一个面积
一个无限小的立体角可以理解成一条射线
Cook-Torrance反射模型(包含漫反射和镜面反射量部分)
Rbd =kd*Rd+ks*Rs[漫反射的计算Rd,镜面反射Rs]
其中kd+ks=1.0
漫反射可使用Lambertian漫反射模型 或者 迪斯尼漫反射模型DisneyDiffuse
漫反射是指光进入物体内部,经由物体内部的粒子反射之后,呈现随机方向离开物体的现象。对于金属来说所有的折射光都会被吸收,所以完美的金属漫反射Rd为0,只有镜面反射Rs
微平面理论(The Microfacet Model)
从微观角度看,物体表面是由一些细小的平面组成的,越光滑的表面,这些微平面排列的越规则,越粗糙的表面,这些微平面排列的越不规则
可以从统计学的角度使用粗糙度(Roughness)这样一个估算的参数来描述这一现象
镜面反射
D:微平面在平面上的分布函数(Normal distribution function)/法线分布函数(实际是h向量分布,用法线称呼更直观)
含义:这个函数计算得出所有微平面中法向量与 n 一致的微平面的比例。假设表面的宏观法向量为 n ,例如,如果有 20% 的微表面的法向量与 n 一致,则这个函数返回 0.2
虚幻4采用 Trowbridge-Reitz GGX 模型
n 为表面的宏观法向量
h 入射光和观察方向的中间向量
α 为表面的粗糙度参数
G:计算微平面由于互相遮挡而产生的衰减
含义:由于微平面的不规则排列,反射光有一定比率会被物体自身的微平面遮挡而衰减
虚幻4使用 Schlick 模型结合 Smith 模型计算此项
F:菲涅尔项
金属以外的物体表面都会具有菲涅尔效果
F0为基础反射率,是一个常数
https://blog.csdn.net/Neil3D/article/details/84112481
虚幻4中Cook-Torrance BRDF的计算和优化方法:
使用了预计算,近似,IBL(Image Based Lighting),以及蒙特卡洛积分(Monte Carlo integration)
使用蒙特卡洛积分将能够预计算的部分使用蒙特卡洛积分求出数值解,以贴图的方式存储起来
分割求和近似法(Split Sum Approximation)(虚幻4使用这个模型将Cook-Torrance BRDF积分分成两部分进行预计算)
一部分的计算结果存储到一个 Cube Map 上,管它叫做“Pre-Filtered Environment Map”;
另外一部分的计算结果存储为一张 R16G16 格式的2D贴图,管它叫做:“Environment BRDF”。
基于近似去优化积分的性能(近似后得到的模型称为Split Sum Approximation)
第一步近似是用蒙特卡洛积分公式,其中的 p(lk,v) 就是概率分布函数:pdf,要说明的是:对于渲染方程,pdf 是一个归一化函数(normalized function),即在半球域内的积分值为 1
第二步近似是把一个 ∑ 拆分成了 ∑⋅∑,这两个 ∑ 要分别使用蒙特卡洛积分去计算,并且都使用了重要性采样(Importance Sampling)
重要性采样(Importance Sampling),作用是进行加速,降低误差
对于非常光滑的表面,那么我们应该在出射光方向(观察方向)的镜面反射方向周围分配更多的样本。这种采样的分布控制就是通过 pdf 函数实现的。
虚幻4中使用了基于 GGX 分布函数的重要性采样来提高蒙特卡洛积分的精确度。GGX 函数的一个重要参数就是粗糙度
虚幻4中对应的代码:“Epic Games\UE_4.20\Engine\Shaders\Private\MonteCarlo.ush
其中 ImportanceSampleGGX(E, a2) 的第一个参数 E,它的取值是 Hammersley Sequence;第二个参数 a2 的取值是粗糙度的四次方
float4 ImportanceSampleGGX( float2 E, float a2 )
{
float Phi = 2 * PI * E.x;
float CosTheta = sqrt( (1 – E.y) / ( 1 + (a2 – 1) * E.y ) );
float SinTheta = sqrt( 1 – CosTheta * CosTheta );
float3 H;
H.x = SinTheta * cos( Phi );
H.y = SinTheta * sin( Phi );
H.z = CosTheta;
float d = ( CosTheta * a2 – CosTheta ) * CosTheta + 1;
float D = a2 / ( PI*d*d );
float PDF = D * CosTheta;
return float4( H, PDF );
}
Hammersley Sequence
这是一个低差异序列(Low-Discrepancy Sequence),你可以粗略的理解为:它生成一系列分布更为均匀的随机数
虚幻4中对应的代码:Epic Games\UE_4.20\Engine\Shaders\Private\MonteCarlo.ush
float2 Hammersley( uint Index, uint NumSamples, uint2 Random )
{
float E1 = frac( (float)Index / NumSamples + float( Random.x & 0xffff ) / (1<<16) );
float E2 = float( ReverseBits32(Index) ^ Random.y ) * 2.3283064365386963e-10;
return float2( E1, E2 );
}
GGX Distribution
pdf 函数是一个基于 GGX 的分布函数
这个模型的目的是想要达到的采样分布 出射光方向的反射方向,采样更多,此外的区域采样更少。
因为反射向量的分布就是收到微平面的法向量分布影响的,所以
pdf 函数在 Trowbridge-Reitz GGX 模型的基础上附加一些计算,就形成了 ImportanceSampleGGX() 函数
计算Split Sum的第一部分(预滤波环境贴图)
对环境贴图进行卷积,它的计算结果仍然是一个 Cube Map,也就是“Pre-Filtered Environment Map
使用 GGX 概率分布函数的话,需要观察方向V和表面法线N,这两个都是运行时才能得到的,离线模型下在虚幻4的预计算中,假设 N=V=R ,也就是观察角度为 0。 这是引起误差最大的一个近似
虚幻4使用下面这个函数:PrefilterEnvMap(),生成“Pre-Filtered Environment Map”。这个函数有两个参数:粗糙度和反射方向:
每一个 Mip Map Level 对应一个“粗糙度”值(也就是这里的mipmap不是常用的分辨率的那个mipmap,而是根据着色表面的粗糙度来构造mipmap的层级,每一个mipmap等级图都由不同的粗糙度计算得到)
对于每个 Mip Map Level,每一个贴图像素(texel)就对应一个反射方向;
对于每个 Mip Map Level 上的每个 texel 运行此函数进行卷积,即可得到这部分积分的预计算结果。这个函数计算的过程中用到了Hammersley()和ImportanceSampleGGX()
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;
}
计算Split Sum的第二部分(预计算 BRDF,公式中的两个积分分别表示 F0 的比例和偏差)
计算的结果是一个2D查找表,存储到一个 R16G16 的 BRDF积分贴图 中,叫做“Environment BRDF”
查找纹理存储是菲涅耳响应的系数(R 通道)和偏差值(G 通道)
使用以下函数计算出这张图,两个输入变量:cosθv(NdotV)作为横坐标,粗糙度作为纵坐标,取值范围都是[0,1]
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;
}
运行时IBL计算,通过采样 Pre-Filtered Environment Map 和 Environment BRDF 来进行 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);
}
https://blog.csdn.net/Neil3D/article/details/84112481
蒙特卡洛积分(Monte Carlo integration)
对于一些复杂的高维定积分,在图形学中通常使用蒙特卡洛方法
用蒙特卡洛公式公式的意思就是在指定的范围内随机取 N 个只,并计算出相应的 f(x) 值。这些值的平均值就是对理想积分的一个近似数值解
pdf即 probability distribution function,概率分布函数,含义是特定样本在整个样本集上发生的概率
IBL(Image Based Lighting)
作用:实际上就是做了环境反射.预计算省掉运行时计算的消耗
用于:皮肤的LUT,通过对蒙特卡洛积分 预计算好的环境光贴图 采用得到 环境光反射/间接光
渲染方程的直观理解就是:计算空间中任意一点所接收到的所有光线在观察方向上的反射。Image Based Lighting 简称 IBL,可以用来计算场景中一点接收到的场景中其他静态对象产生的光照。
把场景中某个点的光照信息,存储到一个 Cube Map 中,就叫做“Light Probe”,虚幻4中叫做“Reflection Capture”,
如下图所示。Light Probe 的生成一般是先把这个点周围的场景静态物体渲染到一张环境贴图中(environment map),然后再使用 BRDF 对其进行预计算,形成光照计算所需的数据。
在 PBR 中,通常会把不同的“粗糙度”计算结果存储到不同的 Mip Map Level 中。也就是说 Light Probe 的 Cube Map 的 Mip Map 不是传统的贴图的 Mip Map 的概念,而是针对不同“粗糙度”的 BRDF 计算结果。
例如,一张有10个 Mip Map Level 的 Cube Map ,每个 Level 对应的就是“粗糙度=0.1*n”。把环境贴图预计算生成 Light Probe 的过程在数学上叫做“卷积(Convolution)”。
理论上,一个 Light Probe 只对应场景中的一个点,而实时渲染中就把一个模型上的所有点都近似的取这个 Light Probe 了。那么,对于空间跨度很大的场景,咱们也不能太“凑合”了!
于是,我们可以在场景中的不同位置放置多个 Light Probe,通常是你认为重要的位置,并且可以设置它的体积等参数。在运行时,按照着色的点的位置,对几个 Light Probe 进行插值。这种东西有个名词,
叫做“Local Light Probes”。与 Local Light Probes 对应的就是 “Global Light Probe”。我们可以用一个 Light Probe 对应无限远的光源所产生的环境光,通常就是天空,每个场景可以包含一个 Global Light Probe 。
总结:
把场景中的静态环境,或者是天空,存储到一个环境贴图中;
环境贴图中的每个像素可以想象成一个发射间接光的光源;使用 BRDF 对这些小小光源进行预计算,也就是所谓的卷积,从而生成 Light Probe;
在运行时,根据需要着色的点的位置,找到合适的 Local Light Probes(可能是多个),还有 Global Light Probe;
分别根据粗糙度和反射向量,对这些 Light Probe 进行采样,然后插值;
这个计算结果就是渲染中所需要的环境光反射量。
在unity中使用
为了尽量真实的模拟粗糙表面的环境反射,需要给 Cubemap 做一层 Blur,在引擎里面为了优化是采用了texCubelod,只需要把 Cubemap 的 lod 打开,然后通过 Roughness 来决定所采 lod 的层级,这样就可以达到粗糙表面的模糊反射
Unity用reflection probe来保存环境光贴图,通过内置变量unity_SpecCube0,unity_SpecCube1访问。IBL就是采样两次,用粗糙度做插值。这个地方可以做些优化。
cubemap贴图制作:https://blog.csdn.net/yangxuan0261/article/details/88976957
全局光照Global Illu mination(GI)
局部光照系统就是由光源+待渲染物体+视点组成的话,那么全局光照系统就是由光源+各待渲染物体之间的反射光+待渲染物体+视点组成
roughness从[0,1]缩放到[0.5,1],会有更好的效果