2016年1月27日 星期三

Unity 5.3中的GGX著色器

作者:MORTEN MIKKELSEN 原文

在Unity 5.3的標準著色器中,我們改為使用GGX來實現BRDF,來進行點光源和平行光等光源的光照計算,當然也可以計算基於圖像的光照。此外,我們徹底修改了方體貼圖的計算,使其能夠以較少的執行時間達到精確無噪點的結果,這項功能將在Unity 5.4中完成。GGX與標準化的Phong算法最大的區別在於GGX所擁有的微表面分佈圖,有著更高更窄的峰值,且緊隨著更大更長的尾巴,如下圖所示。

這讓最後的光照效果變成:GGX具有更亮的高光部分並在周圍蔓延著光暈,給人以更加真實的視覺效果。
如下圖所示:


GGX與傳統的標準化Phong光照對比


跨行業也能相容的材質

在學術界基於物理的BRDF光照模型一般用roughness(粗糙度)這個參數來控制微平面分佈函數,而roughness被定義為分佈函數的均方根斜率。

一個常見的誤解是認為在CG中使用的粗糙度貼圖和學術上所說的roughness是一回事。之所以學術上的roughness沒被用在紋理貼圖或者是使用滑塊控制是因為它的“粗糙度”並不是均勻分佈的,導致難以操作,並且紋理貼圖的有限位精度難以達到比較好的效果。

為了避免誤解,Unity使用smoothness(平滑度)而不用roughness這個字眼,smoothness在shader中會使用公式(1-smoothness)^2來轉換為學術上的roughness參數。通過這個轉化,我們得到的結果和roughness一樣,只是剛好顛倒了一下,也就是說0.0對應於最粗糙的表面而1.0對應於最光滑的表面。
我們認為這樣的對應方式給人的感覺更為直覺。這樣一個標準化的分佈方式最大的意義在於我們可以在Unity中導入其它外部工具製作的材質,並在Unity中實現非常接近的效果。 

因為如今業界大部分的CG繪製工具都支持smoothness貼圖。不過光是這樣並不能保證可以達到一模一樣的效果,但是可以肯定的是,漫反射光與高光的比例以及整體的高光模糊度都會是非常接近的。

下圖是由Allegorithmic公司的Wes McDermott提供的Unity 5與Substance Painter的效果對比圖。從圖中我們可以看到兩者的視覺效果是非常相似的。



Unity 5.4中的改進


在Unity 5.4中我們將以改進方體貼圖計算速度與基於圖像光照的計算(IBL)達到一個無噪點的視覺效果。

如下圖所展示在Unity 5.4中一個球體的光照計算與傳統的光跡追蹤法的效果比較。

傳統的光跡追蹤法即使每個圖素執行了50000次的光追蹤,最終的光照還是帶有大量的噪點。追根究柢,光跡追蹤法在進行BRDF採樣時,對於環境貼圖中類似太陽這樣的奇點(Singularities)有著天生缺陷。這問題已在Unity 5.4版本中已經得到解決,並且方體貼圖的離線計算比Unity 5.2快了將近兩倍的速度。

2016年1月14日 星期四

技術分享:呼叫1000個Update()

作者:VALENTIN SIMONOV 原文

Unity有一個訊息系統可以在遊戲執行時對腳本內部呼叫各種Mothod。對於初學者也非常簡單又容易理解。只需要定義一個Update方法,便可以每幀呼叫Mothod中的內容。
void Update() {
    transform.Translate(0, 0, Time.deltaTime);
}
有經驗的人可能會對此產生疑問:
1.不清楚這個方法究竟是如何被呼叫的
2.不清楚當一個場景中有多個物件時,這些方法的呼叫順序是如何
3.這種程式風格不是十分智能

UPDATE是怎麼被呼叫的

Unity並沒有使用System.Reflection進行方法的定位。
取而代之的是,首先給定類型的MonoBehaviour通過底層腳本進行檢查,判斷腳本執行過程中(無論是Mono或IL2CPP)是否有方法被定義,同時其中內容有沒有被緩存。如果檢查到特定的方法,便增加到一個合適的列表中。例如,當一個腳本中定義了Update方法後,這個腳本便被增加到一個需要每幀更新的腳本列表中。

