找回密码
 立即注册
查看: 153|回复: 0

学习笔记-Unity实现PBR

[复制链接]
发表于 2022-11-29 11:22 | 显示全部楼层 |阅读模式
知乎上有许多大佬其实已经总结过了在Unity实现多种PBR材质的方法,我作为一名还在学习的小菜鸡,这篇文章只是一个学习笔记,欢迎大佬们纠正错误^_^本文尝试实现的是COD里的PBR材质。



整体效果

首先是核心的渲染方程:


这个公式无需赘述,有很多大佬已经对其进行了解释,一个点的亮度收到了许多不同的入射光Li(Wi)不同影响的累加效果,对其进行无穷小的累加是对该点进行半球积分。
其次是BRDF,双向反射分布函数,是一个用来描述表面如何反射光线的方程,其进阶还有BTDF是一个用来描述物体如何投射光线的方程,BSDF=BRDF+BTDF, 业界广泛采用的公式是基于微平面理论的:Microfacet Cook-Torrance BRDF形式


其中F项代表菲涅尔函数,D项为法线分布函数,G项为几何函数,这里是直接光的镜面反射项的使用,实际的效果应该是镜面反射项+漫反射项。
镜面反射项:
菲涅尔函数:
F项:
使用公式:


代码:
half3 F0 = lerp(half3(0.04,0.04,0.04),BaseColor,Metallic);
half3 Fresnel = F0 + (1 - F0) * pow( 1 - saturate(dot(LightDir,H)), 5);使用的是Schlick近似法,光线以不同的角度射向物体也就是入射会有不同的反射率,相同的入射角度也会有不同的反射率。F0是0度角入射的菲涅尔反射值。
法线分布函数:
D项:
使用公式:


代码:
float n = pow(2,13*Gloss);
float PI = 3.1415926;
float NormalizedFactor = (n+2)/(2*PI);
float NormalizedBlin = dot(H,Worldnormal);
NormalizedBlin = pow(NormalizedBlin,n) * NormalizedFactor;
Blinn-Phong分布,其中n是Blinn-PhongNDF的粗糙度参数,高值表示光滑表面,低值表示粗糙表面。在COD:Black Ops中其被设为8192。
阴影遮挡函数:
G项:
严格意义上的应该称它为几何函数,是保证MicrofacetBRDF理论上能量守恒,逻辑上自洽的重要一环,描述了微平面自阴影的属性,表示具有半矢量法线的微平面中,同时被入射方向和反射方向可见,也就是没有被遮挡的比例,也是没有被遮挡m=h微表面的百分比。其实现形式是多种多样的,从Cook-Torrance GSF, Neumann GSF等等到分离遮蔽阴影类型的SmithGGX导出的G项,Smith-BeckmanGSF, Shlick-BeckmanGSF,这里实现的是分离遮蔽阴影类型的。
公式:


代码:
float Vis_SmithJointApprox(float a2, float NoV, float NoL )
            {
                float a = sqrt(a2);
                float Vis_SmithV = NoL * ( NoV * (1 - a) + a );
                float Vis_SmithL = NoV * ( NoL * (1 - a) + a );
                return 0.5 * rcp( Vis_SmithV + Vis_SmithL );
            }漫反射项:
COD的PBRshader与UnityStandardShader在直接光漫反射部分都是相同的,都是采用了一个Lambert光照模型,并且都在下面除了一个PI,这个PI的作用是保证能量守恒。


