2017年2月15日 星期三

GGJ遊戲開發經驗分享 - Splash Clash

作者:Ciro Continisio
潤稿:Ted


本文由參與GGJ 2017的遊戲開發者Ciro Continisio為大家分享他與美術設計師Jana Kilianová組成的兩人團隊,在48小時內開發出叫做Splash Clash的遊戲過程與經驗。

Global Game Jam 2017已於1月22日落幕。全球共有三萬多位遊戲開發者在700多個不同的城市參與了為期三天的遊戲創作活動,圍繞著共同的主題"Waves"製作了7000款遊戲。

Splash Clash就是其中一款優秀的遊戲。這款可以輕鬆玩的雙人對戰遊戲是由兩個像素風格角色透過跳躍製造波浪,並借助波浪將對手推出平台。下面我們從波浪效果、物理、場景設置和粒子效果這幾個方面一起來看看開發者是如何在 48 小時內將它開發出來的。


波浪效果


在最初,我們希望實現較為酷炫且逼真的波浪效果,所以不想用像是圓環或是粒子去擴散3D波浪物件。最終的方案是在位移著色器(Displacement Shader)中使用一張圓環貼圖,這樣就可以從平面(Plane)產生波浪,對這個圓的貼圖進行縮放,就可以實現動態的波浪。需要說明的一件事:本文分享的是如何在GGJ中快速實現這樣的效果,並非介紹實現該效果的最佳方案。可能還會有性能更好的解決方案。

初步模擬

在開始製作前,我們先在 Photoshop 中繪製了圓環貼圖並利用網路所找到的角色,例如下圖的蝙蝠俠,進行了初步模擬。


我們在Unity Manual中找到了簡單易用的位移著色器(Displacement Shader),它還支持曲面細分(Tessellation),這樣就不用擔心幾何問題了。將該著色器附加於材質後效果如下:


如上圖所示,Tessellation數值已被設為最大值,它用於控制表面被細分的次數。下面的Displacement滑動條則用於控制波浪的高度。

可行性分析

初步模擬過後,首先要確認該想法的可行性。因此,我們創建了一個白色並含有透明通道的圓環貼圖,並通過函數將其固定在不透明貼圖上。將其作為位移貼圖後的效果如下:

粗糙的開始

接下來,我擴充了函數功能,以允許更多圓環的情況。

複數靜止圓環

我們必須合成多個透明的圓環來實現表面上移動的多個波浪。如上圖所示,目前這些圓環貼圖的解析度相當高,基本都是256x256的。

由於波浪之間是相互獨立的,所以每幀必須刪除整張貼圖後再重新計算縮放的圓環。動態效果如下:

臨時彩色貼圖的隨機動態波浪效果


然而此時不得不大幅度減小貼圖大小了,因為之前的方法完全未經優化:一張512x512的凹凸貼圖(Bump)表示每個波浪都有262144的像素讀寫操作,而且256x256的波浪貼圖是隨時間縮放的,每幀都需要重寫整個貼圖。採用這種做法,屏幕上只有4個波浪時,每幀就會產生非常恐怖的1048576次像素操作

所以我們先縮小了所有貼圖尺寸,將圓環貼圖縮至32x32,凹凸貼圖為64x64。經過少許測試後遇到了個新問題:小貼圖在縮小後再次放大會導致貼圖變形,不再是圓環。

這個問題的解決方案是暫存原始圓環貼圖,每幀都使用原始貼圖重新生成縮放後的圓環,而非一直使用同一張貼圖,因為多次縮放會導致變形。也就是說每個波浪都有一個原始貼圖的引用,但實際並未使用原始貼圖,而是僅複製其像素到重新縮放的貼圖上

可以這種方案在平面上效果正常,但如果將材質應用到圓盤上,就會出現圓盤邊緣擠出邊界的異常效果:

圓盤左邊邊緣超出邊界導致出現鏤空

於是我們試著對著色器做些修改,不再沿著頂點法線進行擴散,改為只向上擴散。將以下程式:

v.vertex.xyz += v.normal * d;