在遊戲過程中,Unity只需要重複執行所有列表中的方法即可。所以你的Update方法究竟是public還是private並不重要。

UPDATE方法的執行順序

UPDATE方法的執行順序可以從Script Execution Order Settings來設定。(Edit > Project Settings > Script Execution Order)要手動設定1000個腳本的執行順序不是什麼好主意,但是要微調某些特定腳本的執行順序還是可以的。當然,未來我們將會提供更加方便的方式來指定執行順序,比如在程式中使用一個屬性。

無法與INTELLISENSE一起使用

在Unity中,我們使用某類IDE編譯C#腳本,這些IDE大多無法識別這些特定方法應該在何處被呼叫。因此常會導致警告以及不容易指向程式。
有一些開發者用一個叫BaseMonoBehaviour或類似名字的抽象類別擴展MonoBehaviour,然後在他們的項目中的每個腳本裡都擴展這個類。他們在其中寫了一些有用的功能以及一堆虛擬的特殊方法:
public abstract class BaseMonobehaviour : MonoBehaviour {
    protected virtual void Awake() {}
    protected virtual void Start() {}
    protected virtual void OnEnable() {}
    protected virtual void OnDisable() {}
    protected virtual void Update() {}
    protected virtual void LateUpdate() {}
    protected virtual void FixedUpdate() {}
}
這個結構可以使你在程式中使用MonoBehaviour時更有邏輯性,但存在一個小缺點。或許你已經發現了

所有你的MonoBehaviour都會儲存在Unity的內部更新列表裡,所有你的腳本都會在每幀裡呼叫這些基本上沒怎麼用到的方法!
有人可能會問為什麼會有人關心一個空方法?因為這些從C++到託管C#的呼叫有效能上的開銷。讓我們來看看成本為何。

呼叫10000個UPDATE

我為這篇文章在Github上做了一個範例,它有兩個場景,可以透過點擊手機設備或在編輯器中按任意鍵互相切換:

(1) 在第一個場景中,使用下面這樣的程式建立了10000個MonoBehaviour:
private void Update() {
    i++;
}
(2) 在第二個場景中,建立了另外10000個MonoBehaviour。不過,不同的是,這個程式中並不是只呼叫Update,而是像下面這樣,加入了一個由Manager腳本在每幀都呼叫一次的自訂UpdateMe方法。
private void Update() {
    var count = list.Count;
    for (var i = 0; i < count; i++) list[i].UpdateMe();
}
測試專案在兩台iOS設備上被編譯為Mono以及IL2CPP,發佈設定中都設為non-Development 模式。它們的執行時間記錄如下:

1. 在第一次Update呼叫時設定一個Stopwatch (在Script Execution Order中配置)
2. 在LateUpdate時停止Stopwatch
3. 將獲得的時間均攤到幾分鐘上

Unity版本: 5.2.2f1
iOS版本: 9.0

Mono


哇!好多時間!測試肯定哪裡出了問題!

實際上,我只是忘了把Script Call Optimization 設為Fast but no exceptions,但是現在我們能看到這種設定對性能的影響了…至於IL2CPP不必太在意。

Mono (fast but no exceptions)


OK,這樣好多了,讓我們切換到IL2CPP。

IL2CPP


這裡我們發現兩件事情:
1.這個優化對於IL2CPP同樣有用
2.IL2CPP仍有改進空間,而且在寫這篇文章的同時Scripting 與IL2CPP團隊正在努力提高性能。比如,最新的Scripting分支內包含的優化可以讓測試執行快35%。

我後面會說明Unity在背後做了什麼,現在先讓我們修改下Manager程式,讓它加速5倍!

介面呼叫,虛擬呼叫以及陣列存取

如果你沒讀過This great series of posts about IL2CPPinternals這篇文章,你可以在讀完本篇之後看一下。

結果告訴我們如果你想在每幀裡都迴圈反覆運算擁有10000個元素的清單,那應該使用陣列而不是List,因為這樣生成的C++程式會更簡單,而陣列存取就會快很多。

