图形 2.4 传统经验光照模型详解

传统经验光照模型详解


光照模型

 

  什么是光照模型?光照模型又称为明暗模型,用于计算物体某点处的光强(颜色值),从算法理论基础而言,光照模型分为基于物理和基于经验的。

基于物理的光照模型偏重于使用物理的度量和统计方法,效果非常真实,但是计算非常复杂,而且实现起来也比较困难。例:PBR

基于经验的光照模型则本质是对光照的一种模拟,通过实践和经验来总结出简化的方法,简化了真实的光照计算,并且能达到很不错的效果。例:Lambert,Billing-Phong。

不同光照模型的效果:

 

  那么我们为什么需要光照模型呢?因为现实世界的光照其实非常复杂,而且会受到诸多因素的影响,有限的计算能力无法完全模拟。使用简化的光照模型对现实的情况进行近似,使得计算处理起来会更容易,并且令效果更加符合需求。这些光照模型就是基于我们对光的物理特性的理解。

光照模型的发展:

 


 

局部光照模型

 

  局部光照模型并不是真正准确的模型,只关心直接光照,即只关心在光源–>物体–>人眼的过程中,仅存在一次反射而没有被其他物体反射过的,直接进入摄像头和人眼的光线。

局部光照模型的组成满足叠加原理,可以将基本光线分成:漫反射、高光反射、环境光、自发光。

 

漫反射:

 

  在光照模型的定义中,当光线从光源照射到模型表面时,光线均匀被反射到各个方向,这种现象就是漫反射。在漫反射的过程中,光线发生了发生了吸收和散射,而因此改变颜色和方向。

  漫反射光照符合Lambert定律,反射光强与法线和光源方向之间的夹角的余弦值成正比。

Lambert定律:一个面元的辐亮度或光亮度在其表面上半球的所有方向相等时,面元在与表面法线夹θ角方向的辐射强度或光强度等于其法线方向的辐射强度或光强度与cosθ的乘积。

即光源方向与表面法线方向越相似(向量点乘知识部分),那么反射光线的强度越大。

漫反射效果与观察者位置无关,与光源位置有关。

 

高光反射

 

  当光线到达物体表面并发生了反射,观察视线在反射光线的附近时,便能够观察到了高光反射。高光反射描述了光线与物体表面发生的反射(光强不变,方向改变),其反射率是根据菲涅尔效应决定的,通常使用对应的反射贴图描述物体表面的反射率,并且使用光泽度(粗糙度,反光度)描述高光范围的大小。

 

不同光泽度下的高光效果:

 

环境光

 

  实际的环境光计算需要考虑的因素非常多,而且计算复杂,所以在局部光照模型中不考虑间接光照的计算。间接光照是指,光线通常会在多个物体之间反射,最后进入摄像机,也就是说,在光线进入摄像机之前,经过了不止一次的物体反射。在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都使用这个环境光。

Cambient = Albedo * Ambientlight

  我们通常使用漫反射的反照率来指示环境光照的反射光量。该模型假定场景中发生多次散射和反射,并在所有方向上均等的射向目标物体。

 

自发光

 

  光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这个部分的贡献度。它的计算也很简单,就是直接使用了该材质的自发光颜色:

Cemissive = Albedo * Emissivelight

  通常在实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不会被当成一个光源。Unity5引入的全局光照系统则可以模拟这类自发光物体对周围物体的影响。

 

局部光照模型总体效果

 

 


 经典光照模型

 

经典光照模型包括Lambert模型,Phong模型,Blinn-Phong模型。

 

Lambert漫反射模型

  漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。漫反射中,视角的位置不重要,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。

  也就是兰伯特定律:反射光线的强度与表面发现和光源方向之间夹角的余弦值成正比。只有当入射光线与平面垂直的时候才能完整的接受所有的光能量,入射角度越倾斜,损失的能量越大。

光照公式:

color = Clight * albedo * dot(normal,L);

 

Phong模型

 

  Phong光照模型就是第一个有影响力的光照模型,考虑直接光照的反射作用,使用环境光代替间接光照。

Cfinal = Alight * mdiffuse + Clight * (mdiffuse * dot(l,n) + mspecular * dot(v,r)gloss)

Alight 环境光量
Clight 入射光量
mdiffuse 漫反射率
mspecular 镜面反射率
l 指向光源入射光向量
n 物体表面法线
v 观察方向向量
r 反射方向向量
gloss 光泽度

 

 

 

 

 

 

 

 

 

 

 

