kyuskoj 发表于 2021-11-20 15:29

【Unity Shader】基于物理的渲染PBR(一)

一、PBR简介

基于物理的渲染,它的材质效果根据真实世界的数据来设置,能够更精确的描绘光和表面之间的作用,在任何光照环境都能表现出正确的结果,提供统一的工作流程,常用来表现写实的材质。
PBR光照的计算分为直接光照和间接两个部分。直接光照分为直接光漫反射和直接光镜面反射。间接光照(全局光照GI),即IBL,指物体受到间接的光照,是基于纹理的光照,大体可以细分为间接光漫反射和间接光镜面反射。
PBR光照模型需要满足以下条件:

[*]基于微平面模型。
[*]能量守恒。
[*]使用基于物理的BRDF。
二、直接光漫反射

lambert漫反射
漫反射系数,公式上更遵循物理,但效果上没有内置宏好
half3 kd = (1 - F)*(1 - metallic);漫反射系数,内置宏
half3 kd = OneMinusReflectivityFromMetallic(metallic);直接光的漫反射项
half3 DiffcuseResult = kd * albedo.rgb;Disney漫反射:相比兰伯特更接近真实测量值。
// Note: Disney diffuse must be multiply by diffuseAlbedo / PI. This is done outside of this function.
half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
{
    half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
    // Two schlick fresnel term
    half lightScatter   = (1 + (fd90 - 1) * Pow5(1 - NdotL));
    half viewScatter    = (1 + (fd90 - 1) * Pow5(1 - NdotV));
    return lightScatter * viewScatter;
}三、直接光镜面反射

BRDF:双向反射分布函数。BRDF有好几种模拟表面光照的算法,然而,基本上所有的实时渲染管线使用的都是Cook-Torrance BRDF。当已知光源位置和方向、视角方向时,我们就知道一个表面是如何和光照进行交互的。
BRDF的高光(镜面反射)部分公式:


Cook-Torrance镜面反射BRDF由3个函数(D,F,G)和一个标准化因子构成。D,F,G符号各自近似模拟了特定部分的表面反射属性。
// Cook-Torrance BRDF
half3 CookTorranceBRDF(half NdotH,half NdotL,half NdotV,half VdotH,half roughness,half3 F0)
{
    half D = GGXTerm (NdotH,roughness);      //法线分布函数
    half G = GeometrySmith(NdotV,NdotL,roughness);          //微平面间相互遮蔽的比率
    half3 F = FresnelSchlick(F0,VdotH);       //近似的菲涅尔函数
    half3 res =(D * G * F * 0.25) / (NdotV * NdotL);
    return res;
}D(NDF),法线分布函数,估算在受到表面粗糙度的影响下,取向方向与中间向量一致的微平面的数量。


这里的h是用来测量微平面的半角向量,α是表面的粗糙度,n是表面法线。 如果将h放到表面法线和光线方向之间,并使用不同的粗糙度作为参数,可以得到下面的效果:


NDF实现代码:
//法线分布函数
inline float GGXTerm (float NdotH, float roughness)
{
    float a2 = roughness * roughness;
    float d = (NdotH * a2 - NdotH) * NdotH + 1.0f;
    return UNITY_INV_PI * a2 / (d * d + 1e-7f);                              
}G(Geometry function),微平面间相互遮蔽的比率 ,当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。


几何函数使用由GGX和Schlick-Beckmann组合而成的模拟函数Schlick-GGX:


这里的kk是使用粗糙度αα计算而来的,用于直接光照和IBL光照的几何函数的参数:


使用Smith函数与Schlick-GGX可以得到如下所示不同粗糙度R的视觉效果:


实现代码:
//G (Geometry function)
float GeometrySchlickGGX(float NdotV, float k)
{
    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;
    return nom / denom;
}
            
float GeometrySmith(float NdotV,float NdotL, float Roughness)
{
    float squareRoughness = Roughness * Roughness;
    float k = pow(squareRoughness + 1, 2) / 8;
    float ggx1 = GeometrySchlickGGX(NdotV, k); // 视线方向的几何遮挡
    float ggx2 = GeometrySchlickGGX(NdotL, k); // 光线方向的几何阴影
    return ggx1 * ggx2;
}F(Fresnel equation),菲涅尔方程,表示在不同观察方向上,表面上被反射的光除以被折射的光的比例。菲涅尔方程可以使用Fresnel-Schlick来近似。


观察方向越是接近掠射角(又叫切线角,与正视角相差90度),菲涅尔现象导致的反射就越强:


F0就是反射率, 当我们90度直视一个表面的时候, 看到的光子回弹的比例。在金属度等于1的情况,很容易分析, 漫反射等于0, 而F0就会等于albedo。
half3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic); 菲涅尔方程:
float3 fresnelSchlick(float cosTheta,float3 F0)
{
      return F0 + (1 - F0) * pow(1.0 - cosTheta,5.0);
}近似的菲涅尔函数:
//近似的菲涅尔函数
float3 FresnelSchlick(float3 F0 , float VdotH)
{
    float3 F = F0 + (1 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH);
    return F;
}四、间接光漫反射

