從零開始手敲次世代遊戲引擎(五十三)

從零開始手敲次世代遊戲引擎(五十三)

來自專欄 高品質遊戲開發

本篇我們來實現一個較為複雜的光源——帶有按距離和角度能量衰減的射燈。最終效果如題圖,動畫場景效果如下:

https://www.zhihu.com/video/972529574927691776

我們首先在Blender當中創建這個場景。場景的基本創建過程參考從零開始手敲次世代遊戲引擎(四十七)當中的視頻,所不同的在於需要將光源類型從預設的點光源改為射燈。

https://www.zhihu.com/video/974274849593659392

然後我們用Blender的OGEX插件導出場景(參見從零開始手敲次世代遊戲引擎(三十二))。用任意文本編輯器打開導出的OGEX文件,觀察其中的光源導出格式:

LightObject $light1 (type = "spot") // Lamp{ Color (attrib = "light") {float[3] {{1.0, 1.0, 1.0}}} Param (attrib = "intensity") {float {10.0}} Atten (curve = "inverse_square") { Param (attrib = "scale") {float {3.9749193675182246}} } Atten (kind = "angle", curve = "linear") { Param (attrib = "begin") {float {0.334917597999862}} Param (attrib = "end") {float {0.5427974462509155}} }}

可以看到與之前的點光源相比,射燈多了一個按照角度進行光能衰減的Atten。其實可以這麼理解射燈:射燈就是一個只在某個角度範圍內發射光線的點光源。因此作為最為簡單的射燈,我們只需要在片元著色器(fragment shader)當中檢查當前像素所對應的場景物體上的點到光源的連線與光源方向的夾角是否落在這個角度之內。如果在這個角度之內這將該光源的貢獻計算進去,否則忽略此光源。下面的代碼展示了這個夾角的一種計算方法:

vec3 light_dir = normalize((viewMatrix * worldMatrix * vec4(lightDirection, 0.0f)).xyz); float lightToSurfAngle = acos(dot(L, light_dir));

然後我們可以將這個角度與光源的光錐頂角進行比較(注意由於我們是計算的與光源中心方向的夾角,所以其實是和光錐頂角的一半進行比較),如果在光錐內,則不對光源強度進行衰減;如果不在光錐內,則將光源強度衰減為0:

float atten; if (lightToSurfAngle < 3.14159f / 6.0f) { atten = 1.0f; } else { atten = 0.0f; }

上面的例子展現了一個60度頂角光錐時的情況。渲染的效果如下:

可以看到這樣的方法所產生的光錐在其邊界產生了極為銳利的明暗界限,並且在光錐區域內的光亮度保持了一個恆定值。這樣的光源是非常理想化的,與真實的射燈所產生的照射效果有著很大的乖離,所以畫面看上去很不真實。

事實上,光能在傳播過程當中,會以距離的平方倍進行能量的衰減。在我們這個場景當中,因為射燈並不是垂直於地面進行照射的,而是稍微有些傾斜。這樣的傾斜除了造成上圖可見的在地面的投影不是正圓之外,其實也會造成光錐內各個點照度的不同。距離光源較近的地方(畫面當中光錐在地面投影的右上部分)應該比距離光源較遠的地方(畫面當中光錐在地面投影的左下部分)更亮。如果我們仔細看我們導出的OGEX文件,我們可以發現事實上光源包括了兩個衰減函數(Atten),其中第一個沒有指定kind屬性。通過閱讀OGEX規格說明書我們可以知道,當Atten沒有指定kind的時候,kind為其預設值「distance」,也就是按照距離衰減的意思。

這就是說一個射燈包括了兩個方面的光能衰減:按照到光源距離的光能衰減;以及按照與光源中心方向夾角的光能衰減。