在下一個測試中,我把List<ManagedUpdateBehavior> 改為了ManagedUpdateBehavior[]。

這看起來好多了! 我用陣列的方式在Mono上執行測試花了0.23ms。

解救之道!

我們發現了從C++呼叫C#函數較慢,不過讓我們再研究下當呼叫所有這些對象的Update方法時,Unity實際上做了些什麼。最簡單的方法就是使用Apple Instruments的Time Profiler。
注意這不是Mono與IL2CPP 的對比測試 - 討論的大多數內容對Mono iOS構建同樣適用。
我在iPhone6上用Time Profiler啟動了測試專案,記錄了幾分鐘的資料,然後選擇了一分鐘檢視一次。從這行程式開始的所有東西我們都很感興趣:
void BaseBehaviourManager::CommonUpdate<BehaviourManager>()

如果你以前沒有使用過Instruments,右邊是按照執行時間排序的函數,以及它們呼叫的其他函數。最左邊的列是以毫秒為單位的CPU時間,以及這些函數及其呼叫的函數所占的CPU時間百分比。左邊第二列是函數自己的執行時間。注意,在這個實驗中Unity並沒有將CPU使用完,所以我們能看到在60秒間隔內有10秒的CPU時間花在了我們的Update上。顯然,我們關心的是那些執行時間最長的函數。

我用矬矬的Photoshop技術畫了一張圖分幾區顏色,說明到底發生了什麼事。

UpdateBehavior.Update()
在中間你能看到我們的Update方法,以及IL2CPP是如何呼叫它的 —UpdateBehavior_Update_m18。但是Unity在那之前還做了很多其他事。
迴圈反覆運算所有的Behaviour
Unity迴圈反覆運算所有的Behaviour並執行更新。特殊的反覆運算類SafeIterator確保了即使移除了列表中的下一項,整個迴圈也不會中斷。僅僅是迴圈反覆運算所有已註冊的Behaviour就用了9979ms中的1517ms。
檢測呼叫是否有效
下一步,Unity做了一堆檢測,確保呼叫的方法是屬於某個已啟動已初始化且Start方法已呼叫過的GameObject的。你肯定不希望在Update裡銷毀一個GameObject時讓遊戲當機,對吧?這些檢測花去了整個9979ms中的另外2188ms。
準備呼叫方法
Unity建立了一個ScriptingInvocationNoArgs實例 (代表了一個從原生到託管的呼叫)以及ScriptingArguments,然後命令IL2CPP虛擬機器呼叫方法(scripting_method_invoke函數)。這一步消耗了整個9979ms中的2061ms。
呼叫方法
scripting_method_invoke函數檢測傳入的參數是否有效(900ms),然後呼叫IL2CPP 虛擬機器的Runtime::Invoke方法 (1520ms)。開始時,Runtime::Invoke檢測方法是否存在 (1018ms)。而後,它呼叫一個生成的RuntimeInvoker函數獲取方法簽名(283ms)。接著再依次呼叫我們的Update函數,根據Time Profiler,這一步花了42ms。

再來一個漂亮的彩色表格。


託管更新

現在使用Time Profiler來測試我們的Manager。你在螢幕上可以看到還是同樣的一些方法(有些方法因為執行時間少於1ms,甚至都沒出現),但是大部分的執行時間實際上都花在了UpdateMe函數上(或者說花在了IL2CPP呼叫ManagedUpdateBehavior_UpdateMe_m14上)。另外,IL2CPP還插入了一個null檢測,確保我們迴圈反覆運算的陣列不會為null。

下面這個圖片使用了和上面相同的顏色分類。

所以,你現在覺得如何,我們應該忽略那小小的方法呼叫嗎?

有關測試的幾句話

老實說,這個測試並不是完全公平的。Unity為了防止你的遊戲出錯或當機,做了很多事:這個GameObject是否已啟動?它是否在Update迴圈中被銷毀了?物件上是否存在Update方法?怎麼處理在這個Update迴圈中建立的MonoBehaviour?—我的Manager腳本沒有處理這其中任何一項,僅僅是迴圈反覆運算了一堆的物件,呼叫它們的Update而已。

