PBR光照模型相关知识

PBR是基于物理的光照模型,与lambert光照模型以及Blinn-Phong光照模型有所不同

一、三种光照模型的区别

原理基础

  • Lambert 光照模型:基于朗伯余弦定律,该定律表明,漫反射光的强度与入射光的方向和物体表面法线的夹角的余弦值成正比。也就是说,当光线垂直照射物体表面时,漫反射光最强;随着入射角增大,漫反射光逐渐减弱。它只考虑了漫反射,认为物体表面是完全粗糙的,光线在表面均匀散射,不考虑光线的反射方向和视角的影响。
  • Blinn - Phong 光照模型:是对 Phong 光照模型的改进。Phong 光照模型将光照效果分为环境光、漫反射光和镜面反射光三部分。Blinn - Phong 模型同样考虑这三部分,但在计算镜面反射时引入了半程向量(光线方向和视线方向的中间向量),通过计算半程向量与物体表面法线的夹角来确定镜面反射的强度,简化了计算过程。
  • PBR 光照模型:基于物理原理,更准确地模拟光线与物体表面的相互作用。它考虑了光的能量守恒、微表面理论和菲涅尔效应等物理现象。PBR 模型将物体表面看作由许多微小的面元组成,每个面元都遵循物理定律进行反射和折射,从而能够更真实地模拟不同材质在不同光照条件下的外观。

表现效果

  • Lambert 光照模型:由于只考虑漫反射,物体表面看起来比较单调、缺乏光泽和立体感。它适用于模拟一些表面粗糙、没有明显高光的物体,如砖块、未经打磨的石头等。但对于需要表现光泽和反射效果的物体,Lambert 模型的效果就显得不够真实。
  • Blinn - Phong 光照模型:引入了镜面反射,能够在物体表面产生高光效果,使物体看起来更有光泽和立体感。通过调整镜面反射的参数(如高光指数),可以控制高光的大小和强度。然而,Blinn - Phong 模型仍然是一种经验模型,其高光效果在某些情况下可能不够真实,尤其是对于金属等具有特殊反射特性的材质。
  • PBR 光照模型:能够提供更加真实和准确的光照效果。它可以模拟不同材质的独特外观,如金属的强反射、塑料的半光泽等。PBR 模型还能更好地处理不同光照条件下的光照变化,使物体在不同环境中都能呈现出自然的外观。例如,在不同的时间和天气条件下,PBR 模型可以更准确地模拟物体的光照效果。

计算复杂度

  • Lambert 光照模型:计算简单,只需要计算光线方向和物体表面法线的点积,因此计算速度快,对硬件性能的要求较低。这使得它在对性能要求较高的场景中(如早期的游戏和一些实时渲染场景)得到广泛应用。
  • Blinn - Phong 光照模型:在 Lambert 模型的基础上增加了镜面反射的计算,计算复杂度有所提高,但仍然相对较低。它在计算效率和表现效果之间取得了一定的平衡,在许多游戏和实时渲染应用中仍然被广泛使用。
  • PBR 光照模型:由于考虑了更多的物理因素,计算复杂度较高。它需要进行更复杂的数学计算,如微表面分布函数、菲涅尔方程的计算等。这使得 PBR 模型在实时渲染中对硬件性能有较高的要求,但随着硬件技术的不断发展,PBR 模型在游戏、电影等领域的应用越来越广泛。

材质参数

  • Lambert 光照模型:主要使用漫反射颜色这一参数来定义物体表面的颜色,参数简单,易于理解和使用。
  • Blinn - Phong 光照模型:除了漫反射颜色外,还需要定义镜面反射颜色和高光指数等参数。高光指数控制高光的大小和强度,不同的高光指数可以模拟出不同光滑程度的物体表面。
  • PBR 光照模型:使用更多的物理参数来定义物体的材质特性,如基础颜色(Albedo)、金属度(Metallic)、粗糙度(Roughness)等。这些参数能够更准确地描述物体的材质属性,使渲染结果更加真实。例如,金属度参数可以区分金属和非金属材质,粗糙度参数可以控制物体表面的光滑程度。

二、PBR光照模型

PBR光照模型的重要公式:

LightingStandard_GI1(o, giInput, gi);   

// 用于计算全局光照中间接光照的漫反射和镜面反射颜色Specular   

// gi.indirect.diffuse ;
// gi.indirect.specular ; 

fixed4 c = LightingStandard1 (o, giInput.worldViewDir, gi);         

// PBR光照模型的核心计算公式,目的是将直接光的漫反射、镜面反射和间接光的漫反射、镜面反射混合     

函数中参数的具体内容:

  • 1.SurfaceOutputStandard o

//初始化SurfaceOutputStandard
SurfaceOutputStandard o;
UNITY_INITIALIZE_OUTPUT(SurfaceOutputStandard,o);

