直接光照部分
BRDF方程
漫反射:兰伯特光照模型,除以π是为了能量守恒
高光反射
D项
这个式子被称为Trowbridge-Reitz GGX,是高光反射项的D项,法线分布项。
其中的h为半角向量,n为法线,α是粗糙度相当于roughness或者1 - smoothness,
unity内部实现的roughness(或者α) = (1 - smoothness) * (1 - smoothness)
可以看出
粗糙度α = 0的时候,也就是完美光滑的情况,得到的结果是0,那么就会返回一团黑色,在unity内部做了一个插值lerp(0.002,1,roughness),这样可以保证带一点点高光。
粗糙度α = 1的时候,也就是完美粗糙的情况,得到的结果是1/π,是一个偏灰色的值。
当α从(0 - 1)之间过渡的时候(D项用的是GGXTR,return float4(D,D,D,1));
G项
F项
菲涅尔方程描述的是被反射的光线对比光线被折射的部分所占的比率,这个比率会随着我们观察的角度不同而不同。当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度告诉我们被反射的光线所占的百分比。利用这个反射比率和能量守恒原则,我们可以直接得出光线被折射的部分以及光线剩余的能量。
当垂直观察的时候,任何物体或者材质表面都有一个基础反射率(Base Reflectivity),但是如果以一定的角度往平面上看的时候所有反光都会变得明显起来。当用垂直的视角观察你自己的木制/金属桌面,此时一定只有最基本的反射性。但是如果你从近乎90度(指和法线的夹角)的角度观察的话反光就会变得明显的多。如果从理想的90度视角观察,所有的平面理论上来说都能完全的反射光线。
方程中的F0理论上是平面的基础反射率,但实际实现时需要考虑另一个情况,即:
菲涅尔方程只对非金属有效(因为金属本身反射率就高,所以在不同角度下的菲涅尔反射不明显),在表面为金属时需要用到跟金属表面颜色相关的另一个方程。为了能够用同一个材质表示金属和非金属的不同属性,将材料的金属性参数整合到F0的计算中。
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
unity_ColorSpaceDielectricSpec.rgb = float3(0.04,0.04,0.04);
F0的计算就是在这个常数和表面颜色之间根据材质的金属性进行插值。
(当然我在unityPBS里面看到的F0只是从_SpecularMap中采样的rgb值。
2017的版本没有_SpecularMap了,把大部分参数都浓缩到Metallic和Smoothness)
而在普通的计算环境映射的时候用的F0是一个自定义的系数(0-1)。
float f = .....
float3 reflectionColor = texCube(_CubeMap,i.reflection);
float3 finalColor = lerp(albedo,reflectionColor ,f);
所以就表明在BRDF的方程中F0是一个颜色值,其他情况也可以当做一个变量。
直接光照部分如下:
这里的kd漫反射系数
float3 kd = (1 - F)*(1 - _Metallic);
1-F是为了能量守恒,1-_Metallic是因为非金属才会吸收能量,金属会更多的反射能量显示高光
这里用1-F是因为:
当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度给出被反射的光线所占的百分比。
利用这个反射比率和能量守恒原则,我们可以直接得出光线被折射的部分以及光线剩余的能量。
还有一个是因为
红圈的两个是系数,绿框里得到的是颜色,很容易把高光反射方程
当做系数,虽然它里面也有F项,也就是ks系数。
间接光照部分
vert着色器中计算ambientOrLightmapUV
计算VertexGI的部分可以看到,有三个分支
1,
如果启用静态光照,会把
half4 ambientOrLightmapUV的xy分量记录下uv位置
2,
非重要光源的计算,前4个按照顶点光照计算,其余的按照球谐光照计算。(为什么是4个呢
可能是float4吧)
3,
动态光照的话,把half4 ambientOrLightmapUV的zw分量记录uv
总结的话,就是在顶点着色器中会根据不同情况,给ambientOrLightmapUV赋值uv值或者直接返
回颜色值。
计算间接漫反射
上图是UnityGlobalIllumination中UnityGI_Base的简化版本
可以看到和VertexGIForward函数能对应上,并且在最后和occlusion相乘,occ是环境光遮蔽,间接漫反射就属于环境光。
1,
如果启用静态光照,把在顶点着色器中赋值好的ambientOrLightmapUV.xy作为光照贴图采样的uv
然后解码得到间接漫反射。
UNITY_SAMPLE_TEX2D和tex2D一样
DecodeLightMap可根据不同平台解码,
Unity烘焙的LightMap事32位的HDR图,在PC端光照贴图编码为RGBM,移动端为double-LDR
2,
非重要光源的计算,因为在顶点着色器中用的球谐光照或者是计算4个顶点光照的结果,作为间
接满反射的结果。
3,
动态光照的话,把在顶点着色器中赋值好的ambientOrLightmapUV.zw作为动态光照贴图采样的
uv,然后解码得到间接漫反射。
计算间接高光反射
这个公式很复杂,Unreal引擎把公式简化成
左括号是描述关于粗糙度的内容,右边的括号是一个固定值,大部分厂商包括Unreal用的都是LUT
粗糙度部分(括号左边)
这里进行了对LightProbe光照探针的采样,BoxProjectedDirection()函数是对方向进行校正。
因为LightProbe原点位置和实际物体世界空间的位置很有可能是不一致的,虽然方向相同,但会导致采样CubeMap贴图位置不同,产生错误的结果。
然后SamplerReflectProbe()函数再根据矫正过的方向进行重新采样。
unity_SpecCube0_BoxMin.w分量储存了LightProbe光照探针的权重,如果小于1的话
那么会根据权重插值第一个光照探针的采样颜色和第二个光照探针采样颜色。
如:
float3 specular = lerp(samplerLightProbeColor1,samplerLightProbeColor2,unity_SpecCube0_BoxMin.w);
光照探头中存储的是一组图像mipmap,内容逐渐模糊,如下
物体粗糙度roughness比较高的时候,反射的内容也是比较模糊的。
代码的意思逐行翻译
1
因为Unity的粗糙度和采样的mipmap等级关系不是线性的,Unity内使用的转换公式为mip = r(1.7 - 0.7r)。
因为是unity拟合的,然而真正的计算方式是如下,反正看不懂,直接使用即可。
2
计算反射方向用于采样cubemap
3
用从0到1之间的mip_roughness函数换算出用于实际采样的mip层级
UNITY_SPECCUBE_LOD_STEPS是一个定义在UnityStandardConfig.cginc文件中的常量,没改的话就是6。
4
UNITY_SAMPLE_TEXCUBE_LOD是采样mipMap的函数
1:第一个参数是mipMap贴图,存储在unity_SpecCube0这个变量里,存储的是场景和天空盒的反射探针数据
(还有一个变量叫unity_SpecCube1,存储的离物体最近的反射探针的数据)
2:第二个是反射方向,这个和普通的texCube一样
3:第三个就是之前计算过的粗糙度等级。
5
最后一行使用DecodeHDR将颜色从HDR编码下解码。可以看到采样出的rgbm是一个4通道的值,最后一个m存的是一个参数,解码时将前三个通道表示的颜色乘上xM^y,x和y都是由环境贴图定义的系数,存储在unity_SpecCube0_HDR这个结构中。
最后iblSpecular得到了间接高光的颜色。
LUT部分(括号右边)
右边的部分是 根据nv和粗糙度对LUT进行采样
float2 envBRDF = tex2D(_LUT, float2(lerp(0, 0.99, nv), lerp(0, 0.99, roughness))).rg;
实际上就是
float2 envBRDF = tex2D(_LUT, float2(nv,roughness)).rg;
因为在nv或者roughness等于1的时候会出问题。
!!!
上面得到iblDiffuse和iblSpecular都是采样LightMap,CubeMap等得到的环境光,并没有和直接光漫反射高光进行融合,也没有考虑能量守恒,所以需要重新计算kd和ks。
之前使用的F值并不适用于间接光,之前的方程会失真。
所以使用下面的方程重新计算ks
得到了新的ks,还需要重新计算能量守恒的kd
完成了间接光的部分,只要把直接光的结果和间接光的结果相加就得到了最终的颜色。
总结:
直接光部分
漫反射使用兰伯特或者迪士尼的漫反射效果差别不是很明显;
直接光的高光部分使用Cook_Torrance的模型;
遵从能量守恒,计算ks,kd。直接光部分要计算衰减,阴影。
间接光部分
漫反射部分需要采样LightMap;
高光部分需要采样环境CubeMap,
注意漫反射和高光反射都只是采样得到的颜色,并没有考虑其他衰减角度啊等等因素
(可能就是Unity里面掠射角那部分)
目前有两种方式一种是Copy Unity自己的魔法
另一种方式是使用Unreal的方式使用LUT,但是得重新计算ks,kd,也要能量守恒,
Unity的方式不好理解,只有数学运算,但是效率高一些。
最后加上自发光,PBR的部分就结束了。
(间接高光部分使用的Unreal的实现,Unity的实现完全看不懂)