在真實世界中,Manager腳本可能會更加複雜,執行得更慢。但是,我是個開發者—我知道我的程式要做什麼,我架構我的Manager類時,知道可能的行為是什麼,什麼不會出現在我的遊戲中。而不幸的是,Unity並不知道這些。


下一步你應該怎麼做?

當然這完全視你的專案而定,但實戰中碰到一個遊戲在單一場景中使用大量需要在每幀都執行一些程式的GameObject的情況並不少見。而且通常這看起來都是些不起眼的小程式,似乎不會影響到任何東西,但當其數量非常巨大時,呼叫幾千個Update方法的開銷將變得很大。這個時候再去修改遊戲架構,可能已經為時已晚。

現在你明白了,在你開始下一個專案時考慮一下吧。

2016年1月7日 星期四

一封來自十年前Unity創始人David Helgason的郵件

2016年1月5號是一位叫做Aras Pranckevicius的員工加入Unity的10週年,我們的創始人David Helgason發了一封內部信件恭喜他,並且與大家分享了十年之前(2005年)發給Aras的信。 

圖片為David在辦公室休息的照片

十年前其實Unity還有一個名稱叫OTEE,後來才正名為Unity,而1月5號當天也剛好是David的13週年紀念


------------------
Hi Aras:

我是David Helgason,OTEE的CEO (OTEE- Over the Edge Entertainment是Unity最初的名字,有時候也簡寫成OTE或OtEE,後來這名字變得既沒法發音也很難拼寫,最終我們決定簡單點改名為Unity)。OTEE是一個遊戲中介軟體,後來我們把它定義於「遊戲開發工具」,最終它成為了「遊戲引擎」。


我們觀察你有段時間了,也非常欣賞你的工作。 我們希望跟你談談,邀請你加入我們的團隊,和我們一起進行這場行業革命。你可以帶領PC 版本的研發,定義屬於自己項目,開發一個面向大眾市場的工具,是不是很酷?它完全可以改變遊戲製作的整個過程...編寫程式來改變世界。

我們正在創造的Unity將改變中小型的開發人員創造遊戲方式,它將是一款強大的工具,既具備其他遊戲引擎的全部功能(雖然現在這麼說有點誇張,但至少那是我們想要達到的),也像Flash一樣靈活易用。

現在Unity已經在市場上佔據了一席之地,我們希望人們能夠通過Unity,開發出任何他們能夠夢想的遊戲。

Unity已經讓美術和獨立開發者們能更快更好地創造出遊戲。Unity現在要做的是吸引更多專業的遊戲開發者,所以還需在新的領域不斷研發,比如:可視化與遮擋剔除,改善現有的光照模式,創造適用於Windows的Web Player,將執行環境移植到各種主機,將編輯器移植到Windows上等等…

當然我們現在還是一個非常小型團隊。我們一開始開發遊戲,但是卻發現自己在技術研發方面能為大家帶來更多。

現在介紹我們的團隊。我,CEO(兼銷售,網站,運營等),Joachim Ante,CTO(他自己就是一支軍隊),Nicholas Francis,遊戲設計師(他是公司裡面最能幹的全能好手),還有2名實習生Peter Jensen和Raimund(PS: 他們的碩士論文還沒有寫完)。

我們迫切地希望你加入我們的團隊。為什麼!

我們希望進行一場遊戲製造的革命,我們需要另外一個人,他將是一支強大的軍隊,推動著Unity向前進。你就是那個人。(儘管這話有一絲讓人尷尬,但是我們確定地相信你就是那個人)

我們會做一起做出非常Cool的東西,當然,我們都是非常Nice的人,哥本哈根也是一個非常棒的城市(這些都是真的!!!)

你準備好加入我們了嗎?


David Helgason, Founder
Unity Technologies
-------------------------
這個郵件的結果是什麼呢?Aras無情的Say No, David悲劇的被拒絕了。不死心的David又堅持不懈的幾個月後邀請Aras去哥本哈根參加Game Jam活動。最終Aras決定"屈服",加入Unity,並且在以後的十年裡面,一直在Unity的核心研發團隊工作,給我們廣大的開發者奉獻了一款承載他們曾經夢想的遊戲引擎。