fixed4 mainTex =tex2D(_MainTex,i.uv);
//fixed3 Albedo  (基础色 三维向量)
o.Albedo=mainTex*_Color;      // base (diffuse or specular) color

//采样法线贴图
half3 normalTex=UnpackNormal(tex2D(_NormalTex,i.uv));
half3 worldNormal=half3(dot(i.tSpace0,normalTex),dot(i.tSpace1,normalTex),dot(i.tSpace2,normalTex));  
o.Normal=worldNormal;

//自发光
o.Emission=0;

//金属度   0=non-metal, 1=metal
fixed4 metallicTex=tex2D(_MetallicTex,i.uv);
o.Metallic=metallicTex.r*_Metallic;  

// 0=rough, 1=smooth
o.Smoothness=metallicTex.g*_Glossiness;    

o.Occlusion=metallicTex.b*_AO;     

o.Alpha=1;      

  •  UnityGI gi

UnityGI gi;
UNITY_INITIALIZE_OUTPUT(UnityGI, gi);  
gi.indirect.diffuse = 0;
gi.indirect.specular = 0;

gi.light.color = _LightColor0.rgb;
//主平行灯的方向
gi.light.dir = _WorldSpaceLightPos0.xyz;

  • UnityGIInput giInput;

UnityGIInput giInput;
UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
giInput.light = gi.light;
giInput.worldPos = worldPos;
giInput.worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
giInput.atten = atten;
giInput.lightmapUV = 0.0;


LightingStandard_GI1(o, giInput, gi)函数源码:

    //计算间接光照的漫反射
    inline UnityGI UnityGI_Base1(UnityGIInput data, half occlusion, half3 normalWorld)
    {
        UnityGI o_gi;
        ResetUnityGI(o_gi);

        // Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason
    #if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
            half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos);
            float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz);
            float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist);
            data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist));
    #endif

        o_gi.light = data.light;
        o_gi.light.color *= data.atten;

    #if UNITY_SHOULD_SAMPLE_SH
            o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
    #endif

    #if defined(LIGHTMAP_ON)
            // Baked lightmaps
            half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy);
            half3 bakedColor = DecodeLightmap(bakedColorTex);

    #ifdef DIRLIGHTMAP_COMBINED
                fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
                o_gi.indirect.diffuse += DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld);

    #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
                    ResetUnityLight(o_gi.light);
                    o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap (o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
    #endif

    #else // not directional lightmap
                o_gi.indirect.diffuse += bakedColor;

    #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
                    ResetUnityLight(o_gi.light);
                    o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
    #endif

    #endif
    #endif

    #ifdef DYNAMICLIGHTMAP_ON
            // Dynamic lightmaps
            fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw);
            half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex);

    #ifdef DIRLIGHTMAP_COMBINED
                half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
                o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld);
    #else
                o_gi.indirect.diffuse += realtimeColor;
    #endif
    #endif

        o_gi.indirect.diffuse *= occlusion;
        return o_gi;
    }
    

    //计算间接光照的镜面反射  Unity_GlossyEnvironmentData为之前的准备数据
    inline half3 UnityGI_IndirectSpecular1(UnityGIInput data, half occlusion, Unity_GlossyEnvironmentData glossIn)
    {
        //声明返回的变量specular,用来存储镜面反射的颜色
        half3 specular;

    //当反射探针中开启了Box Projection模式则执行下面语句
    #ifdef UNITY_SPECCUBE_BOX_PROJECTION
            // we will tweak reflUVW in glossIn directly (as we pass it to Unity_GlossyEnvironment twice for probe0 and probe1), so keep original to pass into BoxProjectedCubemapDirection
            half3 originalReflUVW = glossIn.reflUVW;
            glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[0], data.boxMin[0], data.boxMax[0]);
    #endif

    //当将材质中的reflections的选项去掉时执行下面的语句
    #ifdef _GLOSSYREFLECTIONS_OFF
            specular = unity_IndirectSpecColor.rgb;
    #else
        //若开启了reflections的分支
        half3 env0 = Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn);
        //如果开启了反射探针混合执行下面语句
        #ifdef UNITY_SPECCUBE_BLENDING
                    const float kBlendFactor = 0.99999;
                    float blendLerp = data.boxMin[0].w;
                    UNITY_BRANCH
                    if (blendLerp < kBlendFactor)
                    {
        #ifdef UNITY_SPECCUBE_BOX_PROJECTION
                            glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[1], data.boxMin[1], data.boxMax[1]);
        #endif

                    half3 env1 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0), data.probeHDR[1], glossIn);
                    specular = lerp(env1, env0, blendLerp);
                    }
                    else
                    {
                        specular = env0;
                    }
        #else
            specular = env0;
        #endif
    #endif

        return specular * occlusion;
    }


    inline UnityGI UnityGlobalIllumination1(UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn)
    {
        //声明UnityGI类型的变量 o_gi,将最后求得的o_gi赋值给gi
        //UnityGI_Base函数中计算了全局光照中间接光照的漫反射
        UnityGI o_gi = UnityGI_Base1(data, occlusion, normalWorld);
        //计算间接光照的镜面反射
        o_gi.indirect.specular = UnityGI_IndirectSpecular1(data, occlusion, glossIn);
        return o_gi;
    }
    //由光滑度计算粗糙度
    float SmoothnessToPerceptualRoughness1(float smoothness)
    {
        return (1 - smoothness);
    }


    Unity_GlossyEnvironmentData UnityGlossyEnvironmentSetup1(half Smoothness, half3 worldViewDir, half3 Normal, half3 fresnel0)
    {
        //声明返回的结构体g
        Unity_GlossyEnvironmentData g;

        //粗糙度的计算
        g.roughness /* perceptualRoughness */ = SmoothnessToPerceptualRoughness1(Smoothness);
        //反射球的UV采样坐标(R)
        g.reflUVW = reflect(-worldViewDir, Normal);

        return g;
    }


    //PBR光照模型的全局光照GI中间接光照的漫反射和镜面反射计算
    inline void LightingStandard_GI1(SurfaceOutputStandard s,UnityGIInput data,inout UnityGI gi)
    {
        //#if defined(UNITY_PASS_DEFERRED)如果是延迟渲染的Pass则执行下面语句   
        //&& UNITY_ENABLE_REFLECTION_BUFFERS - render reflection probes in deferred way, when using deferred shading(如果使用延迟渲染的Pass时,使用延迟渲染的方式渲染反射探针)
        #if defined(UNITY_PASS_DEFERRED) && UNITY_ENABLE_REFLECTION_BUFFERS
            gi = UnityGlobalIllumination1(data, s.Occlusion, s.Normal);
        #else
            //Unity_GlossyEnvironmentData是镜面反射的准备数据
            Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.Smoothness, data.worldViewDir, s.Normal, lerp(unity_ColorSpaceDielectricSpec.rgb, s.Albedo, s.Metallic));
            //进行GI计算并输出gi
            gi = UnityGlobalIllumination1(data, s.Occlusion, s.Normal, g);
        #endif
    }