对于间接光漫反射的计算,我们可以把它近似的看成对CubeMap(环境贴图) 的采样,我们通过Unity内置的一个宏,把相关的采样数据计算在了宏里。这个就是球谐函数,我们使用不同的球谐基底,便可以得到天空盒或光照探针的数据,对光照进行还原。最后再乘以漫反射系数,即为间接光的漫反射。
//间接光漫反射
half3 ambient_contrib = ShadeSH9(float4(bump, 1));   //球谐光照
half3 ambient = 0.03 * albedo.rgb;    //环境光,取很小的值即可,可省略      
half3 iblDiffuse = max(half3(0, 0, 0), ambient.rgb + ambient_contrib);
half3 Flast = FresnelSchlickRoughness(ndv,F0, roughness); //引入了粗糙度的菲涅耳项计算高光反射比例 反推出漫反射比例
half3 kdLast = (1 - Flast) * (1 - metallic);          //间接光漫反射系数
half3 iblDiffuseResult = iblDiffuse * kdLast * albedo.rgb;五、间接光镜面反射

对于镜面反射部分,因为数值不是恒定的,它依赖于入射光的方向和视角方向,所以同样很难做到实时计算。这里借鉴了UE4引擎里的近似算法split sum,并把它简化成两项Li和BRDF。对于Li即光照信息部分(左边的部分),我们同样使用采样CubeMap来计算,但与漫反射不同的是,因为镜面反射受到粗糙度影响,所以我们根据粗糙度而使用对应的多个模糊程度不同的mipmap进行采样。
//间接光镜面反射
half mip = CubeMapMip(perceptualRoughness);         //计算Mip等级
half3 reflectDir = reflect(-viewDir, bump);
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, mip);
half3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);右边的部分BRDF是一个定值,业界的做法是将值放到一张查找图中,用的时候根据nv和粗糙度采样。这种LUT如下:


将导入的查找图拖到材质的LUT上,接着进行编码。
float2 envBDRF = tex2D(_LUT, float2(lerp(0, 0.99, ndv), lerp(0, 0.99, roughness))).rg; // LUT采样
float3 iblSpecularResult =iblSpecular* (Flast * envBDRF.r + envBDRF.g);//最后通过使用采样得到的r值进行缩放和g值进行偏移得到结果模仿Unity的间接光镜面反射实现。
float grazingTerm = saturate(1 - roughness + kd);
float surfaceReduction = 1 / (pow(roughness,2) + 1);
float3 iblSpecularResult= surfaceReduction * iblSpecular * FresnelLerp(float4(F0,1.0),grazingTerm,ndv);六、PBR参考模板