Blinn-Phong模型

 

  Blinn-Phong模型不再依赖于反射向量,而是采用了所谓的半程向量(Halfway Vector),即光线与视线夹角一半方向上的一个单位向量。当半程向量与法线向量越接近,镜面光分量越大。

Cfinal = Alight * mdiffuse + Clight * (mdiffuse * dot(l,n) + mspecular * dot(h,n)gloss)

 

  半角向量比反射向量的计算更加简洁,当光源与视线都在物体表面之上时,半角向量与发现向量的角度永远不会大于 90°
  设想一种情况,当材质的反光度非常低,因此物体被光线照射的大部分区域都会发生高光反射。这些区域中一部分高光部分的反射向量与视线的家教超过了 90°
  如果使用 Phong 模型就会导致高光区域一部分发生缺失。这是由于 Phong 模型只考虑视线与光照分布在法线两侧的情况。当视线与光照在法线同侧时且高光反射对高度有较大影响时,就会发生断层。
 

着色方法

 

  上面我们介绍完了经典局部光照模型,其中主要利用了观察方向,入射光线与法线向量的位置关系,但并没有具体说究竟是三角形面的法线向量还是三角形顶点的法线向量,即我们要确认着色方式,如到底是逐顶点着色还是逐像素着色或是逐面着色。这三种着色方式对应着不同的表现效果。

 

Flat Shading

 

  面着色,即逐面进行着色,顾名思义以面作为一个着色单位。模型数据,大多以很多歌三角面进行储存,因此也就记录了每个面的法线向量,利用每个面的法线向量进行一次Blinn-Phong反射光照模型的计算,将该颜色赋予整个面,效果如下:

  逐面进行着色虽然计算很快,只需对每一个面进行一次着色计算,但是效果是很差的,可以很明显的看到一块块面的形状(实际上,这种LowPoly的风格比较适合做风格化的,更加卡通风格的场景,只是不适合用来给追求写实的画面使用),因此一种改进方法就是对三角形面的每个顶点进行着色,再对三角形面内的颜色插值,也就是高洛德着色方式(Gouraud Shading)。

 

Gouraud Shading

 

  Gouraud Shading(高洛德着色,逐顶点着色)会对每个三角形的顶点进行一次着色,我们需要将所有共享这个点的面法线加起来求均值,最后再标准化就得了该顶点的法线向量了。

  有了每个三角形的顶点向量之后,自然就可以计算出每个顶点的颜色了,那么对于三角形内部的每一个点应该怎么办?我们可以用重心插值坐标来计算,这样就能得到每个点的颜色,效果如下:

 

Phong着色模型

 

  我们既然能得到每个顶点的法线向量,那么对于三角形内部的每点的法线向量自然也可以像差值颜色一样得到,然后逐像素进行着色,三种着色方式的对比如下。

  

其实可以看到Phong Shading对于高光的显示相比于,Gouraud Shading是更真实的。有一点的要注意的是,这里所有的茶壶所使用的都是低精度模型,即模型的精度越高,逐顶点的效果更好。这里给出一张根据面数的增加,各个着色方法的对比。

 


 

作业

 

基于能量守恒的理念,自己写一条完整的光照模型,需要包含环境光照。

上头 。磕磕绊绊写了一套IBL的PBR,理性要蒸发了,可能会有不对的地方。

在整个庄懂、games101一路上PBR那听得可是不少,所以自然也想自己动手写写PBR,因此查了一堆资料后终于磨了一个出来。

 

效果:

 

接下来说过程和shader代码。

 

现在大多数PBR公式都是用的Cook-Torrance反射率方程,即Cook-Torrance BRDF

PBR公式长这样:

看起来比较离谱,初见只觉得完全意义不明。

实际上,我们整理一下便能明白这个公式的意思:

 

关于BRDF公式的研究到处都有,我就不笨比的复读了,我做的工作基本上也是站在巨人的肩膀搬砖罢了。这里给个完善解释的链接:LearnOpenGl

 

回到话题上来,现在我们一步步拆分这个方程来转换成我们的shader。可以看到这个公式整体由两个部分组成,一个是左边部分的漫反射部分,另一个则是右边的镜面反射部分。

 

漫反射部分:

