標籤:

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

既然我們已經可以從Blender當中導入模型、光照和攝像機信息,那麼我們可以開始嘗試導入更多更複雜的模型/場景來測試我們的代碼,發現並解決問題。

我是一個碼農,雖然學過一點點美術,但是還是沒能有啥大的發展。所以我只能依靠網上的資源了。網上有很多同樣使用CC協議共享的好的Asset。比如Blend Swap這個網站。很多碼農都是蘿莉控,所以我們嘗試導入ermmus製作的小蘿莉(AILI)一隻。感謝??

首先請注意一下這個Asset的授權信息。是CC-BY,和我們這個系列文章是一樣的。所以我在本文開始按照這個授權的要求,提及了製作者,並加入了到原頁面的鏈接。任何原創性活動都是應該得到充分尊重的,因為只有這樣,我們才能分享到更多的更精彩的內容。

點擊Download下載下來一個ZIP包,解壓之後,雙擊其中的aili_cycle.blend。這會自動啟動Blender(前提是你已經安裝了它。安裝鏈接在上一篇文章裡面有)

按下F12,就可以進行渲染出圖了。但是你會很快注意到,裙子和眼睛等都是紫紅色的。

紫紅色在3D圖形渲染領域好像是一個不成文的約定,表示shader的缺失或者不正常。具體到這個例子,如果我們查看裙子的材質,會發現貼圖不見了。查看解壓後的目錄,可以看到雖然有一個名為tex的目錄,但是裡面是空的;貼圖都在項目的根目錄下,而不是tex裡面。

嘗試著將所有貼圖移動到tex目錄之下,然後重新打開項目,發現問題解決了。按下F12進行渲染。這個渲染是採用的路徑跟蹤(Path Tracing)的方式,也就是嚴格按照光學方程進行的離散採樣(或者說蒙特卡羅數值求解)過程,要求反覆迭代直到收束到一定誤差範圍之內,計算量是很大的(相對的,效果也是驚艷的)。在我的mac pro上面,渲染出圖大約需要40分鐘的時間(插上電源的情況。用電池會更長)

我也嘗試了使用台式機的GTX1080顯卡進行計算渲染,速度方面並沒有很大的提升。這其實就是為什麼目前在遊戲當中還不能大範圍採用路徑跟蹤的渲染方式的原因之一。路徑跟蹤演算法是一個非常典型的基於CPU的演算法,在GPU上執行的效率並不高。我所理解的主要原因,一個是因為其複雜的程序分支結構,另外一個是因為其需要雙精度(double)的計算精度。一般來說,GPU適合的是直統統的,大量並列重複的,要求精度低的情景。因為GPU相對於CPU來說,其實是在差不多的半導體材料上面,在總晶體管數量一定的前提下,採用「多計算核心」但是「少分支預測」的策略進行設計的。而CPU正好相反。(當然,近年N卡有專門用於計算的雙精度專業卡,谷歌也搞了基於FPGA的專用計算ASIC,這個是另外一個事情了)

好了,寫完上面這些文字,圖也渲染好了。最終效果如下:

AILI: Model by ermmus Creative Commons Attribution 3.0 CC-BY

是不是很不錯呢?

不過到這裡為止我們並沒有做什麼。接下來才是正文。

如果我們直接如同文章三十二那樣將這個場景導出為OGEX,然後用我們寫的引擎進行渲染的話,會發現效果是這樣的:

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

小女孩晒黑了,胸(??)沒有了,頭髮沒有了,裙子是透的;天空???浮著不明的黑板子。

回到Blender,我們首先來看看場景的構成:

首先可以注意到場景並沒有光源。但是沒有光源,為什麼在Blender裡面可以渲染出那麼漂亮的圖呢?其實答案就在天空當中的那些板子上。選取Plane,查看它的屬性:

看到這個白色的圓了么?這個叫材質球,是用來顯示被選中的物體的材質的。注意看球下面的地板,是不是被淺藍色照亮了呢?再往下看,可以看到Surface被設置為Emission,Color恰恰是淺藍色。所以,對,這個板子就是光源。