fixed4 c = LightingStandard1 (o, giInput.worldViewDir, gi)函数源码:

    //cosA代表N和V点乘后的结果,视线垂直于物体表面时N·V的结果(cosA的值)最大且接近于1
    inline half3 FresnelLerp1(half3 F0, half3 F90, half cosA)
    {
        half t = Pow5(1 - cosA); // ala Schlick interpoliation
        return lerp(F0, F90, t);
    }

    //V项计算
    inline float SmithJointGGXVisibilityTerm1(float NdotL, float NdotV, float roughness)
    {
    #if 0
        // Original formulation:
        //  lambda_v    = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
        //  lambda_l    = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
        //  G           = 1 / (1 + lambda_v + lambda_l);

        // Reorder code to be more optimal
        half a          = roughness;
        half a2         = a * a;

        half lambdaV    = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
        half lambdaL    = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);

        // Simplify visibility term: (2.0f * NdotL * NdotV) /  ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f));
        return 0.5f / (lambdaV + lambdaL + 1e-5f);  // This function is not intended to be running on Mobile,
                                                    // therefore epsilon is smaller than can be represented by half
    #else
        // Approximation of the above formulation (simplify the sqrt, not mathematically correct but close enough)
        float a = roughness;
        float lambdaV = NdotL * (NdotV * (1 - a) + a);
        float lambdaL = NdotV * (NdotL * (1 - a) + a);

    #if defined(SHADER_API_SWITCH)
        return 0.5f / (lambdaV + lambdaL + UNITY_HALF_MIN);
    #else
        return 0.5f / (lambdaV + lambdaL + 1e-5f);
    #endif

    #endif
    }

    //D项计算
    inline float GGXTerm1(float NdotH, float roughness)
    {
        float a2 = roughness * roughness;
        float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
        //加上1*e的-7次方可以使得分母不为0
        return UNITY_INV_PI * a2 / (d * d + 1e-7f); // This function is not intended to be running on Mobile,
                                                // therefore epsilon is smaller than what can be represented by half
    }
    
    //求解roughness
    float PerceptualRoughnessToRoughness1(float perceptualRoughness)
    {
        return perceptualRoughness * perceptualRoughness;
    }


    //使得归一化向量安全求解
    inline float3 Unity_SafeNormalize1(float3 inVec)
    {
        // normalize(v) = rsqrt(dot(v,v))*v;
        float dp3 = max(0.001f, dot(inVec, inVec));    //使得根号下dot(v,v)的值>0  (使得归一化的值更安全)
        return inVec * rsqrt(dp3);
    }

    //F0是在限定条件下折射和反射的比例,也称为反射率
    inline half3 FresnelTerm1(half3 F0, half cosA)
    {
        half t = Pow5(1 - cosA); // ala Schlick interpoliation
        return F0 + (1 - F0) * t;
    }

    //计算直接光的漫反射分布系数
    half DisneyDiffuse1(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
    {
        half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;     //fd90指的是视线向量与法线向量呈90度夹角
        // Two schlick fresnel term
        half lightScatter = (1 + (fd90 - 1) * Pow5(1 - NdotL));
        half viewScatter = (1 + (fd90 - 1) * Pow5(1 - NdotV));

        return lightScatter * viewScatter;
    }

    half4 BRDF1_Unity_PBS1(half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness,
        float3 normal, float3 viewDir,
        UnityLight light, UnityIndirect gi)
    {
        float perceptualRoughness = SmoothnessToPerceptualRoughness(smoothness);   //return (1 - smoothness);
        float3 halfDir = Unity_SafeNormalize1(float3(light.dir) + viewDir);        //半角向量(灯光向量和视线向量相加得半角向量)

    // NdotV should not be negative for visible pixels, but it can happen due to perspective projection and normal mapping
    // In this case normal should be modified to become valid (i.e facing camera) and not cause weird artifacts.
    // but this operation adds few ALU and users may not want it. Alternative is to simply take the abs of NdotV (less correct but works too).
    // Following define allow to control this. Set it to 0 if ALU is critical on your platform.
    // This correction is interesting for GGX with SmithJoint visibility function because artifacts are more visible in this case due to highlight edge of rough surface
    // Edit: Disable this code by default for now as it is not compatible with two sided lighting used in SpeedTree.
    #define UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 0

    #if UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV
        // The amount we shift the normal toward the view vector is defined by the dot product.
        half shiftAmount = dot(normal, viewDir);
        normal = shiftAmount < 0.0f ? normal + viewDir * (-shiftAmount + 1e-5f) : normal;
        // A re-normalization should be applied here but as the shift is small we don't do it to save ALU.
        //normal = normalize(normal);

        float nv = saturate(dot(normal, viewDir)); // TODO: this saturate should no be necessary here
    #else
        half nv = abs(dot(normal, viewDir)); // This abs allow to limit artifact
    #endif

        float nl = saturate(dot(normal, light.dir));
        float nh = saturate(dot(normal, halfDir));

        half lv = saturate(dot(light.dir, viewDir));
        half lh = saturate(dot(light.dir, halfDir));

        // Diffuse term 迪士尼漫反射
        half diffuseTerm = DisneyDiffuse1(nv, nl, lh, perceptualRoughness) * nl;

        // Specular term
        // 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
        float roughness = PerceptualRoughnessToRoughness1(perceptualRoughness);
    
    //GGX具有比较好的效果
    #if UNITY_BRDF_GGX
        // GGX with roughtness to 0 would mean no specular at all, using max(roughness, 0.002) here to match HDrenderloop roughtness remapping.
        
        roughness = max(roughness, 0.002);
        float V = SmithJointGGXVisibilityTerm1(nl, nv, roughness);
        float D = GGXTerm1(nh, roughness);
    
    #else
        // Legacy
        //half V = SmithBeckmannVisibilityTerm(nl, nv, roughness);
       // half D = NDFBlinnPhongNormalizedTerm(nh, PerceptualRoughnessToSpecPower(perceptualRoughness));
    #endif

        //由于直接光的漫反射强度计算中未除以Pi,所以在镜面反射的计算时需乘以pi以平衡
        float specularTerm = V * D * UNITY_PI; // Torrance-Sparrow model, Fresnel is applied later

    //若处于Gamma空间时,进行下面计算
    #ifdef UNITY_COLORSPACE_GAMMA
            specularTerm = sqrt(max(1e-4h, specularTerm));
    #endif

        // specularTerm * nl can be NaN on Metal in some cases, use max() to make sure it's a sane value
        specularTerm = max(0, specularTerm * nl);   //乘以法线与入射光的点乘结果
    
    //若关闭镜面高光时,使specularTerm = 0.0
    #if defined(_SPECULARHIGHLIGHTS_OFF)
        specularTerm = 0.0;
    #endif

        // surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(roughness^2+1)
        half surfaceReduction;
    //若在Gamma空间
    #ifdef UNITY_COLORSPACE_GAMMA
            surfaceReduction = 1.0-0.28*roughness*perceptualRoughness;      // 1-0.28*x^3 as approximation for (1/(x^4+1))^(1/2.2) on the domain [0;1]
    //若在线性空间
    #else
        surfaceReduction = 1.0 / (roughness * roughness + 1.0); // fade \in [0.5;1]
    #endif

        // To provide true Lambert lighting, we need to be able to kill specular completely.
        specularTerm *= any(specColor) ? 1.0 : 0.0;

        half grazingTerm = saturate(smoothness + (1 - oneMinusReflectivity));
        // 漫反射 = 贴图颜色 * (间接光漫反射+灯光颜色*直接光漫反射系数/强度) 
        half3 diffuse = diffColor * (gi.diffuse + light.color * diffuseTerm); // diffColor=s.Albedo(贴图颜色)
        // 镜面反射 = DV * 灯光颜色 * F
        half3 specular = specularTerm * light.color * FresnelTerm1(specColor, lh);
        // IBL  用于计算间接光照下的镜面反射效果  
        // surfaceReduction通常与物体表面的粗糙度相关,粗糙度越高,表面的微表面结构越复杂,反射光越分散,surfaceReduction 的值可能越小;粗糙度越低,表面越光滑,反射光越集中,surfaceReduction 的值可能越大
        half3 ibl= surfaceReduction * gi.specular * FresnelLerp1(specColor, grazingTerm, nv);
    
        half3 color = diffuse + specular + ibl;
    
        return half4(color, 1);   //最终的颜色效果
        
    }

    #if !defined (UNITY_BRDF_PBS1) // allow to explicitly override BRDF in custom shader
        // still add safe net for low shader models, otherwise we might end up with shaders failing to compile
        #if SHADER_TARGET < 30 || defined(SHADER_TARGET_SURFACE_ANALYSIS) // only need "something" for surface shader analysis pass; pick the cheap one
            #define UNITY_BRDF_PBS BRDF3_Unity_PBS   // 效果最差的BRDF
        #elif defined(UNITY_PBS_USE_BRDF3)
            #define UNITY_BRDF_PBS BRDF3_Unity_PBS   
        #elif defined(UNITY_PBS_USE_BRDF2)
            #define UNITY_BRDF_PBS BRDF2_Unity_PBS
        #elif defined(UNITY_PBS_USE_BRDF1)
            #define UNITY_BRDF_PBS1 BRDF1_Unity_PBS1
        #else
            #error something broke in auto-choosing BRDF  //#error作用:进行报错并使得编译失败
        #endif
    #endif

    inline half OneMinusReflectivityFromMetallic1(half metallic)
    {
        // We'll need oneMinusReflectivity, so
        //   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic) 
        // lerp(dielectricSpec, 1, metallic)为物体高光反射率的计算公式,dielectricSpec为非金属的反射率,1为金属的反射率
        // lerp(A,B,v)=A+v(B-A);
        // 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;
    }

    inline half3 DiffuseAndSpecularFromMetallic1(half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity)
    {
        // specColor:镜面高光颜色
        // lerp(A, B, alpha) 若alpha = 0 则返回A; 若alpha = 1 则返回B; alpha在0-1之间时,返回A与B之间的混合值
        // 当metallic=0(非金属/绝缘体),返回unity_ColorSpaceDielectricSpec.rgb; half4(0.04,0.04,0.04,1.0-0.04) (非金属的反射率≈0.04)
        // unity_ColorSpaceDielectricSpec.rgb表示非金属、绝缘体的通用反射颜色,用0.04来表示反射颜色、反射率
        // 当metallic=1(金属),返回albedo;也就是物体本身的颜色信息
        specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
    
        //计算1-反射率
        oneMinusReflectivity = OneMinusReflectivityFromMetallic1(metallic);
    
        //返回物体的基础颜色贴图乘以(1-反射率=漫反射率)
        return albedo * oneMinusReflectivity;
    }

    // PBR的核心计算
    inline half4 LightingStandard1(SurfaceOutputStandard s, float3 viewDir, UnityGI gi)// s = 模型表面的数据信息 // viewDir = 视线方向 // gi = 全局光照
    {
        s.Normal = normalize(s.Normal);

        half oneMinusReflectivity;
        half3 specColor;
    
        // 该函数的作用是计算s.Albedo; 然后分别计算并输出specColor和oneMinusReflectivity
        s.Albedo = DiffuseAndSpecularFromMetallic1 (s.Albedo, s.Metallic, /*out*/specColor, /*out*/oneMinusReflectivity);

        // shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
        // this is necessary to handle transparency in physically correct way - only diffuse component gets affected by alpha
        // 当开启透明模式时用于计算半透明通道Alpha的值 :/*out*/outputAlpha
        half outputAlpha;
        s.Albedo = PreMultiplyAlpha (s.Albedo, s.Alpha, oneMinusReflectivity, /*out*/outputAlpha);
        
        // s.Albedo = 物体表面的基础颜色
        // specColor = 镜面反射颜色
        // oneMinusReflectivity = 漫反射率
        // s.Smoothness = 物体表面的光滑度
        // s.Normal = 物体表面的法线
        // viewDir = 视线的方向
        // gi.light = 直接光信息
        // gi.indirect = 间接光信息
        half4 c = UNITY_BRDF_PBS1(s.Albedo, specColor, oneMinusReflectivity, s.Smoothness, s.Normal, viewDir, gi.light, gi.indirect);
        c.a = outputAlpha;
        return c;
    }

  三、解析PBR光照模型 

1.总漫反射

 half3 diffuse = diffColor * (gi.diffuse + light.color * diffuseTerm);

 // 总漫反射 = 贴图颜色 * (间接光漫反射+灯光颜色*直接光漫反射系数/强度)

 //diffColor = s.Albedo(贴图颜色)      

2.直接光镜面反射

// 直接光镜面反射 = DV * 灯光颜色 * F
half3 specular = specularTerm * light.color * FresnelTerm1(specColor, lh); 

接下来看一下Blinn-Phong光照模型的直接光镜面反射与PBR光照模型有何区别:

其中,Blinn-Phong 光照模型将光照效果分为环境光、漫反射光和镜面反射光三部分

Cdiff:漫反射颜色

Cspec:高光颜色/镜面反射颜色

m:用来控制高光的范围,m越大,高光的范围越小,光斑越集中

同时可将上面的公式换为以下公式:

之后对上述公式的参数进行一定的调整

在修改后的公式中镜面反射增加了m+8/8π,可以解决当m增大时,光斑越集中而其颜色没有相应的变亮的问题;乘以N·L可以使当物体表面处于背光面时使得高光变暗

上面公式中括号内的即为基于Blinn-Phong的BRDF公式

以上公式是一个简化的基于经验的 BRDF(双向反射分布函数)公式,常用于传统光照模型(如 Blinn - Phong),它与 PBR(基于物理的渲染)中的 BRDF 公式相比,存在以下缺点:

物理准确性:

  • 简化的光学模型(没有考虑到微平面理论):该公式中的漫反射部分只是简单地将漫反射颜色除以,基于朗伯漫反射模型,它假设物体表面是完全均匀且各向同性的漫反射体,没有考虑微表面结构等真实物理因素对漫反射的影响。而 PBR 的 BRDF 公式(如基于微表面理论的 Cook - Torrance BRDF)考虑了表面的微观几何结构,能更准确地描述光线在实际粗糙表面的散射情况。
  • 菲涅尔效应处理不足:在提供的公式的镜面反射部分,没有显式地考虑菲涅尔效应,即光线在不同入射角下反射和折射比例的变化。PBR 的 BRDF 公式会精确地结合菲涅尔方程,能够准确地模拟不同材质在不同角度下反射光的强度变化,比如金属在掠射角下反射光增强的现象。

材质表现能力:

  • 参数单一性:该公式仅通过漫反射颜色和镜面反射颜色以及高光指数来描述材质反射特性,对于复杂材质的表现能力有限。PBR 的 BRDF 公式通常使用更多基于物理的参数,如金属度、粗糙度等,能够更细致地表现各种材质,如区分金属与非金属材质,以及不同粗糙度表面的反射差异。
  • 缺乏各向异性支持:此公式默认材质是各向同性的,即反射特性在各个方向上相同。而 PBR 的 BRDF 可以支持各向异性材质的模拟,比如拉丝金属、头发等具有方向依赖反射特性的材质。

能量守恒:

  • 潜在的能量不守恒:在某些情况下,该经验公式可能无法严格保证能量守恒。例如,当高光指数和反射颜色设置不当时,可能会导致反射光的能量超过入射光的能量。而 PBR 的 BRDF 公式基于物理原理推导,在理论上能够保证能量守恒,确保渲染结果的合理性。

光照一致性:

  • 难以处理复杂光照环境:该公式在处理间接光照、多次反射等复杂光照场景时表现不佳。PBR 的 BRDF 公式可以更好地与全局光照算法集成,准确地模拟光线在场景中的多次反弹和间接光照效果,从而在各种光照条件下提供更一致和真实的渲染结果。

而对于PBR光照模型中的BRDF公式则考虑到了微平面理论(法线分布函数D、可见性函数V)、菲尼尔效果F以及能量守恒等各种问题,从而使得模型具有更复杂且更真实的材质效果

  • 法线分布函数(D)

由于物体表面非绝对光滑,而是由大量微小的面元组成。这些微观面元的法线方向各不相同,法线分布函数用于描述这些微表面法线的分布情况。比如金属表面看似光滑,但在微观尺度下有许多不规则起伏。通过法线分布函数,可以精确刻画不同材质表面微结构的差异,进而准确模拟光线与这些微表面的相互作用,使渲染出的物体更接近真实外观。

法线分布函数在计算镜面反射的过程中起着关键作用,它决定了有多少微表面的法线方向与半程向量h(入射光方向和视线方向的中间向量)接近,从而对镜面反射有贡献。微表面法线与半角向量方向越接近,该微表面对镜面反射的贡献越大;因为只要半角向量和微表面法线越接近,反射光线就有可能进入观察者的视线,并且越接近,反射光线进入视线的概率和强度就越大。

  • 可见性函数(V)

由于自身的遮挡,有很多点根本无法接受到入射光线,也有很多点反射的光线无法射出表面。因此,我们此处再定义一个函数叫“可见性函数”,用V表示。它的作用就是根据给定的入射光线和出射光线的方向,计算出不被自身遮挡的光线的比例

在源码中D和V的计算:

half3 specular = specularTerm * light.color * FresnelTerm1(specColor, lh);

//由于直接光的漫反射强度计算中未除以Pi,所以在镜面反射的计算时需乘以pi以平衡
float specularTerm = V * D * UNITY_PI;

  • 菲尼尔系数(F)

光垂直射向表面时,折射光线最多,反射光线最少;相反当入射光线与法线的夹角越大,反射的光线越多,折射的光线越少

例如,我们能够看见水底下或者玻璃后的东西,那肯定是光线发生了折射所导致的。而对于水面上的倒影或者是玻璃变得像镜子一样这些都是因为光线发生了反射所导致的。

F0 是当入射光线垂直照射到物体表面时,物体的反射率

根据F0求得菲尼尔方程F:

图中代表的是折射率为1.5的绝缘体(例如某种玻璃)的反射情况。可以发现当夹角为0时,即入射光垂直于表面,反射光的比例只有4%左右,而当夹角为90度时,即入射光平行于表面,反射比例将近100%,符合我们前面所说的菲涅尔效应。

上图代表一导体的反射率,光线垂直于导体表面,有90%的光被反射(反射率较高)。这也证实了为什么我们可以用铜来做镜子,却没法使用玻璃来做镜子


​​​​​​​菲涅尔方程(Fresnel Equation) - 知乎 (zhihu.com)

PBR物理光照计算公式推导详解_引擎中的pbr公式-CSDN博客


3.间接光镜面反射 

half3 ibl= surfaceReduction * gi.specular * FresnelLerp1(specColor, grazingTerm, nv);     

// IBL :用于计算间接光照下的镜面反射效果  

surfaceReduction 通常与物体表面的粗糙度相关,粗糙度越高,表面的微表面结构越复杂,反射光越分散,surfaceReduction 的值可能越小;粗糙度越低,表面越光滑,反射光越集中,surfaceReduction 的值可能越大

//若在Gamma空间
#ifdef UNITY_COLORSPACE_GAMMA
        surfaceReduction = 1.0-0.28*roughness*perceptualRoughness;     
//若在线性空间
#else
        surfaceReduction = 1.0 / (roughness * roughness + 1.0);
#endif


FresnelLerp1(specColor, grazingTerm, nv) :

//cosA=v·h / l·h,视线垂直于物体表面时H·V的结果(cosA的值)最大且接近于1
 inline half3 FresnelLerp1(half3 F0, half3 F90, half cosA)
 {
     half t = Pow5(1 - cosA); // ala Schlick interpoliation
     return lerp(F0, F90, t);
 }

四、PBR光照模型代码

Shader "unity/PBR"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        //金属度贴图
        _MetallicTex("Metallic(R) Smoothness(G) AO(B)",2D)="white"{}
        
        //法线贴图
        _NormalTex("Noraml",2D)="bump"{}

        //粗糙度调节杆
        _Glossiness ("Smoothness", Range(0,1)) = 0.5

        //金属度调节杆
        _Metallic ("Metallic", Range(0,1)) = 0.0

        //AO调节杆
        _AO ("AO", Range(0,1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

    // ---- forward rendering base pass:
    Pass {
            Name "FORWARD"
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            //GPU实例化
            //#pragma multi_compile_instancing
            #pragma multi_compile_fog
            #pragma multi_compile_fwdbase

            //渲染优化 (如果没有定义GPU实例化则执行下面代码)
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "UnityPBSLighting.cginc"
            #include "AutoLight.cginc"
            //引用自定义的cginc文件
            #include"../CGIncludes/MyPBRRendering.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _NormalTex;
            sampler2D _MetallicTex;
            half _Glossiness;
            half _Metallic;
            fixed4 _Color;
            half _AO;


            struct appdate
            {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
                float4 texcoord1 : TEXCOORD1;
                float4 texcoord2 : TEXCOORD2;
                float4 texcoord3 : TEXCOORD3;
                fixed4 color : COLOR;
                UNITY_VERTEX_INPUT_INSTANCE_ID
             };
       
            struct v2f 
            {
                float4 pos:SV_POSITION;
                float2 uv : TEXCOORD0; // _MainTex
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                #if UNITY_SHOULD_SAMPLE_SH
                    half3 sh : TEXCOORD3; // SH
                #endif
                UNITY_FOG_COORDS(4)
                UNITY_SHADOW_COORDS(5)
                float3 tSpace0:TEXCOORD6;
                float3 tSpace1:TEXCOORD7;
                float3 tSpace2:TEXCOORD8;
             };

            // vertex shader
            v2f vert (appdate v) 
            {
                v2f o;
                //初始化
                UNITY_INITIALIZE_OUTPUT(v2f,o);

                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                //世界空间下的顶点坐标
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                //模型世界空间下的顶点法线
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);

                //计算切线空间
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
     
                o.tSpace0 = float3(worldTangent.x, worldBinormal.x, worldNormal.x);
                o.tSpace1 = float3(worldTangent.y, worldBinormal.y, worldNormal.y);
                o.tSpace2 = float3(worldTangent.z, worldBinormal.z, worldNormal.z);

                o.worldPos.xyz = worldPos;

                //球谐光照计算
                #if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
                    o.sh = 0;
                    // Approximated illumination from non-important point lights
                    #ifdef VERTEXLIGHT_ON
                    o.sh += Shade4PointLights (
                        unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                        unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                        unity_4LightAtten0, worldPos, worldNormal);
                    #endif
                    o.sh = ShadeSHPerVertex (worldNormal, o.sh);
                #endif

                UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy); // pass shadow and, possibly, light cookie coordinates to pixel shader
                UNITY_TRANSFER_FOG(o,o.pos); // pass fog coordinates to pixel shader
                return o;
            }

            // fragment shader
            fixed4 frag (v2f i) : SV_Target 
            {

                UNITY_EXTRACT_FOG(i);
  
                float3 worldPos = i.worldPos.xyz;

                //初始化SurfaceOutputStandard
                SurfaceOutputStandard o;
                UNITY_INITIALIZE_OUTPUT(SurfaceOutputStandard,o);

                fixed4 mainTex =tex2D(_MainTex,i.uv);
                //fixed3 Albedo  (基础色 三维向量)
                o.Albedo=mainTex*_Color;      // base (diffuse or specular) color

                //采样法线贴图
                half3 normalTex=UnpackNormal(tex2D(_NormalTex,i.uv));
                half3 worldNormal=half3(dot(i.tSpace0,normalTex),dot(i.tSpace1,normalTex),dot(i.tSpace2,normalTex));  
                o.Normal=worldNormal;

                //自发光
                o.Emission=0;

                //金属度   0=non-metal, 1=metal
                fixed4 metallicTex=tex2D(_MetallicTex,i.uv);
                o.Metallic=metallicTex.r*_Metallic;  

                // 0=rough, 1=smooth
                o.Smoothness=metallicTex.g*_Glossiness;    

                o.Occlusion=metallicTex.b*_AO;     

                o.Alpha=1;      

                // compute lighting & shadowing factor
                UNITY_LIGHT_ATTENUATION(atten, i, worldPos)

                // Setup lighting environment
                UnityGI gi;
                UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
                gi.indirect.diffuse = 0;
                gi.indirect.specular = 0;

                gi.light.color = _LightColor0.rgb;
                //主平行灯的方向
                gi.light.dir = _WorldSpaceLightPos0.xyz;

                //
                UnityGIInput giInput;
                UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
                giInput.light = gi.light;
                giInput.worldPos = worldPos;
                giInput.worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
                giInput.atten = atten;
                giInput.lightmapUV = 0.0;
                //如果开启球谐
                #if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
                    giInput.ambient = i.sh;
                #else
                    giInput.ambient.rgb = 0.0;
                #endif
                //对CubeMap进行解码的数据
                giInput.probeHDR[0] = unity_SpecCube0_HDR;
                // giInput.probeHDR[1] = unity_SpecCube1_HDR;
                // #if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION)
                // giInput.boxMin[0] = unity_SpecCube0_BoxMin; //.w holds lerp value for blending
                // #endif
                // #ifdef UNITY_SPECCUBE_BOX_PROJECTION
                // giInput.boxMax[0] = unity_SpecCube0_BoxMax;
                // giInput.probePosition[0] = unity_SpecCube0_ProbePosition;
                // giInput.boxMax[1] = unity_SpecCube1_BoxMax;
                // giInput.boxMin[1] = unity_SpecCube1_BoxMin;
                // giInput.probePosition[1] = unity_SpecCube1_ProbePosition;
                // #endif

                LightingStandard_GI1(o, giInput, gi);
                // return fixed4(gi.indirect.specular,1);

                // realtime lighting: call lighting function
                //PBS的核心计算(参数o:模型的表面数据,例如表面颜色、法线、自发光等; gi参数:与全局光照有关的数据)
                fixed4 c= LightingStandard1 (o, giInput.worldViewDir, gi);

                //雾效的应用 // apply fog
                UNITY_APPLY_FOG(_unity_fogCoord, c); 

                //使得c.a=1
                UNITY_OPAQUE_ALPHA(c.a);
                return c;
            }

            ENDCG

            }

    #LINE 50

    }
}

PBR材质详解_哔哩哔哩_bilibili      —可以参考此视频

 其中_MetallicTex贴图的红绿蓝通道分别为金属度贴图、粗糙度贴图、AO贴图

如何将Standard Surface Shader 转换为 Unlit Shader?

选中创建后的Standard Surface Shader,点击右侧的Show generated code可将Surface Shader编译为顶点片元Shader

然后将编译后的代码复制到创建好的Standard Surface Shade中,然后进行修改