half3 DiffuseColor = lerp(BaseColor,half3(0,0,0), Metallic)/3.14;
half lambert = max(0, dot(Worldnormal,LightDir));
float3 Final = lambert*DiffuseColo间接光照:
间接高光:IBL的意思是把光照部分烘焙到图像中,烘焙出多个粗糙度下的环境图,对其进行采样。这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。
间接漫反射:环境贴图cubemap积分成模糊的全局光照贴图,再将全局光照贴图投影到球谐光照的基函数上储存。
总结:
总的来说PBR实现的方式有非常多种,只需要计算出直接光照,间接光照,自发光,最后的光照效果也是计算完这三个光加起来就可以了,其直接光照的计算方式是多种多样的,TA在项目中需要考虑到游戏引擎,光照环境,项目性能开销,游戏整体美术效果与氛围来做出决定。掌握PBR的实现是作为一个TA的基础要求,TA新手并不一定需要去完全理解这些复杂数学公式其中的原理,只需要掌握其中的基础知识,并知道这些公式在是用来做什么的,并将它们使用代码实现出来即可,随着学习工作的时间增加,将其完全掌握。
完整代码:
Shader "PBR/PBR_02"
{
    Properties
    {
        _MainTex ("Texture1", 2D) = "blck"{}
        BaseColor ("BaseColor", Color) = (1,1,1,1)
        Gloss("Gloss",Range(0,1))=0.5
        Metallic("Metallic", Range(0,1)) = 0
    }
    SubShader
    {
        pass
        {
            Tags {"LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex VS
            #pragma fragment PS
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #include "Lighting.cginc"
            struct vertexInput
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };

            struct VertexToPs
            {
                float4 NDCPos : SV_POSITION;
                float3 Worldnormal : TEXCOORD0;
                float3 WorldPos : TEXCOORD1;
                float3 GI : TEXCOORD2;
            };

            VertexToPs VS(vertexInput input)
            {
                VertexToPs O;
                O.NDCPos = UnityObjectToClipPos(input.vertex.xyz);
                O.Worldnormal = UnityObjectToWorldNormal(input.normal.xyz);
                O.WorldPos = mul(unity_ObjectToWorld,input.vertex);
                O.GI = ShadeSH9(float4(O.Worldnormal,1));
                return O;
            }

            float3 BaseColor;
            float Gloss;
            float Metallic;

            float GeometrySchlickGGX(float NdotV, float roughness)
            {
                float r = (roughness + 1.0);
                float k = (r*r) / 8.0;

                float nom = NdotV;
                float denom = NdotV * (1.0 - k) + k;

                return nom / denom;
            }

            float GeometrySmith(half3 N, half3 V, half3 L,float roughness)
            {
                float NdotV = max(dot(N, V), 0.0);
                float NdotL = max(dot(N, L), 0.0);
                float ggx2 = GeometrySchlickGGX(NdotV, roughness);
                float ggx1 = GeometrySchlickGGX(NdotL, roughness);

                return ggx1 * ggx2;
            }

            float Vis_SmithJointApprox(float a2, float NoV, float NoL )
            {
                float a = sqrt(a2);
                float Vis_SmithV = NoL * ( NoV * (1 - a) + a );
                float Vis_SmithL = NoV * ( NoL * (1 - a) + a );
                return 0.5 * rcp( Vis_SmithV + Vis_SmithL );
            }//阴影-遮挡函数

            float V_SmithGGXCorrelated(float NoV, float NoL, float a)
            {
                float a2 = a * a;
                float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
                float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
                return 0.5 / (GGXV + GGXL);
            }

            float4 PS(VertexToPs input):SV_TARGET
            {
                float3 LightDir = _WorldSpaceLightPos0.xyz;
                LightDir = normalize(LightDir);

                float3 Worldnormal = input.Worldnormal;
                Worldnormal = normalize(Worldnormal);

                float3 VDir = UnityWorldSpaceViewDir(input.WorldPos);
                VDir = normalize(VDir);

                float3 H = LightDir + VDir;
                H = normalize(H);

                float Blin = dot(H,Worldnormal);

                float n = pow(2,13*Gloss);
                float PI = 3.1415926;
                float NormalizedFactor = (n+2)/(2*PI);
                float NormalizedBlin = dot(H,Worldnormal);
                NormalizedBlin = pow(NormalizedBlin,n) * NormalizedFactor;

                half3 DiffuseColor = lerp(BaseColor,half3(0,0,0), Metallic)/3.14;

                float3 R = reflect(-LightDir, Worldnormal);
                R = normalize(R);

                half lambert = max(0, dot(Worldnormal,LightDir));
               
                //菲涅尔函数
                half3 F0 = lerp(half3(0.04,0.04,0.04),BaseColor,Metallic);
                half3 Fresnel = F0 + (1 - F0) * pow( 1 - saturate(dot(LightDir,H)), 5);

                half a = 1/sqrt(PI/4 * (1 - Gloss) + PI/2);
                half NoL = saturate(dot(LightDir,Worldnormal));
                half NoV = saturate(dot(Worldnormal,VDir));
                half VisV = NoV * (1 - a) + a;
                half VisL = NoL * (1 - a) + a;
                half V = 1/(VisV * VisL)/(4 * NoL * NoV);
               
                float3 Final = lambert*DiffuseColor+max(0,NormalizedBlin*Fresnel*V*NoL);

                float3 reflectionDir = reflect(-VDir , Worldnormal);
                Unity_GlossyEnvironmentData envData;
                envData.roughness = 1 - Gloss;
                envData.reflUVW = reflectionDir;
                float3 specular = Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData);
                Final += specular * F0;

                float3 GI = ShadeSH9(float4(Worldnormal,1));
                Final = Final + GI * DiffuseColor * 3.14;
                return float4(Final,1);
            }
            ENDCG
        }
    }
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-18 12:50 , Processed in 0.091417 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表