光方向和法线方向的点乘,一看,这不就是兰伯特吗。这个部分我曾尝试过了一下迪士尼的漫反射公式,

但迪士尼的公式我对比了一下和兰伯特的效果,发现差别没多大,又换了回来,这里说一下当时理解的一些地方。可以跳过。

迪士尼公式中多了一个重要的参数叫Fd90,随着法线与光线视角与法线与视线视角的增大,反射率也随着增大。Fd90则会根据视线与光线角度变小而增大。什么意思呢,这代表着迪士尼漫反射中考虑了粗糙度的影响,漫反射在视线与光线角度比较小的时候,粗糙度更大的表面会反射到更多的光在眼睛的现象,这个现象在光线、视线与法线的角度越接近90,效果就越明显。对比来说,兰伯特光照模型则没有这种考量。

 

漫反射代码:

//Lambert漫反射
float3 Diffuse(float NdotH, float3 Albedo, float Tint)
{
    return Albedo * Tint * NdotH ;
}

效果:

 

但漫反射的计算还没有结束,我们还需要乘上一个漫反射的系数。这个系数其实在UnityStandardUtils.cginc中有一个专门定义的方法叫做OneMinusReflectivityFromMetallic。

源码:

inline half OneMinusReflectivityFromMetallic(half metallic)
{
    // We'll need oneMinusReflectivity, so
    //   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
    // store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
    //   1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
    //                  = alpha - metallic * alpha
    half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a;
    return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}

 

这个系数代表着什么,为什么要乘上这个系数?虽然,金属度决定了镜面反射的系数,所以漫反射是(1-金属度),代表着金属吸收更多的折射光线导致漫反射消失,但是实际上非金属也会存在一点镜面反射,单纯的减法没有考虑到这个影响,1-菲涅尔项则是保证能量守恒。最后我们把这个系数乘算给漫反射结果。

漫反射代码:

//Lambert漫反射
float3 Diffuse(float NdotH, float3 Albedo, float Tint)
{
    return Albedo * Tint * NdotH ;
}

//菲涅尔近似方程 Fresnel-Schlick
float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
    return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}


//漫反射系数
float3 Kd(float3 Flast, float Metallic)
{
    return (1 - Flast) * (1 - Metallic);
}

fixed4 frag (v2f i) : SV_Target
{
    float3 Flast_VH = fresnelSchlickRoughness(max(VdotH, 0.0), F0, _Roughness);
    float kd_VH = Kd(Flast_VH, metallic);

    float3 diffuse = Diffuse(NdotH, albedo.rgb, _Color);
    diffuse = diffuse * kd_VH;

    return float4(diffuse,1); 
}

 

这里插入一下,因为我们的这个漫反射公式其实还有一项PI没有进行除算,这是因为Unity的UnityStandardBRDF.cginc中有这么一段注释:

HACK: theoretically we should divide diffuseTerm by Pi and not multiply specularTerm!
BUT 1) that will make shader look significantly darker than Legacy ones
and 2) on engine side "Non-important" lights have to be divided by Pi too in cases when they are injected into ambient SH

翻译一下:

理论上,我们应该将漫反射除以π,而不是乘以镜面反射项!

但1) 这将使着色器看起来比传统着色器暗的多

2)在非重要光源不去除π防止IBL部分做特殊处理

 

完整的漫反射效果:

 


 

 

我们发现在刚刚的漫反射代码中加入了奇怪的东西。

正好我们也要进入高光反射部分了:

 

而刚刚插入在漫反射里的公式则是菲涅尔方程的近似版本:

 

菲涅尔效应我觉得已经耳熟能详了,当光从一个介质向另一个介质传播时,在两者的交界处可能会同时发生光的反射和折射,视角与表面的夹角越大,反射效果越弱,夹角越小,反射效果越明显。

而这个菲涅尔方程描述的就是被反射的光线对比光线被折射的部分所占的比例,这个比例随着观察角度而变化。利用这个反射比例和能量守恒原则,我们可以直接得出光线被这些的部分和光的剩余能量。

而这个方程为什么叫做近似方程呢?因为真正的菲涅尔方程非常复杂,不够实用。因此我们使用一种resnel-Schlick近似法求得的求菲涅尔项常用版本。

可能你在有些资料上查到的菲涅尔近似公式是长这个样子的:

 

不要慌,这也是对的,这是虚幻引擎用的拟合版本,因为exp2函数运算效率比pow高,所以计算会更加快一些。