改為:

v.vertex.xyz += float3(0,1,0)* d;

這樣就可以獲得正常的波浪效果了!

波浪沖撞完成,但波浪效果還不太明顯

實現波浪衝撞後,就要考慮彩色貼圖了。如上面動畫所示,波浪雖然有一道來自於方向光的陰影以便突出顯示,但這還遠遠不夠,還需要顯示得更為為明顯,讓玩家可以更好地用於判斷跳躍的時機。

我們起初還是使用同樣的方法,將白色的圓環貼圖放在彩色圓盤貼圖上方,與凹凸/位移貼圖的位置完全一致:


從上方的動畫中可以清楚地看到白色圓環如何顯示低解析度的貼圖,尤其是當圓環很大時。

此時我們決定調整著色器,在波浪上方就繪製白色像素。畢竟這樣每幀只需要讀取一半像素,運行效率會更快。這就需要更改surf函數中,對應計算像素顏色的程式,如下所示:

half4 c = tex2D(_MainTex, IN.uv_MainTex)* _Color *(30 * IN.worldPos.y + 1);

藉由使用IN.worldPos.y可以確保考慮到像素在世界空間的高度。像素越高,則亮度越強,也越接近於白色。效果如下:


至此,波浪特效製作完畢。

物理


圖形問題解決後,我們下面來實現角色被波浪推開的物理效果。這個過程不太複雜,只需要每個波浪作為帶有球形碰撞器(Sphere Collider)的遊戲對象,並將碰撞設定為觸發器即可。由於貼圖是縮放的,碰撞器也會隨之縮放。

將碰撞器進行縮放以搭配位移貼圖中的波浪

一旦碰撞器與某個角色發生碰撞(OnTriggerEnter),就檢測角色的Y坐標是否低於某個特定的臨界值,該臨界值與波浪強度相關,是隨時間減小的浮點數。如果是低於這個特定的臨界值,則角色被Rigidbody上添加的力推開,這個力的大小也同樣與波浪強度成比例關係。

我們使用標籤(Tag)對碰撞器進行過濾,保證玩家1產生的波浪僅與玩家2碰撞,反之亦然。這裡稍微提一下阻力(Drag),用純物理方法對角色添加外力(AddForce)使其快速加速時,通常會使用阻力進行平衡以防止角色漂移。但添加太多阻力會讓角色在跳躍時降落緩慢,從而產生一些奇怪的漂浮物理效果。

我們在此例中實現了一個自定義阻力,藉由將 Rigidbody 速度的 x 與 z 分量乘以一個隨意因子,不改變 y 分量(重力):

rb.velocity = new Vector3(rb.velocity.x * .8f,rb.velocity.y,rb.velocity.z * .8f);

場景設置


從下圖中可以看出,這個遊戲是2D與3D混合的,角色是沿X軸旋轉30度的Sprite,以配合相機旋轉,場景下方掉落的水滴與岩石也同樣旋轉。盛水的圓盤是遊戲中唯一貨真價實的3D對象,因為要用它來實現複雜而逼真的波浪。


角色都帶有 Capsule Collider (膠囊碰撞器),以及一個鎖定旋轉的 Rigidbody,保證角色只會走動不會傾斜。

粒子

我們為跳躍動作添加了一些粒子作為反饋,增加一種“水花四濺”的有趣體驗:


其中,瀑布的粒子特效是使用一個大的圓形發射器。我還將前面的粒子與後方粒子分開,後方粒子是通過另一個粒子系統生成,放在180度的圓形模塊上。後方粒子的顏色不變,且生命週期非常短,因為它們完全不可見。



通常我們會盡可能讓所有參數(生命週期,速度,重力等等)都隨機變化,但這裡我們使用固定數值,因為希望粒子能實現像素風的感覺。同理,我們對粒子隨生命週期的顏色漸變也使用了固定模式而非混合,這樣粒子會快速改變顏色,而不是如往常一樣漸變消失。


總結