如上面已經提到的,物理學上光能按照距離的平方進行衰減。然而正如我們在前面很多篇當中所提到的,遊戲產業是一種文化產業,遊戲製作是一種內容創作,而遊戲引擎是一種建立在嚴謹的計算機技術基礎上的自由的藝術創作工具。所以,為了讓藝術家們更為方便地控制光照效果,在大多數DCC工具當中,光源的光能衰減函數是可以自由調整的,並不一定符合實際的物理學。而遊戲引擎當中需要大量使用藝術家使用DCC所製作的內容,因此也需要支持類似的功能。OGEX規格當中定義了4種常用的光能衰減函數:

  1. 線性衰減。通過指定一個開始值和一個終止值,在開始值到終止值區間內光能按照線性關係從最強衰減到最弱(0);
  2. 平滑衰減。在線性衰減的基礎上,增加一個額外的平滑計算公式, 3a^{2}-2a^{3} ,使得在開始衰減的地方以一個較小的衰減梯度開始平滑衰減,避免形成視覺上的斷裂感;
  3. 反比衰減。即與到光源的距離成反比進行衰減。為了方便藝術家進行控制,引入了縮放係數s、偏移o以及多項式一次項和常數項的係數 k_l,k_c

    a(t)=satleft( frac{s}{k_lt+k_cs} + o 
ight)

    縮放係數 s 相當於對光源強度進行了一個縮放;偏移 o 相當於將光照度的上下限進行了一個平移; k_l 相當於對到光源的距離進行了一個縮放; k_c 則相當於給定了一個到光源的起始距離。
  4. 平方反比衰減。即與到光源的距離的平方成反比進行衰減。這是最接近物理學上正確答案的衰減方式,但是依然為了便於創作人員控制,加入了一些控制參數:

    a(t)=satleft( frac{s^2}{k_qt^2+k_1st+k_cs^2} +o
ight)

    這些參數的意義與反比衰減基本相同,除了增加了一個距離多項式二次項的係數( k_q

由於被渲染場景當中每一個像素所對應的場景位置到光源的距離都可能是不同的,因此我們需要在片元著色器(fragment shader)當中來實現這些計算。同時,我們需要在每個Draw Call當中通過Constant Buffer將當前每個光源適用的衰減公式告訴Shader。

下面的GLSL Shader的代碼片段展示了上述4種光衰函數的計算實現:

float linear_interpolate(float t, float begin, float end){ if (t < begin) { return 1.0f; } else if (t > end) { return 0.0f; } else { return (end - t) / (end - begin); }}float apply_atten_curve(float dist, int atten_type, float atten_params[5]){ float atten = 1.0f; switch(atten_type) { case 0: // linear { float begin_atten = atten_params[0]; float end_atten = atten_params[1]; atten = linear_interpolate(dist, begin_atten, end_atten); break; } case 1: // smooth { float begin_atten = atten_params[0]; float end_atten = atten_params[1]; float tmp = linear_interpolate(dist, begin_atten, end_atten); atten = 3.0f * pow(tmp, 2.0f) - 2.0f * pow(tmp, 3.0f); break; } case 2: // inverse { float scale = atten_params[0]; float offset = atten_params[1]; float kl = atten_params[2]; float kc = atten_params[3]; atten = clamp(scale / (kl * dist + kc * scale) + offset, 0.0f, 1.0f); break; } case 3: // inverse square { float scale = atten_params[0]; float offset = atten_params[1]; float kq = atten_params[2]; float kl = atten_params[3]; float kc = atten_params[4]; atten = clamp(pow(scale, 2.0f) / (kq * pow(dist, 2.0f) + kl * dist * scale + kc * pow(scale, 2.0f) + offset), 0.0f, 1.0f); break; } } return atten;}

有了這些基礎和準備工作,我們就可以根據到光源的距離來確定光衰係數了:

// distance attenuation atten *= apply_atten_curve(lightToSurfDist, lightDistAttenCurveType, lightDistAttenCurveParams);

適用了按照距離的光衰函數之後的渲染效果如下,可以看到右上方要明顯亮於左下方,所形成的最終圖像也更加符合人的生活經驗:

接下來,我們可以看到OGEX所導出的kind="angle"的Atten當中,也指定了一個衰減公式。這是要模擬現實當中的射燈在其光錐的邊緣所產生的照度變化效果的。基本原理與上面所討論的按照距離衰減的函數是完全一樣的,只不過這裡函數的自變數從到光源的距離變為了與光源方向的夾角而已。修改我們的fragment shader,在距離光衰計算之後疊加這個按照角度的光衰:

// angle attenuation float atten = apply_atten_curve(lightToSurfAngle, lightAngleAttenCurveType, lightAngleAttenCurveParams);

執行效果如下:

可以看到在地上形成的光斑除了右上方比左下方亮之外,在光錐的明暗界線處出現了一個逐漸過渡的區域。這使得整個照明效果看起來更加真實。但是,在過渡區開始的地方由於梯度不連續(因為我們用的是linear方式,所以一旦進入衰減區域光能立即按照固定梯度進行衰減,導致在衰減區域開始位置導數不連續照成視覺上的不連續感)

我們可以手工修改OGEX文件,將角度衰減模式從linear改為smooth,這次得到的效果如下:

可以看到相對於上面的線性衰減,這次的衰減起步比較平穩,呈現一個梯度逐漸增大的衰減過程,效果看起來更為真實一些。


推薦閱讀:

《Real Time Rendering》之Vertex Blending/Skinning
一起來寫Unity渲染管線吧!零 開篇
深入淺出基於物理的渲染一

TAG:遊戲引擎 | 實時渲染 | OpenGL |