實現一個前向渲染的Phong模型(一)

陈侠云發表於2024-04-17

標準Phong模型實現

Shader "Unlit/PhongJian"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Shininess ("Shininess", Range(0.01, 100)) = 1.0    // 高光亮度對比度
        _SpecIntensity("SpecIntensity",Range(0.01,5)) = 1.0 // 高光亮度控制
        _AmbientColor ("Ambient Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
			#pragma multi_compile_fwdbase
			#include "UnityCG.cginc"
			#include "AutoLight.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal_dir : TEXCOORD1;
                float3 pos_world : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _LightColor0;
            float _Shininess;
            float4 _AmbientColor;
            float _SpecIntensity;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.normal_dir = normalize(mul(float4(v.normal, 0), unity_WorldToObject).xyz);
                o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base_color = tex2D(_MainTex, i.uv);
                
                half3 normal_dir = normalize(i.normal_dir);
                half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
                half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);

                half NdotL = dot(normal_dir, light_dir);
                half3 diffuse_color = max(0, NdotL) * _LightColor0.xyz * base_color;

                half3 reflect_dir = reflect(-light_dir, normal_dir);
                half RdotV = dot(reflect_dir, view_dir);
                half3 spec_color = pow(max(0, RdotV), _Shininess) * _LightColor0.xyz * _SpecIntensity;

                half3 final_color = diffuse_color + spec_color + _AmbientColor.xyz;
                
                return fixed4(final_color,1);
            }
            ENDCG
        }
    }
    
    Fallback "Diffuse" 
}

AO貼圖

Ambient Occlusion(環境遮擋貼圖)簡稱AO貼圖,模擬物體之間所產生的陰影,在不打光的時候增加體積感。也就是完全不考慮光線,單純基於物體與其他物體越接近的區域,受到反射光線的照明越弱這一現象來模擬現實照明(的一部分)效果。
image

遮擋貼圖用於提供關於模型哪些區域應接受高或低間接光照的資訊。間接光照來自環境光照和反射,因此模型的深度凹陷部分(例如裂縫或摺疊位置)實際上不會接收到太多的間接光照。

遮擋紋理貼圖通常由 3D 應用程式使用建模器或第三方軟體直接從 3D 模型進行計算。

遮擋貼圖是灰度影像,其中以白色表示應接受完全間接光照的區域,以黑色表示沒有間接光照。有時,對於簡單的表面而言,這就像灰度高度貼圖一樣簡單(例如前面高度貼圖示例中顯示的凸起石牆紋理)。

在其他情況下,生成正確的遮擋紋理稍微複雜一些。例如,如果場景中的角色穿著罩袍,則罩袍的內邊緣應設定為非常低的間接光照,或者完全沒有光照。在這些情況下,遮擋貼圖通常將由美術師製作,使用 3D 應用程式基於模型自動生成遮擋貼圖。

程式碼實現

half4 ao_color = tex2D(_AOMap, i.uv);
half3 final_color = (diffuse_color + spec_color + _AmbientColor.xyz) * ao_color;

image
左:無AO 右:有AO

點光源衰減公式計算

                half3 light_dir = normalize(_WorldSpaceLightPos0.xyz - i.pos_world);
                half distance = length(_WorldSpaceLightPos0.xyz - i.pos_world);
                half range = 1.0 / unity_WorldToLight[0][0];
                half attuenation = saturate((range - distance) / range);

法線貼圖

每個頂點有法線和切線資料,其中切線的走向模型匯入unity時根據UV座標中的U來確定的(並不是模型自帶),下圖藍色是法線,綠色是切線,紅色是副切線
image

法線、切線、副切線

                o.normal_dir = normalize(mul(float4(v.normal, 0), unity_WorldToObject).xyz);
                o.tangent_dir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0)).xyz);
                o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir)) * v.tangent.w;          // tangent.w 是為了處理不同平臺下的翻轉問題,不是1就是-1

PC端上,採用DXT/BC對法線貼圖壓縮,通道資訊會變更,所以要用UnpackNormal函式進行解碼,順便把法線圖範圍從0-1轉為-1到1
image
解碼後,會得到如下圖所示的貼圖,其中大量藍色說明法線仍然是平面朝上的(0,0,1),紅色是(0,1,0)程式碼沿著平面,這樣在特定角度看,我們就能感受到它的凹凸不平,但平行於平面看還是會感覺很平,這是因為沒有它的3D資料。
image
_NormalIntensity法線xy偏移強度,預設為1,下面會將將頂點法線改變為貼圖上的位置

                half3 normal_data = UnpackNormal(normalMap);
                normal_data.xy = normal_data.xy * _NormalIntensity;
                
                half3 normal_dir = normalize(i.normal_dir);
                half3 tangent_dir = normalize(i.tangent_dir);
                half3 binormal_dir = normalize(i.binormal_dir);
                float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
                normal_dir = normalize(mul(normal_data.xyz, TBN));
                //normal_dir = normalize(tangent_dir * normal_data.x * _NormalIntensity + binormal_dir * normal_data.y * _NormalIntensity + normal_dir * normal_data.z);
                

右圖:無法線貼圖 左圖:有法線貼圖
image