總體來說,本文涉及的一些解決方案都不是完美且未經過優化的,更像是一個為實現靈感的Demo。但是因為這是追求快速極致的 Game Jam,所以性能不那麼優秀也沒關係。

Splash Clash的開發者Ciro Continisio向我們分享了參加Game Jam活動時的時間分配方法:將更多時間花在程式質量上而非優化,在編寫程式時採用更穩健的方法,少一些混亂程式,多一些邏輯上的類結構與信息流,以避免在最後關頭出現Bug。

2017年2月2日 星期四

Unity WebGL的低階Plugins

作者:Marco Trivellato 原文
潤稿:Kelvin Lo

我們之前分享了詳解Unity WebGL記憶體。今天我們一起來看看Unity WebGL平台如何使用低階
(Low-Level)Plugin,如果你想用網頁重用現有的C/C++程式碼,例如OpenGL ES的圖形效果,就別錯過今天的內容。我們從Plugin類型、如何實現以及展示範例幾個方面進行探討。

Unity支援兩種類型的Plugin:託管(Managed)
Plugin和原生(Native)Plugin。對於託管Plugin來說,WebGL與其它平台支援託管Plugin的方式一致,唯一的區別在於Plugin的託管程式集與引擎及使用者的託管程式碼會一起轉為JavaScript,這裡指asm.js/wasm。

對於原生
Plugin來說,針對網頁來談"native"真的有意義嗎?如果原生指的是例如Mac,Win 32/64等底層架構,當然沒有意義。然而,Unity WebGL也支援幾種其它類型的Plugin:JavaScript、C/C++以及預編譯的LLVM位元組碼。

Unity使用手冊中有幾個WebGL平台下JavaScript和C語言
Plugin的例子,並且展示了如何透過程式和它們溝通。既然可以使用C/C++原始程式碼,那就可以存取低階的Plugin介面來渲染。Unity 5.5就可以透過新加的連結器,來和低階Plugin綁定

如你所見,我們需要加很多程式,來看看實現Plugin需要哪些步驟。

介接Plugin


首先,需要將Unity安裝目錄下Editor/Data/PluginAPI中的Plugin API標頭檔,複製到
Plugin原始檔案所在的目錄。Plugin使用標頭檔IUnityInterface.h和IUnityGraphics.h來宣告所需的介面。由於這些標頭檔適用於特定的Unity版本,所以確保與編輯器版本同步是很重要的。註冊Plugin需要呼叫函數UnityRegisterRenderingPlugin。

extern "C" void UnityRegisterRenderingPlugin(PluginLoadFunc loadPlugin, PluginUnloadFunc unloadPlugin);

無論如何,首先需要實現Load和Unload的callback函數來取得IUnityGraphics介面,並註冊或登出用於底層渲染的圖形設備callback函數。程式如下:


IUnityInterfaces* s_UnityInterfaces = 0;

IUnityGraphics* s_Graphics = 0;


extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
    s_UnityInterfaces = unityInterfaces;

    s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
    s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);

    // Run OnGraphicsDeviceEvent(initialize) manually on plugin load
    OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload()
{
    s_Graphics->UnregisterDeviceEventCallback(OnGraphicsDeviceEvent);
}

實現了以上介面,就可以註冊
Plugin了。

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API RegisterPlugin()
{
    UnityRegisterRenderingPlugin(UnityPluginLoad, UnityPluginUnload);
}

最後,啟動時需要加上C#綁定並註冊
Plugin,確保UnityRegisterRenderingPlugin確實執行了。

class MyRenderPlugin
{
    #if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport ("__Internal")]
    private static extern void RegisterPlugin();
    #endif

    void Start()
    {
        RegisterPlugin();
    }
}

然後需要實現OnGraphicsDeviceEvent並加上渲染碼。

#include <GLES2/gl2.h>

static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType)
{
    // OpenGL ES calls here...
}

請注意,如果不同的平台使用相同的
Plugin原始檔案,可以檢查__EMSCRIPTEN__或UNITY_WEBGL(僅限Unity 5.6及以上版本)來條件編譯程式。

#if defined(UNITY_WEBGL)