而这个F0是什么呢?他理论上是平面的基础反射率,为了让同一个材质表示金属和非金属的不同属性,我们要将材料的金属参数整合的F0计算中,实际F0的计算代码为:

float3 F0 = float3(0.04, 0.04, 0.04);
F0=lerp(F0, albedo.rgb, metallic);

这里有个比较神必的值,就是这个float3(0.04, 0.04, 0.04),这个是Unity里面有定义的一个常数unity_ColorSpaceDielectricSpec.rgb。F0的计算在这个常数和表面颜色间根据金属度进行一个插值运算。

整个F项的计算则是

float3 F0 = float3(0.04, 0.04, 0.04);
F0=lerp(F0, albedo.rgb, metallic);

//F 菲涅尔近似方程 Fresnel-Schlick
float F(float VdotH, float3 F0)
{
    half t = Pow5 (1 - VdotH);
    return lerp(t, 1, F0);
}

 


 

接下来是D项。

D项长这样:

 

D项代表着法线的分布函数,这代表着什么意思呢。我们知道在传统的光照模型中,一个表面的法线方向就是一条法线。但是PBR是基于微表面理论的,因此实际上整个表面是由许许多多微表面法线朝向的组合成了我们宏观上的那条法线。这个D项就是说明在这个微表面上,有多少微表面的法线是正确(能反射到视线方向)的朝向,这些能被观察到的法线会对最终结果造成影响,因此我们可以得知这个函数本质是做了一个统计分布,而这个式子则叫做Trowbridge-Reitz GGX。

代码:

//D 法线分布函数GGX
float D(float NdotH, float Roughness)
{
    float alpha_2 = pow(lerp(0.002, 1, Roughness), 2);
    float NdotH_2 = pow(saturate(NdotH), 2);
    float deno = UNITY_PI * pow((NdotH_2 * (alpha_2 - 1) + 1), 2);
    
    return  alpha_2 / deno;
}

 


 

最后我们来看G项,G项为:

 

关于这个k项,它在直接光照的时候为(α + 1)2/8,间接光照时为α2/2。

G项叫做几何函数,描述微表面自阴影的一个属性,当材质比较粗糙的时候,微表面可能会挡住其他微表面的光线,从而减少表面所反射的光线。这是说,虽然D项能算出所有有用的微表面法线,但他们不一定都能被看得见,可以认为这一项是在D项的基础上的一次再过率,把真正有实际效果的法线提取出来参与到最后的计算中。

示例图:

 

而之所以要乘算两次,是因为这个遮蔽也确实执行了两次,一次在入射的时候进行了一次遮蔽,另一次则是在出射的时候也进行了一次遮蔽,两者乘算的结果才是完整的结果。关于这个k值在直接光照和间接光照的区别,这是因为保证表面在绝对光滑时也会吸收一部分光线,因为现实是没有不吸收光线的物体的。两个k大体都非常接近1/2,不过直接光照最小值锁到了1/8.

 

代码:

//G 几何函数 GeometrySchlickGGX
float G(float NdotV, float NdotL, float Roughness)
{
    float k = pow(Roughness+1, 2) / 8;
    //float k = pow(Roughness, 2) / 2;
    float g_sub1 = NdotV / (NdotV * (1 - k) + k);
    float g_sub2 = NdotL / (NdotL * (1 - k) + k);

    return g_sub1 * g_sub2;
}

 

这样一来我们就得到了DFG所有的三个参数,可以得出最后的高光部分了

//高光反射
float3 Specular(float D, float F, float G, float NdotV, float NdotL)
{
    return (D * F * G) / (4 * NdotL * NdotV + 0.000001);//防除0
}

float d = D(NdotH, _Roughness);
float f = F(VdotH, F0);
float g = G(NdotV, NdotL, _Roughness);

float3 specular = Specular(d, f, g, NdotV, NdotL);
specular = specular * _Color * NdotL * UNITY_PI;

return float4(specular ,1); 

 

效果:

 

我们把漫反射的部分也结合起来:

float3 DirectLight = diffuse + specular;

return float4(DirectLight,1);

 

 

但是我们的工作还没有做完。我看一些教程都没写IBL,有点难顶,因为BRDF的直接光部分和传统的经验模型的渲染效果区别也不是太大,想要看更好看的效果还需要加入真正区别不同的间接光部分。我也不是那些顶尖的大牛,做不了太多的研究,本着将理论做工具的精神,借着巨人的肩膀把剩下部分做完。

 


 