回首十年過往,Unity已經成為最受歡迎的遊戲開發引擎和平台,但初心從沒改變。

照片由David & Aras 提供。 


 

2016年1月3日 星期日

利用資源分割優化iOS包裝大小


POVILAS KANAPICKAS 原文

App分割(App Slicing)是iOS9.0和tvOS開始加入的一個新功能,主要為了減小主要App包的大小。開發者可以根據不同蘋果設備上傳不同的版本的包到App Store, 只有符合使用者設備的版本才會被下載。

App分割在iOS平台上很有用,因為這將幫助開發者包入更多的資源(Asset)到主要的包內還能不超過100MB。要達到這個目的,你當然也可以採用我們之前介紹的動態載入資源(On-demand Resource)方法。但就代表產品必須連著網路隨時動態下載所需資源,不是很方便做法也需要一些技術。App分割沒有這些問題。

App分割可分為兩類:可執行程式的分割和資源的分割。前者分割後會移除執行檔中不需要的可執行程式。App Store對於所有iOS/tvOS 9.0及以上版本的App會自動執行這動作。而後者會移除不需要的資源,開發者需要設定些東西來達成這個功能,本文將會說明如何實現。

資源分割的主要目標是解決相同資源各種不同形式的使用下減少浪費空間和頻寬,頻繁的重複利用資源是很常發生的,各種不同新舊程度的iOS設備也仍在廣泛使用中,它們硬體版本之間存在許多效能的差異。所以開發者常會因為要在不同設備表現最佳品質需要使用不同的資源(例如:不同解析度的圖片),好照顧到不同效能的設備。在iOS 9.0之前,資源需要包含在主App包中,這既浪費設備空間也浪費頻寬,因為最後只會用到所有資源的一部分。透過App資源分割,用不到的資源都能被排除在外。

資源分割最簡單的方法就是透過Asset Bundle,這是最有效也最簡單的方案。Asset Bundle是Unity內建的功能,效能和載入一般普通檔案的效能沒差別。Asset Bundle也能用於剛剛提到動態載入資源的後端,所以對一個已經使用了動態載入的App要額外幫App瘦身就很方便,反之亦然。當Asset Bundle設定完成後,基本不需要做額外修改就能讓App資源分割。本篇文章不會說明如何使用Asset Bundle,如果你有興趣可以參考這裡

要使用應用資源分割,開發者必須先指定哪些設備要使用哪些資源,然後在App啟動時載入這些資源,在iOS App開發設定,你需要先在資源目錄裡建立資料集(Data Set)或圖像集(Image Set),設定裝置變數然後指定包含哪些資源。在Unity裡,你需要設定Asset Bundle變數(即多個含有相同資源集的不同包裝)。並在程式中用UnityEditor.iOS.BuildPipeline.collectResources 來註冊所有參與App資源分割的資源包。然後在Player Settings中指定設備要求。最後在App啟動時使用AssetBundle.CreateFromFile 手動載入資源包。


下面兩段範例說明App資源分割的做法:
註冊要使用App資源分割的資源(編輯器腳本)

using UnityEditor.iOS;
#if ENABLE_IOS_APP_SLICING
public class BuildResources
{
 [InitializeOnLoadMethod]
 static void SetupResourcesBuild()
 {
   UnityEditor.iOS.BuildPipeline.collectResources += CollectResources;
 }
 static UnityEditor.iOS.Resource[] CollectResources()
 {
  return new Resource[] {
    new Resource("asset-bundle-name").BindVariant("path/to/asset-bundle.hd"), "hd")
                                     .BindVariant("path/to/asset-bundle.md"), "md")
                                     .BindVariant("path/to/asset-bundle.sd"), "sd"),
  };
 }
在啟動時載入資源包
< ...>
var bundle = AssetBundle.LoadFromFile("res://asset-bundle-name");

// now use AssetBundle APIs to load assets
// or Application.LoadLevel to load scenes

var asset = bundle.LoadAsset("Asset");
< ...>

簡單入門Asset Bundle和App分割的方法就是研究Asset Bundle 範例。我們已經放在BitBucket讓開發者下載,裡面也有詳細描述如何使用這些Demo。

著作人