// Unity WebGL-specific code

#endif

最後,如果需要瀏覽系統標頭檔,如:gl2.h,可以在Unity安裝目錄下找到:

Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten/system/include

Plugin範例


如果有興趣嘗試製作渲染
Plugin,研究Unity官方Bitbucket上的NativeRenderingPlugin是一個非常棒的起點。它已經設定註冊了所需的callbacks,編譯了自己的著色器,並展示如何在簡單的Unity場景中渲染一個三角面。

注意,在Unity WebGL中不需要單獨建立C/C++
Plugin。這個範例中的Unity專案包含一個簡單的檔(Plugins / WebGL / RenderingPlugin.cpp),其中包含了Plugin的具體實現,如下所示:


#include "../../../../PluginSource/source/RenderingPlugin.cpp"

#include "../../../../PluginSource/source/RenderAPI.cpp"

#include "../../../../PluginSource/source/RenderAPI_OpenGLCoreES.cpp"

展示


最初的OpenGL 2.0 Demo是用C++和GLSL寫的,所以只需做一些修改,就能和前面的原生渲染Plugin Demo完美相容。您可以下載Unity的Demo專案和Plugin原始碼

總結


本文介紹了Unity WebGL中低階
Plugin的實現辦法。考慮到Unity WebGL平台的除錯工具並不豐富,建議可以在不同的GLES2/3 API平台上製作原型,確保WebGL平台一旦建立成功同時也能在iOS、Android或PC正常執行。

2017年2月1日 星期三

Unity的批次處理與GPU Instancing技術介紹

作者:馬瑞曾 原文
潤稿:Kelvin Lo

我們都希望能在場景中放它一百萬個物體,可惜的是渲染和管理大量的遊戲物件代價是消耗大量的CPU和GPU效能,因為有太多Draw Call需要處理,所以我們必須找到其他的解決方案。

本文我們將討論兩種優化技術,它能幫你減少Unity的Draw Call數量從而提高整體效能,那就是批次處理和GPU Instancing。

批次處理


開發者會遇到最常見的問題之一就是效能不足,因為CPU和GPU的處理能力的問題。一些遊戲可以在PC上順暢執行,但在手機上不行。遊戲執行是否流暢Draw Call數量的影響很大。有幾個常見的解決方法是批次處理,包括Static Batching和Dynamic Batching。

Static Batching靜態批次處理可以讓引擎降低任何網格的Draw Call,要讓場景中的物體使用Static Batching,只要將它標記為靜態物件並在Mesh Renderer中共用相同的材質即可


因為Static Batching不會在CPU上做頂點轉換,所以它通常比Dynamic Batching更有效果。但它會用較多的記憶體,如果你的場景有相同物體的多個實例物件,Unity會將它們組合成一個大網格,這可能會增加記憶體使用。Unity會將盡可能多的網格結合到一個靜態網格中,並將它作為一個Draw Call送出申請。這種方法的缺點是靜態物件在整個執行週期都不能移動。

Dynamic Batching啟用時,Unity會自動試著把物件編到同一Draw Call處理。要使物體可以被動態批次處理,它們必須要共用相同的材質,還要遵守一些限制:

  • 頂點數量:Dynamic Batching場景中物件的每個頂點都有一定的消耗,因此動態批次處理只適用於少於900個頂點的網格物件。舉例來說,如果你的Shader使用頂點位置,法線和一個UV,那麼你可以動態批次處理多達300個頂點。而如果你的Shader使用頂點位置,法線,UV0,UV1和切線,那麼只能動態處理180個頂點。這部分我們未來有可能會改寬限制。
  • 反射資訊:如果物件的Transform有反射資訊,例如A物件的大小是(1f, 1f, 1f),而B物件的大小則是(-1f, -1f, -1f),則無法做批次處理。
  • 材質:如果物體使用不同的實例材質,即使它們本質上相同,也不會被批次處理。但Shadow Caster Rendering是個例外。
  • 渲染器:擁有光照貼圖的物件有其他渲染器參數,例如光照貼圖索引或光照貼圖的偏移與縮放。一般來說,動態光照貼圖的遊戲物件應該指向要批次處理的完全相同光照貼圖的位置。
  • 不能使用Multi-pass著色器的情況:幾乎所有的Unity著色器都支援多個燈光的Forward Rendering模式,這會需要額外的渲染次數,所以繪製"額外的每圖元燈"時不會被批次處理,傳統的Legacy Deferred(Light Pre-Pass)渲染通道不能被動態批次處理,因為它必須繪製物體兩次。