于是乎,我们的公式又多加了一部分,变成了这样

 

说起间接光,就不能不说道IBL和球谐,但是这俩大概是无法用三言两句概括的东西。你可以看做是对天空盒进行了一个采样,我们使用一个环境贴图级数来对环境贴图采样,粗糙度越大反射则越模糊,对应的环境贴图级数越高。

而Unity非常方便的是,它将环境贴图cubemap积分成模糊的全局光照贴图,再讲全局光照的贴图投影到球谐光照的基函数上储存,这个可调用的API就是ShadeSH9,它传进归一化法线,返回重积分后的环境光照信息。

代码:

//间接光漫反射
float3 IBL_Diffse(float Albedo, float3 Ndir, float Kd)
{
    //球谐
    half3 ambient_contrib = ShadeSH9(float4(Ndir, 1));
    float3 ambient = 0.03 * Albedo;
    float3 iblDiffuse = max(half3(0, 0, 0), ambient.rgb + ambient_contrib);

    return  iblDiffuse * Albedo * Kd;
}

float3 Flast_NV = fresnelSchlickRoughness(max(NdotV, 0.0), F0, _Roughness);
float kd_NV = Kd(Flast_VH, metallic);
float3 ibl_Diffse = IBL_Diffse(albedo, nDir, kd_NV);

 

这个部分代码有几个地方需要注意,一个是环境光的影响,环境光的影响其实不是很大,随便设个很暗的值如0.03即可。需要注意的是这个Kd用的是NdotV的kd值,而非VdotH。

这是因为使用NdotV的kd值是宏观的,菲涅尔方程的确是由表面法线和视角方向求出来的,但是为什么上面直接漫反射的kd值却是VdotH呢,因为这是构建在微表面模型上的kd值,不是由宏观平面而是由D项筛出来的法线为h的微表面,基于微表面模型的理论是PBR重要的一环,因此这里需要自己去理解。

效果:

 


 

 

最后的最后,也就是间接光照的镜面反射部分。这部分被epic公司简化成了下面的形式:

 

左边是一个说过的大概类似于采样天空盒的东西,PBR里面的这种Cubemap基本是HDR的,需要DecodeHDR将HDR信息转成正常信息。右边部分是个定值,常见的做法是将值放在一张LUT图中,根据nDir和vDir的点乘结果和粗糙度采样即可。

LUT图大概长这样:

 

代码部分:

float mip_roughness = _Roughness * (1.7 - 0.7 * _Roughness);
float3 reflectVec = reflect(-vDir, nDir);
half mip = mip_roughness * 6;
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip);
float3 ibl_Specular = DecodeHDR(rgbm, unity_SpecCube0_HDR);

 

来说说这部分吧,第一行是说粗糙度和级数的关系并非线性关系,在UnityImageBasedLighting.cginc中有定义这个转换公式,这个转换公式就是

mip_roughness = _Roughness * (1.7 - 0.7 * _Roughness);

接下来一行求反射向量好说。

然后下面它马上又乘算了一个6,其实是一个叫做UNITY_SPECCUBE_LOD_STEPS的常量,这玩意定义在UnityStandardConfig.cginc中,表明整个粗糙度范围内多级渐远纹理的总级数。

接着我们就能使用UNITY_SAMPLE_TEXCUBE_LOD这个函数进行Cubemap的采样了,粗糙度越高结果越模糊,最后用DecodeHDR解码HDR颜色,得到了间接高光。

效果:

 

看着是不是不太对,我们还有最后一些参数没有计算完:

float2 LUT_lerp = float2( lerp(0, 0.99, NdotV), lerp(0, 0.99, _Roughness) );
float2 envBDRF = tex2D(_LUT,  LUT_lerp).rg; // LUT采样

float3 ibl_Specular = DecodeHDR(rgbm, unity_SpecCube0_HDR) * (Flast_NV * envBDRF.r + envBDRF.g);

 

LUT注意要去掉sRGB勾选,我们整个shader的计算都是在线性空间中进行的,但并不意味这不用做gamma校正。之所以要设置sRGB格式是因为大部分显示颜色的贴图在导入前就进行过一次gamma校正。同时关闭mipmap生成,并取消压缩,设置clamp。