完整程式碼提供
點選檢視程式碼
Shader "Unlit/PhongJian"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NormalMap("NormalMap",2D) = "bump"{}
        _AOMap ("Texture", 2D) = "white" {}                 // AO貼圖
        _SpecMask ("Texture", 2D) = "white" {}              // 粗糙度貼圖反向得到
        _Shininess ("Shininess", Range(0.01, 100)) = 1.0    // 高光亮度對比度
        _SpecIntensity("SpecIntensity",Range(0.01,5)) = 1.0 // 高光亮度控制
        _NormalIntensity("NormalIntensity", Range(0, 5)) = 1// 法線xy偏移強度
        _AmbientColor ("Ambient Color", Color) = (1,1,1,1)  
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
			#pragma multi_compile_fwdbase
			#include "UnityCG.cginc"
			#include "AutoLight.cginc"

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal_dir : TEXCOORD1;
                float3 pos_world : TEXCOORD2;
                float3 tangent_dir : TEXCOORD3;
                float3 binormal_dir : TEXCOORD4;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _LightColor0;
            float _Shininess;
            float4 _AmbientColor;
            float _SpecIntensity;
            sampler2D _AOMap;
            sampler2D _SpecMask;
            sampler2D _NormalMap;
            float _NormalIntensity;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.normal_dir = normalize(mul(float4(v.normal, 0), unity_WorldToObject).xyz);
                o.tangent_dir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0)).xyz);
                o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir)) * v.tangent.w;          // tangent.w 是為了處理不同平臺下的翻轉問題,不是1就是-1
                o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base_color = tex2D(_MainTex, i.uv);
                half4 ao_color = tex2D(_AOMap, i.uv);
                half4 spec_mask = tex2D(_SpecMask, i.uv);
                half4 normalMap = tex2D(_NormalMap, i.uv);
                half3 normal_data = UnpackNormal(normalMap);
                normal_data.xy = normal_data.xy * _NormalIntensity;
                
                half3 normal_dir = normalize(i.normal_dir);
                half3 tangent_dir = normalize(i.tangent_dir);
                half3 binormal_dir = normalize(i.binormal_dir);
                float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
                normal_dir = normalize(mul(normal_data.xyz, TBN));
                //normal_dir = normalize(tangent_dir * normal_data.x * _NormalIntensity + binormal_dir * normal_data.y * _NormalIntensity + normal_dir * normal_data.z);
                
                half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
                half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);

                half NdotL = dot(normal_dir, light_dir);
                half3 diffuse_color = max(0, NdotL) * _LightColor0.xyz * base_color;

                half3 reflect_dir = reflect(-light_dir, normal_dir);
                half RdotV = dot(reflect_dir, view_dir);
                half3 spec_color = pow(max(0, RdotV), _Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;

                half3 final_color = (diffuse_color + spec_color + _AmbientColor.xyz) * ao_color;
                
                return fixed4(final_color,1);
            }
            ENDCG
        }
        
        Pass
        {
            Tags{"LightMode" = "ForwardAdd"}
            Blend One One
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
			#pragma multi_compile_fwdadd
			#include "UnityCG.cginc"
			#include "AutoLight.cginc"

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal_dir : TEXCOORD1;
                float3 pos_world : TEXCOORD2;
                float3 tangent_dir : TEXCOORD3;
                float3 binormal_dir : TEXCOORD4;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _LightColor0;
            float _Shininess;
            float4 _AmbientColor;
            float _SpecIntensity;
            sampler2D _AOMap;
            sampler2D _SpecMask;
            sampler2D _NormalMap;
            float _NormalIntensity;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.normal_dir = normalize(mul(float4(v.normal, 0), unity_WorldToObject).xyz);
                o.tangent_dir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0)).xyz);
                o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir)) * v.tangent.w;          // tangent.w 是為了處理不同平臺下的翻轉問題,不是1就是-1
                o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base_color = tex2D(_MainTex, i.uv);
                half4 ao_color = tex2D(_AOMap, i.uv);
                half4 spec_mask = tex2D(_SpecMask, i.uv);
                half4 normalMap = tex2D(_NormalMap, i.uv);
                half3 normal_data = UnpackNormal(normalMap);
                normal_data.xy = normal_data.xy * _NormalIntensity;
                
                half3 normal_dir = normalize(i.normal_dir);
                half3 tangent_dir = normalize(i.tangent_dir);
                half3 binormal_dir = normalize(i.binormal_dir);
                float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
                normal_dir = normalize(mul(normal_data.xyz, TBN));
                //normal_dir = normalize(tangent_dir * normal_data.x * _NormalIntensity + binormal_dir * normal_data.y * _NormalIntensity + normal_dir * normal_data.z);
                
                half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);

                #if defined (DIRECTIONAL)
                half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
                half attenuation = 1;
                #elif defined (POINT)
                half3 light_dir = normalize(_WorldSpaceLightPos0.xyz - i.pos_world);
                half distance = length(light_dir);
                half range = 1.0 / unity_WorldToLight[0][0];
                half attenuation = saturate((range - distance) / range);
                #endif
                
                half NdotL = dot(normal_dir, light_dir);
                half3 diffuse_color = max(0, NdotL) * _LightColor0.xyz * base_color * attenuation;

                half3 reflect_dir = reflect(-light_dir, normal_dir);
                half RdotV = dot(reflect_dir, view_dir);
                half3 spec_color = pow(max(0, RdotV), _Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb * attenuation;

                half3 final_color = (diffuse_color + spec_color) * ao_color;        // add中不計算環境光
                
                return fixed4(final_color,1);
            }
            ENDCG
        }
    }
    
    Fallback "Diffuse" 
}

##### 最終效果

image

參考文件

  1. https://mp.weixin.qq.com/s?__biz=MzU0MDcxOTc5MA==&mid=2247493155&idx=1&sn=856fc57a251058d31695f840c88c277e&chksm=fb3649e2cc41c0f4e479fc23d468ef3491f55b3e3ee6f109205b92a873b0d5776ae8474faf26&scene=27
  2. https://zhuanlan.zhihu.com/p/655471214

相關文章