Dynamic Batching原理是將所有物體的頂點轉換為CPU上的世界空間來工作,所以它只能在渲染Draw Call的工作量小於CPU頂點轉換工作量的時候才會有優化作用。當在PS4遊戲機或者像Metal這樣新的API,Draw Call的消耗通常低得多,或許開啟Dynamic Batching也無法提高效能。瞭解以上限制後,就能正確判斷是否要用批次處理來提高遊戲效能。

GPU Instancing


提高圖形效能另一個好辦法是使用GPU Instancing。GPU Instancing的最大優勢是可以減少記憶體使用和CPU消耗。當使用GPU Instancing時,不用打開批次處理,GPU Instancing的目的是一個網格可以與一系列附加參數一起被推送到GPU。要使用GPU Instancing,物件必須使用相同的材質,且可以傳遞額外像是顏色或浮點數的參數到著色器。

Unity從5.3版本開始支援GPU Instancing。 唯一的限制是在遊戲物件上要使用相同的材質和網格。且目前只支持以下平台:

  • Windows DX11/DX12 和 SM 4.0 或更高
  • OpenGL 4.1 或更高
  • OS X and Linux:OpenGL 4.1 或更高
  • 手機:必須支援OpenGL ES 3.0 或更高/Metal
  • PlayStation 4
  • Xbox One

如果想要進一步優化,例如減少管理場景物件的消耗,也可以使用Graphics.DrawMeshInstanced方法。 只需要傳遞網格、材質和附加屬性來繪製該物體。目前的限制是一次最多1023個實例物件。在Unity 5.6已經加了Graphics.DrawMeshInstancedIndirect新方法,可以用來指定需要渲染的實例數量。

GPU Instancing做法


要建立支援GPU Instancing的材質可以在專案新增一個專用Shader:
從Project視窗點右鍵選Create->Shader->StandardSurfaceShader(Instanced)。


然後,在材質裡的Shader選擇這個新的著色器。



雖然實例的物體共用網格和材質,但你可以用MaterialPropertyBlock API針對每一個物體單獨設定不同的著色器屬性。

要注意的是,如果一個物件被標為"Static"進行了Static Batching,那這個物件就不能用GPU Instancing技術處理,檢視介面會出現警告,提示"靜態批次處理"標誌可以在Player Settings中取消。如果物件符合Dynamic Batching條件,但同時也符合GPU Instancing條件,那麼這個物件會自動使用GPU Instancing技術。

當使用Forward Rendering渲染模式時,受多個燈光影響的物體無法有效地產生實例。只有Base Pass可以有效利用產生實例。此外,使用光照貼圖或受不同光或Reflection probe影響的物件無法產生實例。如下圖所示,您可以開啟Frame Debug視窗,查看和GPU Instancing相關的Draw Call(被標記為Draw Mesh(Instanced)"。



GPU Instancing是一個非常強大的功能。在Unity 5.6你可以用Graphics.DrawMeshInstancedIndirect繪製大量網格。我們用Mac Pro畫出約68萬個各自不同顏色的移動方塊,並保持穩定的60FPS。


可以看看Unite大會上的超強展示




總結

本文中我們描述了用於優化渲染效能的兩種技術:批次處理和GPU Instancing。我們展示了如何實踐它們並探討可能的應用。正因為有這樣的技術,我們才能夠在繪製大量物件的情況下還能保持穩定效能。


更多關於GPU Instancing的文章
http://unitytaiwan.blogspot.tw/2016/05/unite-2016-unity-54-gpu-instancing.html

著作人