查看另外兩個板子,也是類似的設置。

那麼為什麼在我們的場景當中幾乎是黑的呢?我們再來看看實際導出的OGEX文件。用vim打開我們導出的aili.ogex文件,冒頭3個Node便是這三個板子。

GeometryNode $node1n{ n Name {string {"Plane.002"}}n ObjectRef {ref {$geometry1}} n MaterialRef (index = 0) {ref {$material1}}n n Transformn { n float[16]n { n {35.663089752197266, 2.4165026245623984e-15, -5.694919294712934e-15, 0.0,n 2.416503048078872e-15, 24.780128479003906, 25.64763641357422, 0.0,n 5.694919294712934e-15, -25.64763641357422, 24.780128479003906, 0.0,n -1.4071846008300781, -45.10104751586914, 48.62202072143555, 1.0}n }n }n}nnGeometryNode $node2n{n Name {string {"Plane.001"}}n ObjectRef {ref {$geometry2}}n MaterialRef (index = 0) {ref {$material2}}nn Transformn {n float[16]n {n {-21.7617244720459, 8.345382690429688, 0.20872513949871063, 0.0,n 2.053757905960083, 5.621259689331055, -10.6276216506958, 0.0,n 6.464678764343262, 16.606595993041992, 10.032995223999023, 0.0,n 8.224732398986816, 22.414443969726562, 41.55237579345703, 1.0}n }n }n}nnGeometryNode $node3n{n Name {string {"Plane"}}n ObjectRef {ref {$geometry3}}n MaterialRef (index = 0) {ref {$material3}}n

注意它們的類型:GeometryNode。也就是說,他們是被作為幾何體導出的,而不是作為光源。

在Blender的Path Tracing當中,進行的是全局光照計算。所謂全局光照計算,指不僅僅考慮了直接來自光源的光線,還考慮了來自其他非光源物體的光。這主要包括兩個方面:

  1. 反射/折射光線
  2. 自發光物體

而在我們的引擎當中(更為準確地說是我們寫的basic Shader當中),目前我們只考慮了「光源類型」的光源,而沒有考慮來自其他非光源物體的光線,所以在這個特別的場景當中,對我們的引擎來說,就是沒有光源。(但是渲染結果當中仍然能夠看到有一點光,這是因為我們的引擎預設補了一個點光源)

至於天空當中的板子出現在畫面當中,這是因為我們旋轉了畫面。Blender裡面是固定視角,板子都在視野之外。我們可以通過關閉板子的渲染開關,使其在導出的OGEX當中的Visibility屬性為FALSE。並且修改我們的RHI/OpenGL/OpenGLGraphicsManager.cpp,讓它忽略Visibility為FALSE的對象來從渲染結果當中去除掉它。(注意這也會導致Blender裡面的渲染變暗)

點擊Plane右側的照相機圖標使其不參與渲染

由於自發光板子屬於面光源。面光源的照射計算比較複雜,我們在後面進行介紹。(但是它的確很好看,因為會產生很柔和的光場和陰影。一般照相館或者拍電視電影的時候,都會在人物邊上支很多白板子,就是這個道理)

接下來我們來看頭髮。我們可以注意到,在Blender當中,代表頭髮的節點的左側的圖標是不太一樣的:

在Blender當中,三角形圖標表示Mesh,而弧線表示這是一個Path。所謂Path,就是一條線段(或者曲線)。它本身是沒有體積的。沒有體積,那麼在渲染結果當中體現不出來,是很正常的。

但是為什麼在Blender裡面就能看到呢?

頭髮在Blender當中是有體積的

這其實是因為Blender在顯示/渲染的時候,對Path進行了實體化計算。但是其內部實際的數據結構,仍然只是一個Path。我們如果將當前的編輯模式從Object Mode改成Edit Mode,我們就可以看到這些頭髮的真實面目:

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

導出到OGEX當中的,正是這些黃綠色/橙色的折線。因此我們在渲染結果當中看不到它們。

解決的方法有兩種:

  1. 在我們的引擎當中實現這些Path的實體化計算,就如同Blender那樣;
  2. 讓Blender將實體化計算的結果保存下來。

這兩種方法都是可以實現的。但是對於遊戲引擎來說,一般希望盡量輕量(因為我們是軟實時系統)。所以一般能在DDC工具當中解決的工作,盡量不要放到引擎當中。除非有別的理由(比如需要做頭髮飄動的動畫。顯然移動一根線段比移動一個mesh實體要方便)

這裡我們先介紹方法2。用滑鼠右鍵選中頭髮,然後按下Alt+C,會出現一個彈出式菜單:

在其中選擇Mesh from Curve/Meta/Surf/Text之後,就可以將對象以Mesh的形式固定下來。對所有的頭髮進行這個操作(注意只需要處理名稱為hair_*的模型。其它的幾個放在頭部右側空中的是用來生成頭髮的放樣曲線,不需要變換)。然後重新導出OGEX。這樣我們的渲染結果就變成了這個樣子:

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

可以看到頭髮的顯示已經正常了。但是裙子依然不正常。上半身的裙子完全沒有,下半身的裙子是透的。

首先來看下半身裙子的問題。之前也提到過,在3D渲染當中,為了減少無謂的渲染量,會根據表面的方向進行裁剪。即:凡是背對相機的Mesh,都不會進行渲染。

判斷一個表面是面對相機還是背對相機的方法是,看這個表面的法線是指向相機的還是指向相反的方向。然而,不見得所有的模型都定義了法線參數,所以實際上GPU是根據多邊形(三角形)的頂點順序來判斷表面的朝向的。

當三角形的頂點坐標被變化到攝像機坐標系當中之後,如果頂點出現的順序是按照逆時針(當攝像機坐標係為右手坐標系)/順時針(當攝像機坐標係為左手坐標系),那麼表面就是面朝攝像機的。反之,則是背離攝像機的。

仔細觀察小女孩的裙擺,可以看到我們能看到的是裙子的內表面,看不到的是裙子的外表面。這就說明目前法線是反的。

但是為什麼在Blender裡面看起來沒有問題呢?我們還是來看這條裙子的屬性。右鍵選中裙子,然後選擇屬性面板當中的Data活頁:

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

我們可以看到有個Double Sided的屬性是處於被勾選的狀態。這就是原因了。這個屬性目前並沒有存在於導出的OGEX當中,我們的引擎也沒有對應這種雙面的材質(事實上,我們的引擎目前還沒有對應材質)。

這裡讓我們首先來修正法線。嗯,能用圖說明的不廢話:

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

好了,這樣就修正好了。再次導出OGEX,看看效果:

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

的確修好了。但是上半身仍然是那樣。

上半身是不是也是法線問題呢?答案是否定的。因為上半身完全是透的,既看不到正面,也看不到反面。

是不是上半身的數據沒有被成功導出呢?我們來看看上半身節點的名字:

「skirt_b」。在我們導出的OGEX當中,通過vim的查找命令「/skirt_b」(如果你用別的圖形界面編輯器,一般是Ctrl+F)查找,發現是存在的:

GeometryNode $node19n{n Name {string {"skirt_b"}}n ObjectRef {ref {$geometry15}}n MaterialRef (index = 0) {ref {$material9}}n MaterialRef (index = 1) {ref {$material7}}nn Transformn {n float[16]n {n {1.0587974786758423, 0.0, 0.0, 0.0,n 0.0, -1.7250000894364348e-07, 1.0587974786758423, 0.0,n 0.0, -1.0587974786758423, -1.7250000894364348e-07, 0.0,n 0.0, 0.0, -0.5038204193115234, 1.0}n }n }n}n

進一步,該節點引用的場景對象「geometry15」,也是存在的:

那麼問題在哪裡呢?

作為碼農,解決問題不能靠猜。我們需要有洞察力。讓我們盯著小姑娘看上3小時。。。

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

看哪裡?看胸。。。當然不是,看衣領。嗯。衣領!?

回到Blender,我們可以確認,衣領確實是上裙的一部分:

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

再回來看我們渲染的結果。如果看得足夠仔細,我們會發現其實胸前的領結,以及腰部前方的系帶,也都是有的:

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

再對比我們用Blender渲染的結果,應該不難發現有的都是白色的地方,沒有的都是黑色的地方。

我們可以通過將Blender的3D視圖顯示模式從「Solid」調整為「Material」來確認:

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

進一步確認的話,我們可以觀察到上裙其實引用了兩種材質:

所以呢?其實我們需要回到從零開始手敲次世代遊戲引擎(二十八):

事實上,對於複雜的模型,其往往也是包含了到不同材質的引用。比如一個人物的模型,其裸露的臉部和身上著衣的部分的材質就很可能是不同的。況且考慮到動畫的需要,我們需要將模型分割為可動的部分和不可動的部分。我們在用maya或者3dmax等DCC工具進行建模的時候,往往會將頂點進行分組,指定不同的材質或者子材質,這些都是很自然很好的分割依據。

我們再仔細看看導出的OGEX文件,在代表上裙的geometry15當中:

有兩個IndexArray!

而我們目前的代碼是(RHI/OpenGL/OpenGLGraphicsManager.cp):

注目這一行:

const SceneObjectIndexArray& index_array = pMesh->GetIndexArray(0);n

知道問題在哪裡了吧。我們現在是寫死的,每個Mesh只渲染第一個頂點數組。

我在now the aili mesh can be properly rendered · netwarm007/GameEngineFromScratch@8a1a44f 當中修正了這個問題。同時也修正了預設的光源在模型身後的問題。最終的效果如下:

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

另外,在這個過程當中我發現Windows版本在渲染複雜模型的時候會Crash。通過兩天的調試,發現這是因為我們的SceneObject當中對於頂點Buffer尺寸的計算不正確,導致將頂點數據從主內存拷貝到GPU顯存的時候訪問了實際上並沒有初始化的內存導致的。這個問題在macOS版以及Linux版上以極低的概率出現,而Window上100%出現。進一步調查發現上因為我們現在編譯的是Debug版,用Visual Studio編譯的Debug版的Windows程序會對分配的內存前後未使用的空間填充cdcdcd這樣的調試內容,當這個內容被解釋為指針地址的時候,就會出現保護錯誤。而macOS和Linux不會進行這樣的填充,所以,依據執行時內存上殘留的數據情況,可能crash可能不crash。

(如果記得我們之前寫的內存管理模塊的話,在Debug模式的時候,我們也是會對未分配的空間進行這樣的填充的。只不過目前我們還沒有對接內存管理模塊。這也從一方面證明了在跨平台開發當中寫自己的內存管理模塊的好處)

#P.S.

啊,忘記了一個比較重要的事情。我們之前場景文件的路徑都是Hard Coding到main.cpp裡面的。現在我們場景資源比較多了,這顯然不是什麼好事情,所以從這篇開始我們的代碼是通過啟動命令行參數載入場景文件的。比如要載入aili,需要這樣運行(場景文件根目錄固定在Asset目錄下,所以文件路徑是相對於Asset目錄的相對目錄):

./build/Platform/Darwin/MyGameEngineCocoaOpenGL.app/Contents/MacOS/MyGameEngineCocoaOpenGL Scene/aili.ogexn

參考引用:

  1. Aili | Blend Swap
  2. How to Flip normals in Blender-

推薦閱讀:

Simcity這個遊戲的宗旨是什麼,開發這個遊戲的人是不是城市規劃相關人員?
基於 Unity 引擎的遊戲開發進階之 敵人AI
從零開始手敲次世代遊戲引擎(三十二)
從零開始手敲次世代遊戲引擎(七)

TAG:游戏开发 |