Wednesday, July 11, 2018

Development Updates



因為已經很久沒有新文章,今天就來更新一下開發進度吧。

六月將Switch繪圖底層移植的工作完成後,終於有時間回到自己的專案來工作。之前雖然已經在Unity上將 voxel系統做了一些嘗試,但一直都沒有時間將程式碼做一個整理以及最佳化,所以就趁著還沒辦法開始production的這段時間先來做一些前製的工作,將該準備好的技術都先整理完成。同時因為還是會用voxel的模型來製作遊戲,所以美術的製程跟一般傳統遊戲會有些不同,因此也需要將技術部分確定後建立起之後的美術製程。

之前其實我用了兩種方法在Unity上實作voxel系統,一種是用內建的particle system,但由voxel system控制每一個分子的移動以及位移,因為底層是Unity自家最佳化過的,所以效能較好,但是缺點就是彈性較小。另一種方法則是使用material instancing,這種做法就跟我之前引擎是類似的,優點就是彈性更大,但是在之前實作中效能跟particle system還是有點差距。所以這次的任務就是優化material instancing based voxel system,同時還要設計一個新的檔案格式符合新的voxel system的新功能。

分析了一下material instancing based voxel system,發現瓶頸是在每個update中的4x4矩陣相乘,不過這部分其實很單純,就是呼叫以下function

static void InstanceTransformUpdate(ref Matrix4x4 o, ref Matrix4x4 p, ref Matrix4x4 t){o = p * t;}


於是我試著將移除矩陣相乘的operator overloading,將function改成如下

static void InstanceTransformUpdate(ref Matrix4x4 o, ref Matrix4x4 p, ref Matrix4x4 t)
{
o.m00 = p.m00 * t.m00 + p.m01 * t.m10 + p.m02 * t.m20 + p.m03 * t.m30;
o.m01 = p.m00 * t.m01 + p.m01 * t.m11 + p.m02 * t.m21 + p.m03 * t.m31;
o.m02 = p.m00 * t.m02 + p.m01 * t.m12 + p.m02 * t.m22 + p.m03 * t.m32;
o.m03 = p.m00 * t.m03 + p.m01 * t.m13 + p.m02 * t.m23 + p.m03 * t.m33;
o.m10 = p.m10 * t.m00 + p.m11 * t.m10 + p.m12 * t.m20 + p.m13 * t.m30;
o.m11 = p.m10 * t.m01 + p.m11 * t.m11 + p.m12 * t.m21 + p.m13 * t.m31;
o.m12 = p.m10 * t.m02 + p.m11 * t.m12 + p.m12 * t.m22 + p.m13 * t.m32;
o.m13 = p.m10 * t.m03 + p.m11 * t.m13 + p.m12 * t.m23 + p.m13 * t.m33;
o.m20 = p.m20 * t.m00 + p.m21 * t.m10 + p.m22 * t.m20 + p.m23 * t.m30;
o.m21 = p.m20 * t.m01 + p.m21 * t.m11 + p.m22 * t.m21 + p.m23 * t.m31;
o.m22 = p.m20 * t.m02 + p.m21 * t.m12 + p.m22 * t.m22 + p.m23 * t.m32;
o.m23 = p.m20 * t.m03 + p.m21 * t.m13 + p.m22 * t.m23 + p.m23 * t.m33;
o.m30 = p.m30 * t.m00 + p.m31 * t.m10 + p.m32 * t.m20 + p.m33 * t.m30;
o.m31 = p.m30 * t.m01 + p.m31 * t.m11 + p.m32 * t.m21 + p.m33 * t.m31;
o.m32 = p.m30 * t.m02 + p.m31 * t.m12 + p.m32 * t.m22 + p.m33 * t.m32;
o.m33 = p.m30 * t.m03 + p.m31 * t.m13 + p.m32 * t.m23 + p.m33 * t.m33;
}
結果效能顯著的提升,從75 fps直接跳到120 fps! 起初我以為是C#的數學運算效能問題,後來有朋友傳來Unity的C# 原始碼,一切才水落石出,以下是Unity的矩陣相乘原始碼