注意截断1这个值,这两个值如果都为1的情况下,会使LUT颜色突变,被渲染的物体上产生亮斑。乘算完毕后我们得到的正确的间接高光是这样的:

 

(2/23更新修改 改进了一些部分)

 

完整的间接光加起来的同时,我们再加上环境光的遮罩图,就是这样:

 

float ao = tex2D(_AO,i.uv).r; 
float3 inDirectLight = (ibl_Diffse + ibl_Specular) * ao;

 

到这里我们4个部分的值也就全部计算完了。最后我们把它们全部都加起来,就是我们整个PBRshader的效果。

 

然后加入一个自发光,就是我们最后的效果。

 

 

面板:

 

 

完整shader:

Shader "MyShader/PBR_Custom"
{
    Properties
    {
        _MainTex("Albedo", 2D) = "white" {}

        _Emissive("Emissive", 2D) = "white" {}
        [HDR]_EmissiveColor("EmissiveColor",Color)=(1, 1, 1, 1)

        //[Gamma]_Roughness("Roughness", Range(0.0, 1.0)) = 0.0
        _Roughness("Roughness", 2D) = "white" {}
        _RoughnessFactor("RoughnessFactor", Range(0.2, 2)) = 1.0
        _MetallicGlossMap("Metallic", 2D) = "white" {}

        _AO("AO", 2D) = "white" {}

        _BumpScale("Scale", Float) = 1.0
        _BumpMap("Normal Map", 2D) = "bump" {}

        [NoScaleOffset]_LUT("LUT", 2D) = "white" {}

        _Color("Color",Color)=(1, 1, 1, 1)
    }
    SubShader
    {
        Tags{"RenderType" = "Opaque"}
        LOD 300

        CGINCLUDE
        #include "UnityCG.cginc"
        #include "Lighting.cginc"

        //D 法线分布函数GGX
        float D(float NdotH, float Roughness)
        {
            float alpha_2 = pow(lerp(0.002, 1, Roughness), 2);
            float NdotH_2 = pow(saturate(NdotH), 2);
            float deno = UNITY_PI * pow((NdotH_2 * (alpha_2 - 1) + 1), 2);
            
            return  alpha_2 / deno;
        }

        //F 菲涅尔近似方程 Fresnel-Schlick
        float F(float VdotH, float3 F0)
        {
            half t = Pow5 (1 - VdotH);
            return lerp(t, 1, F0);
        }

        //G 几何函数 GeometrySchlickGGX
        float G(float NdotV, float NdotL, float Roughness)
        {
            //float k = pow(Roughness+1, 2) / 8;
            float k = pow(Roughness, 2) / 2;
            float g_sub1 = NdotV / (NdotV * (1 - k) + k);
            float g_sub2 = NdotL / (NdotL * (1 - k) + k);

            return g_sub1 * g_sub2;
        }

        //高光反射
        float3 Specular(float D, float F, float G, float NdotV, float NdotL)
        {
            return (D * F * G) / (4 * NdotL * NdotV + 0.000001);//防除0
        }

        //Lambert漫反射
        float3 Diffuse(float NdotH, float3 Albedo, float Tint)
        {
            return Albedo * Tint * NdotH ;
        }

        //菲涅尔近似方程 Fresnel-Schlick
        float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
        {
            return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
        }

        //漫反射系数
        float3 Kd(float3 Flast, float Metallic)
        {
            return (1 - Flast) * (1 - Metallic);
        }

        //间接光漫反射
        float3 IBL_Diffse(float Albedo, float3 Ndir, float Kd)
        {
            //球谐
            half3 ambient_contrib = ShadeSH9(float4(Ndir, 1));
            float3 ambient = 0.03 * Albedo;
            float3 iblDiffuse = max(half3(0, 0, 0), ambient.rgb + ambient_contrib);

            return  iblDiffuse * Albedo * Kd;
        }
        
        ENDCG

        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
            CGPROGRAM

            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityStandardBRDF.cginc" 

            struct appdata
            {
                float4 vertex       : POSITION;
                float3 normal       : NORMAL;
                float2 uv           : TEXCOORD0;
                float4 tangent      :TANGENT;
            };

            struct v2f
            {
                float4 vertex       : SV_POSITION;
                float2 uv           : TEXCOORD0;               
                float3 lightDir     : TEXCOORD1;
                float3 viewDir      : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            sampler2D _Emissive;
            sampler2D _Emissive_ST;
            float4 _EmissiveColor;

            sampler2D _Roughness;
            sampler2D _Roughness_ST;
            float _RoughnessFactor;
            sampler2D _MetallicGlossMap;
            sampler2D _MetallicGlossMap_ST;
            sampler2D _AO;
            sampler2D _AO_ST;
            
            float _BumpScale;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;

            sampler2D _LUT;
            fixed4 _Color;
            
            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                TANGENT_SPACE_ROTATION;//省写TBN真好用(
                o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz);
                o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //准备数据
                float4 albedo = tex2D(_MainTex,i.uv) * _Color;                                  //颜色
                float4 packNormal = tex2D(_BumpMap,i.uv);                                       //法线
                float metallic = tex2D(_MetallicGlossMap,i.uv).r;                               //金属
                float roughness = tex2D(_Roughness,i.uv).r * _RoughnessFactor;                  //粗糙
                float ao = tex2D(_AO,i.uv).r;                                                   //环境光遮蔽
                float4 emissive = tex2D(_Emissive,i.uv).r;                                        //自发光
                
                float3 nDir = UnpackNormal(packNormal);
                nDir.xy *= _BumpScale;
                nDir.z = sqrt(1 - saturate(dot(nDir.xy,nDir.xy)));                              //法线转切线空间
                float3 lDir = normalize(i.lightDir);
                float3 vDir=normalize(i.viewDir);
                float3 hDir=normalize(lDir+vDir);    

                //准备中间数据1
                float NdotH = saturate(dot(nDir,hDir));
                float VdotH = saturate(dot(vDir,hDir));
                float NdotV = saturate(dot(nDir,vDir));
                float NdotL = saturate(dot(nDir,lDir));
                //准备中间数据2
                float3 F0 = float3(0.04, 0.04, 0.04);
                F0=lerp(F0, albedo.rgb, metallic);
                float3 Flast_VH = fresnelSchlickRoughness(max(VdotH, 0.0), F0, roughness);     //微观
                float3 Flast_NV = fresnelSchlickRoughness(max(NdotV, 0.0), F0, roughness);     //宏观
                float kd_VH = Kd(Flast_VH, metallic);
                float kd_NV = Kd(Flast_VH, metallic);
                float d = D(NdotH, roughness);
                float f = F(VdotH, F0);
                float g = G(NdotV, NdotL, roughness);
                
                float2 LUT_lerp = float2( lerp(0, 0.99, NdotV), lerp(0, 0.99, roughness) );
                float2 envBDRF = tex2D(_LUT,  LUT_lerp).rg; // LUT采样
                //计算
                float3 diffuse = Diffuse(NdotH, albedo.rgb, _Color);
                float3 specular = Specular(d, f, g, NdotV, NdotL);
                diffuse = diffuse * kd_VH;
                specular = specular * _Color * NdotL * UNITY_PI;
                float3 DirectLight = diffuse + specular;

                float3 ibl_Diffse = IBL_Diffse(albedo, nDir, kd_NV);
                
                //这部分为ibl_specular-----------------------
                float mip_roughness = roughness * (1.7 - 0.7 * roughness);
                float3 reflectVec = reflect(-vDir, nDir);
                half mip = mip_roughness * 6;
                half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip);
                float3 ibl_Specular = DecodeHDR(rgbm, unity_SpecCube0_HDR) * (Flast_NV * envBDRF.r + envBDRF.g);
                //--------------------------------------------

                float3 inDirectLight = (ibl_Diffse + ibl_Specular) * ao;
                
                return float4(DirectLight + inDirectLight + emissive * _EmissiveColor, 1);
            }
            ENDCG
        }
    }
}

 

 


参考

 

Unity Shader 简单实现一个PBR模型(不含IBL)

由浅入深学习PBR的原理和实现

从零开始在Unity中写一个PBR着色器

如何在Unity中造一个PBR Shader轮子

DisneyDiffuse解析

基于物理的渲染(PBR)白皮书 | 迪士尼原则的BRDF与BSDF相关总结 

LearnOpenGl

【技术美术百人计划】图形 2.4 传统经验光照模型详解

 

跳转回百人合集

posted @ 2022-01-10 23:21  anesu  阅读(743)  评论(0编辑  收藏  举报