我們做光柵化模式的渲染都瞭解有兩種比較常用的渲染方式,一個是blinnphong的渲染,一個是pbr的渲染。
blinnphong:
blinnphong的渲染模式更多的是一種經驗值模擬光照對物體的效果。所以他不是一個正確的能量守恆的渲染方式。
blinnphong的渲染公式
其中是漫反射的顏色值,也就是我們的貼圖乘顏色。是高光圖乘高光顏色
是頂點法線與光線方向的點積,主要要得到光線與法線的角度,決定漫反射的顏色。
是頂點法線與半角向量的點積,主要得到光線照射到視角範圍的角度。
原理:
爲什麼需要H呢?
按照現實的光照顯示,應該是光線射到地面上每個分子然後進行反射或散射後吸收熱量然後最終反射出來的光線。但是如果按照這個方式去判斷究竟顯示的是什麼內容的話,那計算量會非常大。
而我們其實只關心的是射入我們眼球範圍內的光照,那麼我們就需要尋找跟我們眼球反射有關的物體反射信息,所以用到了半角向量,半角向量是通過入射光線到點、射線方向和點相加後得到的向量,實際上他反映了是與視線和燈光相關的法線信息。
那麼我們要判斷指定部位的光照強度就可以用半角向量與頂點的法線向量做點乘後得出。點乘值越大說明法線與半角向量越重合,那麼高光肯定越高。
那麼這裏可能會考慮另一個問題,正常來說法線向量和半角向量重疊才能說明光線L射入了視線V中。爲什麼用來表示呢?因爲點乘可能會再-1到1之間。
是因爲我們真實世界是每個分子都能做該運算,然後照到瞳孔接收光線範圍內就讓我們看到了物體的顏色。但再光柵化下,每個像素可能對應的是多個分子,我們要做的只能是得出該像素下分子反射的平均值,如果我們要L和V的法線完全與H重疊的話,會導致一些像素上完全沒有高光,看起來是不自然的。所以我們再光柵化下接受了點乘再-1到1之間的浮點數值來表示他再該像素上高光的比值。
說完參數原理,我們可以開始帶入具體的參數來實現我們的效果了。
這裏跟有關係的原因是,我們知道圓面積是,已經單位圓的半徑r=1,所以單位圓面積就是。
我們做的顏色控制都是再對單元圓做的,所以需要用顏色除單位圓面積得到具體面積內的顏色。
然後需要說下m,m是一個高光聚焦強度的值,m越大越聚焦,反之越小。同樣需要跟單位圓做運算。可以試着把值帶進去就知道效果了。
之後的Cspec就是高光的顏色信息,沒什麼可說的了。
代碼:
Shader "Custom/BlinnPhong"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SpecTex("SpecTex", 2D) = "white" {}
_SpecColor("SpecColor", Color) = (1,1,1,1)
_SpecM("Spec M", Range(0.0, 10.0)) = 0.5
}
SubShader{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}
Pass
{
Tags{ "LightMode" = "ForwardBase"}//設置光照類型
Blend SrcAlpha OneMinusSrcAlpha //開啓顏色混合模式
CGPROGRAM
#pragma vertex vert //vextex着色器階段
#pragma fragment frag //fragment着色器階段
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
sampler2D _SpecTex;
float4 _SpecTex_ST;
fixed4 _SpecColor;
fixed _SpecM;
//定義輸入頂點着色器階段的數據結構
struct Input
{
float4 vertex : POSITION; //頂點位置
float4 texcoord : TEXCOORD0; //紋理座標
float4 normal : NORMAL;
float2 uv : TEXCOORD1;
};
//定義頂點着色器階段輸出的數據結構
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 normalWorld : TEXCOORD1;
float4 posWorld : TEXCOORD2;
};
//輸出v2f到下一渲染階段
v2f vert(Input v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float4 posWorld = mul(unity_ObjectToWorld, v.vertex);
float3 normalWorld = UnityObjectToWorldNormal(v.normal);
o.uv = v.uv;
o.normalWorld = normalWorld;
o.posWorld = posWorld;
return o;
}
fixed4 frag(v2f i) :SV_TARGET
{
float3 lightDirWorld = normalize(_WorldSpaceLightPos0.xyz - i.posWorld.xyz);
half3 viewDirWorld = normalize(_WorldSpaceCameraPos - i.posWorld.xyz);
half NdotL = saturate(dot(i.normalWorld, lightDirWorld));
half3 halfDir = normalize(lightDirWorld + viewDirWorld);
half NdotH = saturate(dot(i.normalWorld, halfDir));
float4 mainTex = tex2D(_MainTex, i.uv) * _Color;
float4 specTex = tex2D(_SpecTex, i.uv) * _SpecColor;
half pi = 3.1415926;
float3 color = (mainTex.rgb / pi + (_SpecM + 8) / (8 * pi) * specTex.rgb * (NdotH * NdotH * NdotH * NdotH * NdotH)) * NdotL;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
總結:
blinnphong是一個前人做出來的經驗值算法,這個算法因爲簡單,運算低,所以不需要真實渲染或要求比較低的話可以用這個公式代替渲染方程。而且因爲不需要真實渲染,這裏也可以只用用GAMMA空間就足夠了,不需要用到線性空間。整體運算量還是比較小的。
PBR:
因爲BlinnPhong是一些渲染的經驗模型,所以他的適用範圍就比較受限,那麼考慮到一個更通用,更合理的運算公式。一個基於物理平衡的渲染公式自然就被研究出來了。
原理:
PBR運算的是一個像素內光照反射和折射後的信息比值。
既然是基於物理平衡的真實渲染,那麼pbr自然就有自己的一套理念(借用一下別人的總結):
- 微平面理論(Microfacet Theory)。微平面理論是將物體表面建模成做無數微觀尺度上有隨機朝向的理想鏡面反射的小平面(microfacet)的理論。在實際的PBR 工作流中,這種物體表面的不規則性用粗糙度貼圖或者高光度貼圖來表示。
- 能量守恆(Energy Conservation)。出射光線的能量永遠不能超過入射光線的能量。隨着粗糙度的上升鏡面反射區域的面積會增加,作爲平衡,鏡面反射區域的平均亮度則會下降。
- 菲涅爾反射(Fresnel Reflectance)。光線以不同角度入射會有不同的反射率。相同的入射角度,不同的物質也會有不同的反射率。萬物皆有菲涅爾反射。F0是即 0 度角入射的菲涅爾反射值。大多數非金屬的F0範圍是0.02~0.04,大多數金屬的F0範圍是0.7~1.0。
- 線性空間(Linear Space)。光照計算必須在線性空間完成,shader 中輸入的gamma空間的貼圖比如漫反射貼圖需要被轉成線性空間,在具體操作時需要根據不同引擎和渲染器的不同做不同的操作。而描述物體表面屬性的貼圖如粗糙度,高光貼圖,金屬貼圖等必須保證是線性空間。
- 色調映射(Tone Mapping)。也稱色調複製(tone reproduction),是將寬範圍的照明級別擬合到屏幕有限色域內的過程。因爲基於HDR渲染出來的亮度值會超過顯示器能夠顯示最大亮度,所以需要使用色調映射,將光照結果從HDR轉換爲顯示器能夠正常顯示的LDR。
- 物質的光學特性(Substance Optical Properties)。現實世界中有不同類型的物質可分爲三大類:絕緣體(Insulators),半導體(semi-conductors)和導體(conductors)。在渲染和遊戲領域,我們一般只對其中的兩個感興趣:導體(金屬)和絕緣體(電解質,非金屬)。其中非金屬具有單色/灰色鏡面反射顏色。而金屬具有彩色的鏡面反射顏色。即非金屬的F0是一個float。而金屬的F0是一個float3,如下圖。
渲染公式
Le是自發光,一些物體可能會有自發光的信息或者有自發光的圖片,通過這個可以得到本身發光物體的顏色信息。
fr是入射與出射之間的反射比例,一般有BRDF,BTDF,BSDF(就是BRDF+BTDF),BSSRDF等。
Li是入射光亮度
是法線與反射的點乘,得出的值可以表示入射光的衰減
是入射方向的半球積分,可以立即理解爲累計的數據值。
近似解
通過蒙特卡洛積分的方式,通過N項相加可以求出積分的近似解。
=*
環境光照
其中是環境光照,我們可以用cubemap 來代替=Cubemap.sample(r.mip)
BRDF
然後我們可以根據高光的BRDF來算自身顏色
其中需要說明的是VDF
D是法線分佈函數,相當於是確定再一個像素內所有分子的法線分佈相關信息的均值。體現得更好得法線分佈函數會有更好得高光長尾。
F是Fresnel的簡寫,是一個反射與折射的關係式,我們現實環境中看物體,比如水面,垂直看下來會比較容易看到水底下的物體,是因爲折射大於反射。越接近平面的看水面會看到反射的內容越多,這是因爲反射大於折射。
V是一個結合函數(其實應該是G(Gemetry Function))這裏描述的是被自身遮擋的信息。
三種情況會導致遮擋,一是光照照射不到的地方,二是視圖方向看不到的物體信息,三是反射後進入視線的信息。
公式分別是:
其中F0是反射的值,分爲導體和電解質,導體一般會有rgb三個不同的顏色,而電解質一般rgb三個顏色是一樣的值。默認的電解質爲(0.4,0.4,0.4),但也有可能一個像素內出現金屬和非金屬,所以我們需要用一個公式來近似這個解(金屬比值加上非金屬比值):(Metallic是當前像素的金屬度)
,這裏是對光照和對視線方向都做了GGX運算結合起來得到視線與光照方向的遮蔽信息。下面除於是對運算的值做光照方向和視線方向的糾正的值
公式爲:,其中k爲
最後爲了能量守恆,應該要把能量消耗的損失加上:
也就是前面公式下的Kdiff和Kspec。
Kspec描述的是有多少光的能靈被高光反射,也就是前面的F0。
Kdiff則是高光吸收後剩下的光線消耗:
代碼:
Shader "Custom/BRDF"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SpecTex("SpecTex (RGB)", 2D) = "white" {}
_SpecColor("SpecColor", Color) = (1,1,1,1)
_Roughness("Roughness", Range(0.0, 10.0)) = 1
_Albedo("Albedo (RGB)", 2D) = "white" {}
_Metallic("Metallic", Range(0.0, 10.0)) = 1
_GGX_V_Transition("GGX V Transition", Range(0.0001, 10.0)) = 0.0001
}
SubShader{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}
Pass
{
Tags{ "LightMode" = "ForwardBase"}//設置光照類型
Blend SrcAlpha OneMinusSrcAlpha //開啓顏色混合模式
CGPROGRAM
#pragma vertex vert //vextex着色器階段
#pragma fragment frag //fragment着色器階段
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
sampler2D _SpecTex;
sampler2D _Albedo;
fixed _Metallic;
fixed4 _SpecColor;
fixed _Roughness;
half _GGX_V_Transition;
//定義輸入頂點着色器階段的數據結構
struct Input
{
float4 vertex : POSITION; //頂點位置
float4 texcoord : TEXCOORD0; //紋理座標
float4 normal : NORMAL;
float2 uv : TEXCOORD1;
};
//定義頂點着色器階段輸出的數據結構
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 normalWorld : NORMAL;
float4 posWorld : TEXCOORD1;
};
//輸出v2f到下一渲染階段
v2f vert(Input v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float4 posWorld = mul(unity_ObjectToWorld, v.vertex);
float3 normalWorld = normalize(UnityObjectToWorldNormal(v.normal));
o.uv = v.uv;
o.normalWorld = normalWorld;
o.posWorld = posWorld;
return o;
}
half doubleNumber(half num)
{
return num * num;
}
half fiveNumber(half num)
{
return num * num * num * num * num;
}
half GGX(half dotX)
{
half k = doubleNumber(_Roughness + 1) / 8;
return dotX / (k + (1 - k) * dotX);
}
fixed4 frag(v2f i) :SV_TARGET
{
half3 lightDirWorld = normalize(_WorldSpaceLightPos0.xyz - i.posWorld.xyz);
half3 viewDirWorld = normalize(_WorldSpaceCameraPos - i.posWorld.xyz);
half3 halfDir = normalize(lightDirWorld + viewDirWorld);
half NdotL = saturate(dot(i.normalWorld, lightDirWorld));
half NdotH = saturate(dot(i.normalWorld, halfDir));
half NdotV = saturate(dot(i.normalWorld, viewDirWorld));
float4 mainTex = tex2D(_MainTex, i.uv) * _Color;
float4 specTex = tex2D(_SpecTex, i.uv) * _SpecColor;
float3 metalliTex = tex2D(_Albedo, i.uv).rgb;
half pi = 3.1415926;
half D = doubleNumber(_Roughness) / (pi * doubleNumber(doubleNumber(NdotH) * (doubleNumber(_Roughness) - 1) + 1));
half3 F0 = metalliTex * _Metallic + (1 - _Metallic) * half3(0.04, 0.04, 0.04);
half F = F0 + (1 - F0) * (1 - fiveNumber(NdotH));
half lambertNL = NdotL * 0.5 + 0.5;
half V = (GGX(NdotL) * GGX(NdotV)) / (4 * lambertNL * NdotV);
half kdiff = F0;
half kSpec = (1 - F0) * (1 - _Metallic);
float3 brdf = (kdiff * mainTex.rgb / pi + kSpec * V * F * D);
return fixed4(brdf.rgb, 1);
}
ENDCG
}
}
}
參考資料