// Multiplies two matrices.
public static Matrix4x4 operator*(Matrix4x4 lhs, Matrix4x4 rhs)
{
    Matrix4x4 res;
    res.m00 = lhs.m00 * rhs.m00 + lhs.m01 * rhs.m10 + lhs.m02 * rhs.m20 + lhs.m03 * rhs.m30;
    res.m01 = lhs.m00 * rhs.m01 + lhs.m01 * rhs.m11 + lhs.m02 * rhs.m21 + lhs.m03 * rhs.m31;
    res.m02 = lhs.m00 * rhs.m02 + lhs.m01 * rhs.m12 + lhs.m02 * rhs.m22 + lhs.m03 * rhs.m32;
    res.m03 = lhs.m00 * rhs.m03 + lhs.m01 * rhs.m13 + lhs.m02 * rhs.m23 + lhs.m03 * rhs.m33;

    res.m10 = lhs.m10 * rhs.m00 + lhs.m11 * rhs.m10 + lhs.m12 * rhs.m20 + lhs.m13 * rhs.m30;
    res.m11 = lhs.m10 * rhs.m01 + lhs.m11 * rhs.m11 + lhs.m12 * rhs.m21 + lhs.m13 * rhs.m31;
    res.m12 = lhs.m10 * rhs.m02 + lhs.m11 * rhs.m12 + lhs.m12 * rhs.m22 + lhs.m13 * rhs.m32;
    res.m13 = lhs.m10 * rhs.m03 + lhs.m11 * rhs.m13 + lhs.m12 * rhs.m23 + lhs.m13 * rhs.m33;

    res.m20 = lhs.m20 * rhs.m00 + lhs.m21 * rhs.m10 + lhs.m22 * rhs.m20 + lhs.m23 * rhs.m30;
    res.m21 = lhs.m20 * rhs.m01 + lhs.m21 * rhs.m11 + lhs.m22 * rhs.m21 + lhs.m23 * rhs.m31;
    res.m22 = lhs.m20 * rhs.m02 + lhs.m21 * rhs.m12 + lhs.m22 * rhs.m22 + lhs.m23 * rhs.m32;
    res.m23 = lhs.m20 * rhs.m03 + lhs.m21 * rhs.m13 + lhs.m22 * rhs.m23 + lhs.m23 * rhs.m33;

    res.m30 = lhs.m30 * rhs.m00 + lhs.m31 * rhs.m10 + lhs.m32 * rhs.m20 + lhs.m33 * rhs.m30;
    res.m31 = lhs.m30 * rhs.m01 + lhs.m31 * rhs.m11 + lhs.m32 * rhs.m21 + lhs.m33 * rhs.m31;
    res.m32 = lhs.m30 * rhs.m02 + lhs.m31 * rhs.m12 + lhs.m32 * rhs.m22 + lhs.m33 * rhs.m32;
    res.m33 = lhs.m30 * rhs.m03 + lhs.m31 * rhs.m13 + lhs.m32 * rhs.m23 + lhs.m33 * rhs.m33;

    return res;
}
原來問題是來自於pass by value,如果直接使用Matrix4x4的乘法operator,每一次的相乘就會new出三個暫時的Matrix4x4,而這就是效能衰退的原因,同時還可能會造成GC的問題。不過我比較不理解的是為何Unity不另外增加一個static的數學lib是可以直接 pass by reference,讓需要效能的地方可以有個選擇,之前我的引擎雖然是C++,但是為了避免多一個copy動作,我就有設計另一組static的數學lib來給需要效能的地方使用。

之後我還嘗試了將矩陣相乘用C++寫成plugin,甚至用組合語言改寫並加上SSE加速,但效能提升很有限,所以最後我還是決定使用C#的static math function就好。不過即便如此,提升後的效能還是跟particle system有段差距。我想到如果能將矩陣運算丟入GPU的話,應該能提升更多效能,於是修改了rendering的code以及shader,終於可以得到跟Particle System差不多的效能,在某些情況下還可以超越。將來如果有時間,我大概會想辦法把剩下的CPU運算都丟進compute shader中,不過以目前的專案來說這樣的效能應該已經足夠,所以我就先在這裡打住了。

這次還做了一個之前一直想做的新設計就是將voxel data跟rendering的code分離,藉由將這兩個類別decouple讓整個系統更有彈性同時也可以得到更好的效能。

回到遊戲專案上,其實目前專案的進度有點卡住,主要是因為還沒有找到合適的美術人選,所以只能先從技術部分下手,但是關於rendering的部分因為沒有確定風格也無法展開太多的研究。這次的專案是一個以voxel為基礎的動作遊戲,如果有對做voxel類型遊戲有興趣的美術高手我們可以聊聊喔 :)




Wednesday, March 21, 2018

Switch Hardware Experiment

現在的遊戲程式們大概很難想像,在遊戲引擎變成免費之前,想要將遊戲移植到新平台,如果沒錢買引擎,唯一的方法就是花時間啃文件,然後紮紮實實的從無到有將系統一步一步實作出來。原本以為今後應該是沒有機會再幹這種需要硬功夫的工作了,沒想到最近在因緣際會下,接到了一份將OGLES的繪圖引擎移植到Switch原生的繪圖API之上的工作,而又有了一次摸到新主機硬體的機會。