Shader "PBR/PBRSource"
{
    Properties
    {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("BumpMap", 2D) = "bump" {}
      _BumpScale ("Bump Scale",float)=1

      
      _Tint("Albedo Color",Color)=(1,1,1,1)
      _SMAE("R:Smoothness G:Metallic B:Occlusion",2D)= "white" {}
      _Metallic("Metallic",Range(0,1))=0
      _Smoothness("Smoothness",Range(0,1))=0

      //_LUT("LUT",2D) = "white"{} // Lut贴图
    }
    SubShader
    {
      Tags { "RenderType"="Opaque" }
      LOD 100

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

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "UnityStandardBRDF.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float4 TtoW0 : TEXCOORD1;
                float4 TtoW1 : TEXCOORD2;
                float4 TtoW2 : TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;   
            float4 _BumpMap_ST;   
            half _BumpScale;

            half4 _Tint;
            sampler2D _SMAE;
            float4 _SMAE_ST;
            half _Metallic;
            half _Smoothness;

            sampler2D _LUT;

            //G (Geometry function)
            float GeometrySchlickGGX(float NdotV, float k)
            {
                float nom   = NdotV;
                float denom = NdotV * (1.0 - k) + k;
                return nom / denom;
            }
            
            float GeometrySmith(float NdotV,float NdotL, float Roughness)
            {
                float squareRoughness = Roughness * Roughness;
                float k = pow(squareRoughness + 1, 2) / 8;
                float ggx1 = GeometrySchlickGGX(NdotV, k); // 视线方向的几何遮挡
                float ggx2 = GeometrySchlickGGX(NdotL, k); // 光线方向的几何阴影
                return ggx1 * ggx2;
            }

            //移动平台上使用的近似ENVBRDF
            //https://www.unrealengine.com/zh-CN/blog/physically-based-shading-on-mobile
            half3 EnvBRDFApprox( half3 SpecularColor, half Roughness, half NdotV )
            {
                half4 c0 = { -1, -0.0275, -0.572, 0.022 };
                half4 c1 = { 1, 0.0425, 1.04, -0.04 };
                half4 r = Roughness * c0 + c1;
                half a004 = min( r.x * r.x, exp2( -9.28 * NdotV ) ) * r.x + r.y;
                half2 AB = half2( -1.04, 1.04 ) * a004 + r.zw;
                return SpecularColor * AB.x + AB.y;
            }

            //立方体贴图的Mip等级计算
            half CubeMapMip(half perceptualRoughness)
            {
                //基于粗糙度计算CubeMap的Mip等级
                half mip_roughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);   //转换公式mip = r(1.7 - 0.7r),接近实际值的拟合曲线
                half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS;   //得出mip层级。默认UNITY_SPECCUBE_LOD_STEPS=6(定义在UnityStandardConfig.cginc)
                return mip;
            }

            //近似的菲涅尔函数
            float3 FresnelSchlick(float3 F0 , float VdotH)
            {
                float3 F = F0 + (1 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH);
                return F;
            }

            //间接光的菲涅尔系数
            float3 FresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
            {
                return F0 + (max(float3(1 ,1, 1) * (1 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
            }

            // Cook-Torrance BRDF
            half3 CookTorranceBRDF(half NdotH,half NdotL,half NdotV,half VdotH,half roughness,half3 F0)
            {
                half D = GGXTerm (NdotH,roughness);      //法线分布函数
                half G = GeometrySmith(NdotV,NdotL,roughness);          //微平面间相互遮蔽的比率
                half3 F = FresnelSchlick(F0,VdotH);       //近似的菲涅尔函数
                half3 res =(D * G * F * 0.25) / (NdotV * NdotL);
                return res;
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                float3 worldPos=mul(unity_ObjectToWorld,v.vertex);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));   
                float3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                float3 halfDir=normalize(viewDir+lightDir);

                float3 bump = UnpackScaleNormal(tex2D(_BumpMap, i.uv),_BumpScale);
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

                //数据准备
                float ndl_noclamp=dot(bump,lightDir);
                float ndl=max(1e-5,saturate(dot(bump,lightDir)));   //防止除0
                float ndv=max(1e-5,saturate(dot(bump,viewDir)));
                float ndh=max(1e-5,saturate(dot(bump,halfDir)));
                float vdh=max(1e-5,saturate(dot(viewDir, halfDir)));
                float ldh=max(1e-5,saturate(dot(lightDir, halfDir)));

                half4 albedo=tex2D(_MainTex, i.uv)*_Tint;
                half4 PBRMask=tex2D(_SMAE, i.uv);
                half metallic=_Metallic*PBRMask.g;
                half perceptualRoughness=1-_Smoothness*PBRMask.r;
                half roughness = PerceptualRoughnessToRoughness(perceptualRoughness);   //粗糙度
                roughness = max(roughness, 0.002);      //防止为0,保留一点点高光
                half occlusion=PBRMask.b;
               
                //直接光镜面反射
                half3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
                half3 specular = CookTorranceBRDF(ndh,ndl,ndv,vdh,roughness,F0);
                half3 SpecularResult =specular;

                //直接光漫反射   
                //half3 kd = (1 - F)*(1 - metallic);   //漫反射系数,公式上更遵循物理,但效果上没有内置宏好
                                half3 kd = OneMinusReflectivityFromMetallic(metallic);   ////漫反射系数,内置宏
                half3 DiffcuseResult = kd * albedo.rgb;

                half3 DirectLightResult =(DiffcuseResult + SpecularResult* UNITY_PI) * _LightColor0 * ndl;

                //间接光漫反射
                half3 ambient_contrib = ShadeSH9(float4(bump, 1));   //球谐光照
                half3 ambient = 0.03 * albedo.rgb;    //环境光,取很小的值即可,可省略      
                half3 iblDiffuse = max(half3(0, 0, 0), ambient.rgb + ambient_contrib);
                half3 Flast = FresnelSchlickRoughness(ndv,F0, roughness);
                half3 kdLast = (1 - Flast) * (1 - metallic);          //间接光漫反射系数
                half3 iblDiffuseResult = iblDiffuse * kdLast * albedo.rgb;

                //间接光镜面反射
                half mip = CubeMapMip(perceptualRoughness);         //计算Mip等级
                half3 reflectDir = reflect(-viewDir, bump);
                half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, mip);
                half3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);
                //half envBDRF = EnvBRDFApprox(F0,roughness,ndv);
                //float2 envBDRF = tex2D(_LUT, float2(lerp(0, 0.99, ndv), lerp(0, 0.99, roughness))).rg; // LUT采样
                float grazingTerm = saturate(1 - roughness + kd);
                float surfaceReduction = 1 / (pow(roughness,2) + 1);
                float3 iblSpecularResult= surfaceReduction * iblSpecular * FresnelLerp(float4(F0,1.0),grazingTerm,ndv);
                //float3 iblSpecularResult =iblSpecular* (Flast * envBDRF.r + envBDRF.g);//最后通过使用采样得到的r值进行缩放和g值进行偏移得到结果

                half3 IndirectResult = iblDiffuseResult + iblSpecularResult;

                half3 finalResult= DirectLightResult+IndirectResult*occlusion;
                return half4(finalResult,1);
            }
            ENDCG
      }
    }
}参考

KaaPexei 发表于 2021-11-20 15:34

[红心][红心][红心]谢谢分享~
页: [1]
查看完整版本: 【Unity Shader】基于物理的渲染PBR(一)