【技術美術】光照技術概述
和現實一樣,即使是遊戲中的虛擬物體,我們也可以認為都是因為發光才可見,包括不含光照計算的特效,因為本質上他們是發出的自發光。所以説,任何物體的渲染,本質都是在做光的渲染。
光的屬性
- 可溯源的:光不是憑空產生的,一定是有源頭的。
- 可傳遞的:光是可以通過介質傳遞的,也正是因為傳遞會使光的能量發生變化,我們才能觀測到傳遞的介質。
- 可疊加的:光的能量可以直接簡單的相加來合成的,大部分情況下光都是由若干不同類型的光組合而成。
光照分類
按光的能量來源分類
- 直接光:從光源直接打到物體後反射到眼睛的光。
- 間接光:從其他物體反射到物體再反射到眼睛的光。
- 自發光:物體自身表面發出的光。
按光的傳遞方式分類
- 反射光:光碰到介質後返回原介質的光。
- 透射光:光碰到介質後穿過介質從另一端射出的光。
- 自發光:物體本身就是光源,導致光線直接進入眼睛的光。
按光的反射方式分類
- 漫反射光(簡稱漫射光):光在物體表面或內部多次彈射後再反射進眼睛的光。
- 鏡反射光(簡稱鏡射光):光在物體表面未經二次彈射,直接反射進眼睛的光。
如果一個函數同時具有這兩種光,則可稱其為“雙向反射分佈函數(BRDF)”。
按光的透射方式分類
- 漫透射光:光在穿過介質的過程中,經過了多次彈射。
- 鏡透射光:光在穿過介質的過程中,沒有經過額外彈射。
如果一個函數同時具有這兩種光,則可稱其為“雙向透射分佈函數(BTDF)”。
按光的反射位置分類
- 非次表面:入射光與出射光位置相同,如默認的漫射、鏡射光。
- 次表面:入射光和出射光位置不同,如次表面漫射。
光照物理
關於光有很多物理現象,人們對此總結出了各種經驗公式和解決方案。(此處暫只討論與光有關的物理現象,更多的是描述光的組成和物體間的關係,不涉及具體的光照反射計算)
首先我們需要獲取一些關照計算上所需的參數:
float3 n:法線方向(normal)float3 v:相機方向(viewDir)float3 l:燈光方向(lightDir)float3 albedo:反照率。物體反射光照的比例,更通俗名稱叫顏色。float metallic:金屬度。確定漫射和鏡射的權重分配。float smoothness:光滑度。用於形容物體表面形狀,對鏡射光影響很大。
此外在此基礎上還有些可預計算的常用參數:
float3 h = normalize(v + l);:半角向量。一種經驗參數,其混合了相機和燈光方向信息。float roughness = max(HALF_MIN_SQRT, pow(1 - smoothness, 2));:粗糙度。
注意:光照計算中存在光滑度和粗糙度兩種工作流,Unity使用的是光滑度,但DCC軟件一般用粗糙度,不過這兩者是可以相互轉換的。
微表面模型
從整體來看,物體表面各式各樣沒有規則。但利用積分的思想,我們可以想象這些表面都是有無數個相同的微小表面組合而成,於是我們便可以對各種物體的表面進行一套統一計算,一套針對微表面的計算。
輻射度量學
輻射度量學是一種物理學科,但我們不需要深入學習,僅僅瞭解其中的一些學術概念就行。
- 輻射能量:光源做工。
- 輻射功率:光源每單位時間做工。
- 輻射強度:光源從每單位立體角發出的輻射功率。
- 輻照度(Irradiance):光源照射到微表面的輻射強度。
- 輻射率(Radiance):光源照射到微表面後反射的輻射強度。
我們光照計算實際上只要考慮:獲取“輻照度”和計算“輻射率”就行,這個過程也不是一定非要物理的,只是單純用他們的名字和關係來表示一些計算中的光照參數。
其中輻射率是基於輻照度計算的,這兩者代表入射光每單位(微分立體角)的總光強和出射光每單位的總光強。可想而知,如果兩者面積總量一樣,根據能量守恆,兩者的值應該是相等的。但實際情況中,由於角度傾斜的原因,入射光的截面積和接收光的表面積是不一定對等的,所以反射時每單位面積的光強就會被均分。
具體而言兩者的關係與法線有關,其公式如下:
float radiance = saturate(dot(n,l)) * irradiance;
光照分佈函數
人們發現光從表現效果上,基本都可以被看成是由幾種簡單的光組合而成(見上文的光照分類)。因此根據具體實現的光照類型,光照函數被分為了一下 3 類:
- BRDF:雙向反射分佈函數(描述反射現象)
- BTDF:雙向透射分佈函數(描述透射現象)
- BSDF:雙向散射分佈函數(BRDF+BTDF)
基本上這些函數就是“反射”、“透射”和“漫射”、“鏡射”的排列組合:
- 漫射:低頻模糊,光線柔和。
- 鏡射:高頻清晰,能形成清晰光源畫面。
注意:這些公式僅僅是一種分類,給我們一個計算光照的骨架,所以其具體實現完全可以因人而異自由搭配。
- 【技術美術】雙向反射分佈函數
- 【技術美術】雙向透射分佈函數
介電質全反射
只要是介電質(可以簡單理解成所有物體吧),那就一定會發生輕微的全反射(即光線不被反照率影響,沒有經過任何吸收就全部反射),這在後續的鏡射率和非涅爾效應中也會有所體現,該最低全反射率是一個常量。
float dielectricSpec = 0.04;
能量守恆
如果要製作一個真實的光照,我們同樣要考慮到能量守恆(當然,非物理的風格化光照不需要,否則反而還不好看)。根據“雙向反射分佈”,我們將光拆分為了“漫射光”和“鏡射光”分開計算,因此也要注意兩者在光能量上的分配。
對此人們使用一種稱為“金屬度”的參數,來分配兩者的權重,越金屬其漫射光越弱,鏡射光越強,而這權重具體反映到的則是“反照率”的變化。
float3 diffuse = lerp(albedo * (1 - dielectricSpec), 0, metallic); //漫射反照率
float3 specular = lerp(dielectricSpec, albedo, metallic); //鏡射反照率
注意:雖然非物理光照一般不用遵照能量守恆,但其金屬度的概念卻已經深度人性,即使卡通渲染,也經常會利用金屬度來控制高光(鏡射光)的強弱。
菲涅爾效應
菲涅爾是一種物理現象,其反應了物體的反照率強會隨着物體折射率、法線、觀察角度的不同而發生變化。最顯著的例子是水面,垂直看水面時,清澈見底,平行看水面時則只能看到倒影而看不清水底了。
原始的非涅爾公式較為複雜還涉及折射率信息,難以使用,因為人們根據其實際表現形式,推出了一套經驗公式(越掠角反射率越強,甚至全反射)。此外非涅爾只會影響到鏡射光,故可簡單將其視作是對鏡射光的一道後處理。雖然漫射光也是一種反射光,但其是經過多次隨機反彈後反射的光,故可以簡單認為其丟失了法線、方向等信息,因此不會因為菲涅爾效應而發生變化。
float F = pow(1 - saturate(dot(n, v)), 4); //反射率增強係數(非涅爾係數)
在Unity中,非涅爾引起的反射率是有上限的,故不一定全反射,具體和物體的鏡射率和光滑度有關。
float reflectivity = lerp(dielectricSpec, 1, metallic) //鏡射率
float grazingTerm = saturate(reflectivity + smoothness);//掠角反射率
specular = lerp(specular, grazingTerm, F);
幾何遮蔽
幾何遮蔽用於表現微表面之間的遮擋導致的光線衰減現象,這在粗糙的表面尤為明顯。這同樣只對鏡射光生效(漫射光本身就是經歷過各種遮擋反彈返回的光,所以實際上已經考慮了幾何遮蔽),可視作是鏡射光的一道係數,其越大鏡射光越亮,越小鏡射光越小。
幾何遮蔽有多種近似公式,在Unity中對直接光計算使用的是 sksm 幾何遮蔽算法。
float G = saturate(dot(n, l) * dot(n, v)) / lerp(roughness, 1, pow(saturate(dot(l,h)), 2))
從該公式中可以觀察到:
- 分子分母的角度點乘次數都是平方且幾何意義相似,故可以粗略看成是相等的。
- lerp操作的終點是1,恰巧也是點乘的上限,所以可以猜出其數值會被限制在0-1。
- 當點乘結果不為1時(即光照視角傾斜於表面),粗糙度將作為分母影響結果大小(越粗糙分母越大,得到的結果越小),從而滿足了幾何遮蔽衰減光照的效果。
對於間接光,因為不用考慮角度問題,Unity則比較簡單,直接根據粗糙度衰減即可。
float G = 1 / (1 + pow(roughness,2));
入射光計算
計算反射光前,必須先獲取入射光信息。
直接光
注意:對於直接光來説,入射光等價於輻照度,因此進行實際反射計算前,需轉換為輻射率。
實時燈光
直接從原始光源獲取的燈光。Unity中將實時燈光分為“主燈光”和“附加燈光”兩類,“主燈光”是平行光,但平行光也可以成為“附加燈光”,“附加燈光”自身也有很多種類。不過這些燈光信息都已經被Unity封裝,成了一種可以統一處理的存在。
float3 positionWS:微表面所在的世界空間位置。
float shadowMask = 1; //陰影遮罩技術,用於實現超遠距離烘焙陰影,具體數值從哪來的,目前我也不知道
Light mainLight = GetMainLight(TransformWorldToShadowCoord(positionWS), positionWS, shadowMask);
for (int i = 0; i < GetAdditionalLightsCount(); i++)
Light additionalLight = GetAdditionalLight(i, positionWS, shadowMask);
Unity提供的光照信息中並沒有直接提供輻照度,需要利用光照信息計算。
Light light:從Unity中獲取的原始燈光信息(上文的計算結果)。
float3 irradiance = light.color * light.distanceAttenuation * light.shadowAttenuation;
間接光
間接光又叫環境光,是來自於環境中即四面八方的光。相比直接光,環境光是不可數的,因此無法直接實時計算,故環境光一般是基於一種叫“IBL”(基於圖像的照明)的烘焙燈光技術實現的(將HDR環境貼圖視作光源,均勻的隨機採樣若干次,再以實時燈光的方式計算累加)。
由於“雙向反射分佈”的存在,故環境光針對“漫射”、“鏡射”兩種光,也根據其特性,使用了兩種不同的烘焙方式:“光照探針”和“反射探針”。
注意:間接光由於是預計算的,導致其存儲的已經是計算好的輻射率,所以不需要額外的轉換。
光照探針
光照探針存儲的是漫射光信息。漫射光是粗糙的低頻燈光,因此不需要形成像鏡子那種清晰的反射畫面,依此特性,為了減少內存提高性能,可以將其用球諧函數(SH)存儲。
球諧函數類似於用泰勒級數解三角函數,通過有限的多項式就可以近似出超越函數的效果,進而使得三角函數可被實際用於計算,非常實用。同理球諧函數只需要存儲幾個多項式的參數,就可以實現輸入方向輸出該方向大概亮度的功能,內存佔用少計算也高效。
float3 irradiance = SampleSH(n);
反射探針
鏡射光是高頻的需要形成清晰鏡面的光照,因此不能使用SH,轉而使用環境貼圖存儲。此外鏡射光還與視角和材質粗糙度有關,其中視角是動態的因此無法烘焙,但粗糙度是固定的01範圍,因此可以針對不同的粗糙度多烘焙幾張。由於越粗糙頻率越低,貼圖分辨率需求越小,因此這多張不同粗糙度烘焙的貼圖可以利用mipmap存儲,藉此還能實現GPU自動插值紋理內容。
環境貼圖本質就是一張具有mipmap的立方體紋理,因此只要根據粗糙度和微表面法向,就能取出對應的鏡射光輻照度。
- UNITY_SPECCUBE_LOD_STEPS:unity中默認環境貼圖的最大mipmap等級。
- unity_SpecCube0:unity中的默認環境貼圖。
- samplerunity_SpecCube0:unity中默認環境貼圖的採樣器。
- unity_SpecCube0_HDR:unity中默認環境貼圖的編碼信息。
float pr = 1 - s; //直覺上的粗糙度(perceptualRoughness)
float mipLevel = pr * (1.7 - 0.7 * pr) * UNITY_SPECCUBE_LOD_STEPS;
float4 encodedIrradiance = unity_SpecCube0.SampleLevel(samplerunity_SpecCube0, reflect(-v, n), mipLevel);
float3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);//環境貼圖一般是HDR貼圖
自發光
光照貼圖
光照貼圖是通過預計算將最終的光照結果烘焙到紋理中,下次渲染時便可避免高昂的光照計算,直接使用。由於是已經計算好的,所以光照貼圖某種意義上已經不是光照了,直接看成自發光就好。
光照模型
光照模型是指用數學手段具體實現的完整光照計算方案,相當於是上述各種光照物理的實現和彙總,有了光照模型就可以實際的進行光照模擬計算了。
【技術美術】光照模型
PBR光照模型
PBR 光照模型是市面上最主流的光照模型,同時也是引擎默認的光照模型,出於兼容性的緣故,基本所有的其他光照模型都是以其為基礎進行修改的,因此 PBR 是必須要學會的一個光照模型。
PBR 模型的具體實現各式各樣,導致每家渲染引擎都有着不同的 PBR 效果,因為 PBR 本質還是一種經驗模型,只是其經驗來源更加嚴謹完整。不過無論是哪一家的 PBR,基本上都是上文那些各種光學現象的彙總。
提示:如果想更深入的瞭解 PBR 並實現 Unity 中的同款,可以參考“基於物理的渲染”文集。
特殊光照實現
因為計算機無法真實模擬現實物體的所有性質(例如不可能真的把一根一根的頭髮絲做出來),因此對一些特殊光照效果,需要進行專門的處理(通過調整光照計算函數,用經驗公式的方法近似模擬這些效果)。一些常用的特殊光照效果如下:
- 次表面散射
- 透射折射
- 各向異性光照
- 卡通風格光照
- 薄膜干涉