通常在移植既有引擎到新硬體上時,我都會先完成memory,file system以及OS相關部分的移植,然後才開始動繪圖部分。這次因為只需要移植繪圖部分,所以輕鬆了一些,不過取而代之的就是要先讀懂別人的程式碼並推測出整個系統架構。而繪圖部分通常我會從幾個步驟來逐步實作

  1. 初始化device
  2. 將device與OS原生context串接,這部分有時還挺麻煩的
  3. 建立frame buffers以及swap chain. 到了這一步,基本上就可以看到畫面了。在fixed function的硬體上,到這一步後其實就能draw primitive了。但是現代支援shader的硬體就比較麻煩了,還要很多步驟才能開始繪出東西。
  4. 實作vertex attribute layout以及vertex buffer
  5. 了解shader語言以及如何Compile and load shaders
  6. 實作貼圖的生成以及載入,到這一步才能開始測試draw primitive

理解硬體如何運作並實作其實並不難,看看文件以及範例其實就懂八九成了,真正麻煩的地方是要思考如何巧妙的把新的繪圖流程套到既有的系統架構上,讓系統可以跨平台執行,同時又不失效能,這部分就很需要經驗了,尤其是遇到架構大更新時(比如DX9進化到DX11)就更考驗功力了。這次Switch的原生繪圖API是類似Vulkan的架構,為的是減少driver的複雜程度以及負擔,而將大部分驗證以及command buffer、queue、記憶體等的管理工作都交由application來負責,藉此解決small batch的問題。也因為整個pipeline 完全不同了,要套用到原本的架構需要許多特別處理。

接下來兩個月大概都會忙這份工作而無法專心在新遊戲的開發上面,不過新遊戲的概念總算有些突破,接下來應該就可以花些時間來製作雛形了,不過恐怕就要犧牲些休息時間了。完成這份工作後,應該就可以投入更多的時間在新遊戲的開發上了吧。

Friday, February 02, 2018

New Chapter

2018對我來說可能會是很特別的一年,因為去年結束掉一些事後,今年可以說是重新開始。前一陣子終於有時間靜下來回想一下過去幾年的生活,卻發現自己越走越遠離原本回國時想要的生活型態。因此今年開始,我便試著調整生活型態,看看接下來能走到哪裡。

首先最重要的當然就是做自己的遊戲了,不過光做自己遊戲沒有收入也是不行,再加上除了自己的遊戲也想做做其他有趣的東西同時多認識一些人,因此將目標訂在用40%時間做一些比較有趣或有技術挑戰性的工作來賺點奶粉錢,剩下60%的時間則用來做自己的遊戲。

所以今年一月開始參與一個有趣的專案,也因此開始更加深入的接觸了UE4。之前其實有開始試著將voxel的東西移植到UE4上,但因為種種原因做到一半就沒有繼續了,但是這次的專案卻得直搗UE4核心,改寫許多跟deferred shading相關的code跟 shader。這一陣子幾乎都是在trace UE4的原始碼,讓我回想起之前在AMD trace UE3原始碼的日子,不過那時修改的是global shader,相對起來不用對引擎架構了解太深入,只要了解RHI的架構就可以解決。這次因為要動到整個rendering pipeline,因此要對UE4整個架構有更深入的挖掘才有辦法植入一些新的shading模式。等專案結束後有時間我再來說說UE4的修改經驗。

而三月開始則要參與一個非常奇妙的專案,要直接跟某個console的繪圖底層對戰,幫新console硬體寫code是我覺得最有趣的工作之一了,所以我個人還蠻期待這個專案的。不過目前還不能透漏太多,等完成專案之後我再來分享一些經驗吧。

接下來則是自己的遊戲了,考量到時間以及人力,這次就決定使用Unity而不用自己引擎了。雖然改用了Unity,但我還是想繼續做voxel的東西,所以花了一些時間把voxel相關功能想辦法加入Unity之中了。因為Unity 5之後支援了hardware instancing功能,所以這一次我實際上implement了兩種不同的voxel renderer,一種是使用particle system,另一種則是使用了hardware instancing。而兩種voxel renderer也可以隨時更換。兩種voxel renderer的效能其實在不同狀況下各有優劣,可以視狀況選用。

有了voxel renderer後,一些voxel的特效也就能輕易地加入了,這次我還增加了對Magic Voxel檔案格式的直接支援,所以.vox檔可以直接拉入Unity中使用,這樣美術就可以直接用Magic Voxel來製作模型了。而為了增加同一個畫面中voxel object的數量,這次還使用了greedy mesh來做最佳化,同時還能自動產生LOD。


至於遊戲的內容,其實都還在構思中,也還有很多問題尚未想出很好的解決方案,所以其實進度是有點卡住了..... 只好多玩玩電動看看能不能找到解答的靈感了 XD