自己实现PBS - 只考虑直接光

发布时间 2023-05-25 01:54:27作者: yanghui01

PBS的全称为Physically Based Shading,就是基于物理的渲染技术

 

1) 为什么需要PBS?

因为PBS渲染出来的效果更好。

 

2) PBS复杂吗?

理论很复杂,但是应用其实也是套用公式,和传统光照不同的公式。

 

3) PBS的公式是怎样的?

a) 只考虑反射的情况,反射颜色=漫反射+镜面反射,没错这里和传统光照是一样的。

b) 漫反射公式:kd为漫反射系数,除以PI是保证能量守恒的

 c) 镜面反射公式,ks一般为1-kd

 

4) 镜面反射中的D, F, G函数,以及微平面理论。 

D: 法线分布函数, 用于拟算多少比例的微面元能把光线镜面反射到我们的眼睛中

微观层面,所有平面都不是光滑的,由许多微面元组成,也就是表面凹凸不平,这也就造成了入射光线会向各个方向反射,只有反射方向和观察方向重叠的(即微平面法线和半角向量重叠),才能进入到我们的眼睛。

所以,入射光线经过微面元镜面反射后,只有一定比例的光线能进入我们的眼睛。

 

G: 阴影遮掩函数, 用于拟算多少比例的微面元反射被遮掉

微观层面,反射的时候,又遇到突起很高的微面元被挡住了;或者其他情况的遮挡。

 

F: 菲涅尔函数,用于拟算镜面反射比例。

就是达到掠射角大,反射弱;掠射角小,反射强这样的效果。

 

最终效果

 

实际写shader时,因为出于实际效果等因素,并不是完全按照上面的理论公式套用的,部分做了调整

Shader "My/PBS/MyPBS"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
        _Color("Tint Color", Color) = (1, 1, 1, 1)

        _Metallic("Metallic", Range(0, 1)) = 0 //金属度要经过伽马校正
        _Smoothness("Smoothness", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100

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

            CGPROGRAM

            #pragma multi_compile_fwdbase
            //#pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half4 _Color;

            half _Metallic;
            half _Smoothness;

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float3 normal : NORMAL; //顶点法线
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                float3 worldViewDir : TEXCOORD3;
            };

            //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;
            }

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

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal); //法线(世界空间)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; //世界坐标
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); //观看方向(世界空间), 顶点指向观察点

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float3 worldNormal = normalize(i.worldNormal); //法线
                float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); //光源入射方向
                float3 worldViewDir = normalize(i.worldViewDir); //观看方向
                float3 worldHalfDir = normalize(worldLightDir + worldViewDir); //半角向量

                float NdotV = max(saturate(dot(worldNormal, worldViewDir)), 1e-5); //防止除0
                float NdotL = max(saturate(dot(worldNormal, worldLightDir)), 1e-5);
                //float LdotH = max(saturate(dot(worldLightDir, worldHalfDir)), 1e-5);
                float NdotH = max(saturate(dot(worldNormal, worldHalfDir)), 1e-5);
                float VdotH = max(saturate(dot(worldViewDir, worldHalfDir)), 1e-5);

                float perceptualRoughness = 1 - _Smoothness;
                float roughness = PerceptualRoughnessToRoughness(perceptualRoughness); //粗糙度
                roughness = max(roughness, 0.002); //防止为0,保留一点点高光

                half3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; //反射率, 我们看到的颜色就是没被吸收的剩余颜色

                //直接光镜面反射相关
                half D = GGXTerm(NdotH, roughness); //法线分布函数, 用于拟算多少比例的微面元的法线与半角向量重叠
                half G = GeometrySmith(NdotV, NdotL, roughness); //阴影遮掩函数, 用于拟算N=H的微面元中, 有多少比例会被遮挡掉
                half3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, _Metallic); //unity_ColorSpaceDielectricSpec为常数: float3(0.04, 0.04, 0.04)
                half3 F = FresnelSchlick(F0, VdotH); //近似的菲涅尔函数(菲涅尔效应)
                half3 specular = (D * G * F * 0.25) / (NdotV * NdotL) * _LightColor0.rgb * NdotL * UNITY_PI;

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

                return fixed4(diffuse + specular, 1);
            }

            ENDCG
        }
    }
}

 

参考

【Unity Shader】基于物理的渲染PBR(一) - 知乎 (zhihu.com)
如何在Unity中造一个PBR Shader轮子 - 知乎 (zhihu.com)
Unity Shader - PBR相关公式及代码 - 知乎 (zhihu.com)