2017年9月5日 星期二

Unity說明文件範例正確性檢測 - 玩轉Unity編輯器測試工具

作者:Karl Jones 原文
發表於:2017/8/18

潤稿:Kelvin Lo

每當我遇到不熟悉的API,當下第一反應一定是去查查Unity文件,看看文件裡的範例來瞭解API的使用方式。但如果按照文件依樣畫葫蘆卻編譯失敗的話,是否可能是文件出錯了呢?

這是Unity Hackweek(註一)的一個專案,利用Unity 5.3以上的編輯器測試工具,來自動檢驗所有Unity腳本的範例程式是否編譯成功。

Unity腳本文件共有15000頁左右,其中並非所有文件都包含範例(這需要另外解決),但也已經不少了。手動檢查每個範例加上測試,在為期一周的Hackweek中很難辦到,也無法解決未來的API更新衍伸的文件更新問題。

然而去年發佈的Unity 5.3中包含一個新功能:Editor Test Runner,它是能在Unity中執行的單元測試框架(unit test framework)。這個功能發佈後就一直用於Unity內部測試,幫助我們追蹤問題。Unity所有腳本文件都保存為XML檔,可以在Unity內部專案中編輯。



專案已經包含解析XML檔的程式,所以只需在專案中加上編輯器測試即可使用這些功能。在編輯器測試框架中會用到TestCaseSource屬性,讓測試多次在不同的來源資料上執行。在本例中,來源資料就是程式範例文件:



這個方法會顯示所有執行在Test Runner上的測試。每個測試都能獨立執行,或通過Run ALL選項來同時執行。


本範例使用CodeDomProvider 編譯,這樣可以傳遞多個表示腳本的字串,並編譯和返回相關的錯誤及警告資訊。
首次測試反覆運算的縮減版(已刪除XML解析):




運作很正常! 編譯腳本的方式還需做些調整,因為有些腳本是組合在一起的方式形成一個範例。分別編譯所有範例以檢測是否有這種情況,如果出現錯誤再將其合併編譯,看看是否成功。

還有一些簡單範例就是一行程式,沒有封裝在函數內。可以在測試中進行封裝來處理這種問題,要遵循的規則就是這些例子必須能獨立執行(即開發者將它複製貼上到一個新的檔案中也可以被編譯和執行),否則就將這些範例視為測試失敗。

這個測試方法現在離正式作為文檔驗證工具還有一段距離。我還需要解決一個小問題:這個測試流程執行需花費30分鐘。由於我們每天需要執行約7000個版本,僅僅作為一個版本驗證測試來說這個執行時間太長了。

目前這個測試方法是依序執行的,一次一個腳本。由於測試彼此獨立,且無需呼叫Unity API,因為只測試編譯是否成功,所以完全可以並行執行這些測試。下面引入用於並存執行任務的.NET API-執行緒池。將測試作為單個任務放入執行緒池中,當執行緒可用時立即執行。這需要從單個函數執行,即不能用單獨的NUnit測試用範例來測試文件的單一範例。儘管不能單獨測試,但我們大大提高了整體執行的速度。



這將測試時間從30分鐘縮短至2分鐘,作為版本驗證來說已滿足需求。由於無法測試單個範例,所以在腳本文件編輯器中加入按鈕,以便文件編寫人員日後更新。執行測試時出錯腳本會顯示為紅色,並在下方顯示錯誤資訊。

首次執行測試時有326個錯誤,將這些加入白名單以便日後更新。畫面現在只剩32個,大多錯誤都是由於無法存取特定的程式集導致。整合該測試並未引入新的問題,所以可以確定如果棄用部分API導致測試失敗,則需要更新文件來使用新的API。

結語

本文只是Editor Test Runner工具一個非常有趣的使用案例,當然還有待改進,例如只能抓取C#範例,無法處理.js的編譯。但在不久的將來,這也將不再是問題。(註二)

感謝各位,可以從這裡下載完整程式碼

<註一> Unity Hackweek是一個Unity內部活動,每年研發人員會集合並討論Unity有哪些需要改進的地方,並在為期一周的時間,動手做個初始原型,有點像是企業內部的黑客松。
<註二> Unity已經對外發佈未來即將不支援 Unity Java Script。會專心維護C#。

2017年9月4日 星期一

Unity遊戲開發中的AI - 基於Q-Learning的強化學習

作者:Arthur Juliani 原文
潤稿:Kelvin Lo


歡迎來到Unity AI系列文章第二篇,我們上次已經為大家介紹了遊戲開發中的AI - 入門,今天這篇文章將延續上次的內容繼續探討如何將它擴展到一個
完整能強化學習問題的吃角子老虎機。在這個過程裡,我們會展示如何利用一個Agent透過Q函數學習,來預估長期在特定條件下採取特定行為的值。本例僅使用一個簡單的網格空間(gridworld)以及一個Q-Representation列表。這原理可以應用於任何遊戲上。

Q-Learning演算法

情境老虎機回顧
強化學習(Reinforcement Learning)的目的是在特定環境下訓練一個Agent代理,通過學習來實現特定環境的最大化預期利益。在"遊戲開發中的AI -入門"一文中,環境是相對靜態的,環境狀態也很單純,就是代理躲在3個箱子其中之一,而行為就是選擇打開哪個寶箱。我們的演算法為每一對狀態行為學習Q函數,Q函數則對應在該狀態下隨著時間前進採取某種行為而獲得的預期利益。這樣的問題通常被稱為“情境老虎機”(Contextual Bandit)。

強化學習問題

要將前面的情境老虎機問題轉換為強化學習問題,還缺少兩個關鍵因素:稀有獎勵與狀態轉換。稀有獎勵是指代理並非每次採取行為都可以獲得獎勵。有時獎勵會“延遲”,因為有些行為可能不是必須的,也可能要在一系列非必須行為之後才能獲得獎勵。更具體的展示是,代理可能跟隨正確路徑,但只有到達終點才能獲得獎勵,而非走過的每一步都有獎。儘管中途沒有獎勵,但走過的每一步對到達終點來說都至關重要。我們需要一種方式來執行“信用分配”,即讓代理知道早期的行為是有價值的,儘管只是間接價值。

第二個缺失的因素是強化學習中狀態之間的轉換。有了狀態轉換之後,我們的行為不再是只根據獎勵函數R(s, a) ⇨ r來獲得獎勵,還會根據狀態轉換函數P(s, a) ⇨ s’來產生新的狀態。具體範例是,沿著路徑走過的每一步都會使代理到達路徑的新位置,也就是新的狀態。因此我們希望代理不僅要學習採取行動優化目前可能的獎勵,還要採取行動向可能提供更多獎勵的狀態移動。

貝爾曼更新

雖然新增的兩個複雜元素初看起來並不相關,但確是直接相關聯的。兩個元素都隱含了影響代理的未來最終狀態及其可能獲得的獎勵。通過這種質樸的理解,我們可以利用這種關係來學習在這些情形下應採取的最優行為。即在一個“真正”最優的Q-函數(只存在於理論)中,目當狀態的估值和行為可以被分解為直接獎勵r加上在下一狀態中代理採取行動的最大預期獎勵乘以一個折扣係數:

 

 這就是貝爾曼方程(Bellman Equation)也被稱作動態規劃方程式(Dynamic Programming Equation),由理查·貝爾曼(Richard Bellman)發現,可以按如下形式表述:




此處的𝛄 (gamma)是一個折扣因數,決定了代理對未來可能獲得獎勵的關注程度。如果值為1.0,則代理對未來所有的獎勵一視同仁,在永無止境的訓練中,估值可能會增加到無窮大。為此,𝛄應在0到1之間取值,典型取值範圍是0.7-0.99。

貝爾曼方程有效的原因在於:它提供了一種自動更新Q-函數的方法。Q*(s, a)是一個優化Q-函數。但即使當前的下一狀態的次優Q-value估值也有助於讓當前狀態的估值更加精確。由於每一步都主要依賴於真正的獎勵,所以可以相信Q-value估值自身會緩慢改進,並不斷趨近真實值。我們可以使用貝爾曼方程進行新的Q-Learning更新:



這個方程和之前情境老虎機中使用的更新演算法類似,不同點是Q-target現在包含下一步中未來期望獎勵的折扣值。


探索

為了讓代理更充分地探索狀態空間,我們使用了一種名為epsilon-greedy的探索更新形式。使用時將epsilon-greedy中ϵ初值設置為1.0,每次代理採取行動將該值減少一部分,而當代理選擇行動時,可選最貪婪的行動argmax(Q(s, a)),或者採用概率為ϵ的隨機行動(在搜索初期增加隨機性,提高搜索範圍空間,隨著代理積累經驗,逐步降低隨機影響)。直覺就是訓練開始時,代理的Q-value 估值與真實值相差較大,但通過對整個世界的學習後,ϵ值減少,Q-函數就慢慢與真實環境的Q-函數更一致,而使用Q函數採取行動的精確性也不斷提高。

Unity GridWorld


藍色的方塊是代理,紅色是障礙物,綠色是目標位置,綠色和紅色的球體代表GridWorld中每種狀態的估值。


為了演示Q-Learning代理,我們使用Unity建立了一個簡單的GridWorld環境。其中包含:
  1. 一個在世界中隨機位置出現的代理;
  2. 一個隨機的目標位置,我們希望代理通過學習向目標移動;
  3. 隨機放置的障礙物,我們希望代理學會躲避;

環境狀態(s)是一個整數,代表網格位置,四個行為(a)包括上、下、左、右,獎勵(r)規則為:移動到帶有目標的狀態時+1,移動到帶有障礙物的狀態時-1,為了鼓勵代理快速移動到目標,每次移動時-0.05。每個階段會在移動100次後,或者在代理到達既非目標又非障礙物的狀態時(碰到障礙時)結束。代理的Q-value存儲在表中,行代表狀態,列代表可採取的行為。

你可以點擊這裡直接從網頁看結果,也可以點擊這裡下載這個Unity專案,然後自己修改並應用於自己的遊戲。隨著代理不斷探索環境,不同顏色的球將會出現在網格世界的每個狀態中,這對應當前狀態下代理的平均Q-value估值,一旦代理完成優化策略的學習,這些球就會形成從開始到目標位置的直接值漸變。

後續計畫

此處的代理和環境代表了Q-Learning問題的經典表格,如果不符合你的需求也不用太擔心,Q-Learning演算法從90年代被提出至今,它已經歷了一系列重大的改進,Q學習可被應用於更加多變的動態場景中。Deepmind的Deep Q網路的一個主要示例,就是用於學習直接從圖元中進行幾十種不同的ATARI遊戲,像這裡僅用一個查閱資料表根本不可能實現的壯舉。為了實現這個目標,他們用到了一個由Deep Neural Network(DNN)網路控制的代理。通過神經網路,它可以學習將廣義Q-函數應用于完全不可見的狀態,例如顯示器上少見的圖元組合。

在後面的幾周中,我們會公佈一系列演算法的介面和展示專案,可以在Unity遊戲和模擬應用中訓練類似的深度強化學習代理。您可以查看影片,先簡要瞭解這些工具的功能。初次發佈版本將有些限制,主要用於研究、工業以及遊戲的QA測試。我們非常樂於看到在Unity中利用現代深度學習方法來學習遊戲行為,希望當此項技術成熟時,可以引爆機器學習技術在遊戲開發中的潛力,例如控制遊戲中複雜的NPC行為與遊戲動態等等。我們尚處於探索深度學習技術應用于遊戲開發的初期階段,也希望大家陪伴我們一起踏入後續的旅程。


2017年9月1日 星期五

遊戲開發中的AI - 入門

作者:Arthur Juliani 原文
潤稿:Kelvin Lo


Unity正朝著AI的方向潛心探索著。本文是Unity發佈的首篇AI相關文章,為大家介紹AI的一些概念及術語,詳細介紹機器學習相關理論與方法,並講解在使用Unity開發遊戲的過程中如何應用AI。未來我們還將為大家分享更多AI的內容。在過去幾年,Machine Learning(機器學習,ML)的進步,在檢查物件、翻譯、辨識語言,甚至玩遊戲等等都有突破性發展。值得一提的是,ML和玩遊戲之間的關係在Unity上很像是我們人類的心態。我們相信繼續鑽研能在製作遊戲這塊有突破性的發展,從改變貼圖和3D建模、動畫、人物動作或場景照明的製作方式,到自動產生NPC(非玩家角色)的程式碼。


這篇文章適合的讀者

我們想和Unity的開發者們一起探討關於AI和ML在遊戲開發過程中能發揮的力量,同時也想討論AI用在美術製程的可能性。藉由這次機會剛好可以對ML研究者展現以Unity作為AI研究/開發平台的潛力。像是機器人和自動駕駛等等的模擬平台,讓有興趣的人或研究學生透入研究Unity和ML。


何謂ML(機器學習)

首先來介紹下Machine Learning(機器學習,下文簡稱ML)和遊戲Artificial Intelligence(人工智慧,下文簡稱AI)間的關係。現在大部分遊戲裡的AI都是手動硬寫的,由大量判斷式組成,有時會包含多達數千條規則。而且必須由人工維護和測試。而ML所依賴的演算法可以自動從原始資料尋找規律,無需專業人員預先定義資料的解讀邏輯。

以圖片內容分類這個電腦視覺問題為例。直到幾年前,科學家們仍採用人工編寫篩檢程式,辨識圖像上的特徵,用來分辨某個圖像中包含的是貓還是狗。而ML,特別是最新的深度學習方法,只需圖像和類型標籤,就可以自動學習有用特徵。我們相信這種自動化學習不僅可以拓展Unity平台的應用範圍,例如用於ML場景模擬,還可以幫助所有開發者簡化和加速遊戲的開發過程。

這種自動化學習尤其可以應用在非玩家角色(NPC)的行為。我們可以使用Reinforcement Learning(增強學習,簡稱RL ) 來訓練NPC,預估某一環境中施行特定行為的價值。一旦訓練完成,NPC即可以最佳行為模式做出反應,不用程式對它們的行為進行篩選。後面的文章我們會介紹RL的應用,所有的範例專案也都會公開放在Github上面,你也可以透過WebGL看看整個專案展示。

用RL演算法學習的吃角子老虎

RL背後的一個核心概念是價值估計,並依照此進行對應動作。在繼續深入之前,最好先瞭解一些術語。

在RL裡執行動作的個體稱為agent(代理),它使用policy(策略)進行動作決策。一個代理通常嵌於一個environment(環境)中,並在任意給定的時刻都處於某個特定的state(狀態)。從那個狀態,它可以進行一系列actions(動作)。某個給定狀態的value(值)指的是處於該狀態的最終回報價值。在某個狀態執行一個動作可以讓代理進入另一個新的狀態,獲得一個reward(回報),或者同時擁有兩者。所有RL代理都在盡可能最大化累計回報。



這個RL 範例專案有個問題,技術上稱為multi-armed bandit(或稱為N-armed bandit problem)。這名字源自於為了平衡多台老虎機的回報產生最優化的結果而生,也稱作"Single-arm bandits",設計用來一點一滴的從玩家身上回收錢。在這樣的設定下,環境只包含一種狀態,代理能採取n個動作中的一個。每個動作都會立即為代理提供一個回報。而代理的目地是找出能提供最多回報的動作。

為了更好理解,可以想成一個迷宮遊戲。代理進入一個房間,發現牆邊放著一排寶箱。每個寶箱都有一定的機率開到一顆鑽石(回報+1)或一個敵人惡魂(回報-1)。

代理的目標就是學習思考哪個寶箱最有可能出鑽石。(比如判斷結果為從右屬過來第三個箱子)。要找出回報最高的寶箱最簡單的方法就是逐一嘗試。在代理獲得足夠的資訊並採取最佳行動之前,大部分RL的工作就是簡單的不斷嘗試錯誤。上面這個例子用RL的術語來描述就是,"嘗試"對應的是採取一系列動作(多次打開每個寶箱),學習對應的是更新每個動作的估計值。一旦有了足夠的樣本,就可以讓代理總是選擇那個具有最高估計值的寶箱。

三個寶箱,每個都有機率開出鑽石或敵人



這些估計值可以透過一個反覆運算過程獲得。這個過程從最初的一系列估計V(a)開始,並根據每次動作的結果進行調整。運算式如下:


α對應我們的學習率,V(a)是給定動作的價值估計,r是採取動作後馬上可以獲得的回報。


上面的算式很直覺,它表示出我們將當下的價值估計向獲得回報的方向做了一些微調。這樣就能確保變化的估計值能更好的反應環境中的真實動態。如此一來,還能確保估計不會變得過大,如果我們只計算正向結果就可能會發生這種情況。要完成相應程式,我們可以使用一組值估計向量,並透過代理動作對應的索引來引用它們。



三個寶箱上方顯示的是代理的估計值(綠色球體),以及真實的概率值(中間的半透明球體)。在這個例子中,三個寶箱的潛在正向回報概率分別是 10%(左),20%(中),80%(右)。隨著訓練進行,代理的估計值會變得越來越精確。



情境吃角子老虎(Contextual Bandits)

上述所描述的情況缺少了真實環境中有的一個要素,它只有一個狀態。在現實或遊戲世界裡,一個環境可能會處於幾十(房子裡的房間)到數十億(一個螢幕上的圖元)種可能狀態中的一種。每個狀態都有它們自己獨特的動態特性,即動作如何提供新的回報或者允許狀態間的轉移。

因此,我們需要對動作與估計值以及狀態設定條件。用符號表示,現在將用Q(s,a)取代V(a)。它的抽象意思是,現在期望獲得的回報,是我們所採取的動作以及採取該動作時所處狀態的一個函數。

在迷宮遊戲中,狀態的概念使我們可以在不同的房間中擁有不同的寶箱組。每個房間可以有不同的最理想寶箱,因此,代理需要學習在不同房間中採取不同的動作。用程式來實現的話就是使用一個價值估計的matrix來取代一個簡單的array。這個matrix可以用[state, action]來索引。



探索(Exploring)和利用(Exploiting)

讓RL能運作還需要一個重要的因素。在代理學得採用最佳回報動作的策略之前,需要有另一個策略使它可以充分瞭解世界,以確保它知道什麼是最佳策略。這會引出了一個經典問題,如何平衡探索(exploration)(通過不斷試錯學習環境的價值結構)和利用(exploitation )(基於環境習得的價值結構採取動作)。雖然有時這兩個目標會一致,但大多數狀況它們都會相左。有一系列策略可以平衡這兩個目標。下面列出了其中一部分:

  • 一個簡單卻強大的策略是遵循“optimism in the face of uncertainty”的原則。它的做法是,代理每個動作都從高值估計V(a)開始,這樣它就會貪婪地(利用最大值)採取動作,使它將每個動作都至少執行一次。
  • 如果動作沒有獲得好的回報,就降低對應的價值估計,反之則保持高價值估計,因為該操作可以作為後續好操作的候選。但是僅靠這種自身啟發式演算法通常仍然不夠,因為我們可能需要持續探索某個特定狀態以找到一個低頻率,卻有巨大的回報。
  • 另一個策略是為每個動作的價值估計添加隨機亂數,隨後根據新的亂數估計進行貪婪行為(act greedily)。使用這種方式,只要亂數值小於真實最優動作與其他動作間的差異值,就能收斂至最優的價值估計。
  • 也可以進一步利用估計值本身的特性,將它們進行歸一化(Normalize),依據概率採取動作。這種情況下,如果每個動作的價值估計大致相等,將會以相同概率採取動作。反之,如果一個動作的價值估計要大得多,我們將會更多的選擇這個動作。這樣就能通過遞減選擇無回報動作,慢慢將它們淘汰。這也是在範例專案中所採用的策略。



結語

有了這篇文章以及相關程式碼,你現在也可以嘗試在Unity中使用文章描述的演算法了。但這僅僅是入門而已,在後面的文章我們將通過Q-Learning來講解一個完整的RL問題,並以此為基礎,開始探討深度神經網路,解決視覺豐富遊戲環境中,愈趨複雜的代理行為的學習策略問題。使用這些進階的方法來訓練代理成為我們遊戲中的同伴或對手,適用的遊戲類型可以是格鬥遊戲、賽車、第一人稱射擊,或甚至即時策略遊戲等等。不需要編寫任何規則,只需專注於你希望代理達成的狀態,而非它如何達成的過程。

後續我們還會提供一些工具的早期版本,讓對使用Unity進行深度RL研究感興趣的開發者,可以將諸如Tensorflow或PyTorch等框架編寫的模型連接到Unity製作的環境中。一起探索未來可能是新的遊戲製作方式領域!

2017年8月20日 星期日

Unity告別UnityScript,未來將只支援C#

作者:Richard Fine 原文
潤稿:Kelvin Lo


UnityScript(Unity中客製的JavaScript),從Unity 1.0開始一直伴隨著我們至今。正所謂天下無不散之宴席,終於要向它告別。我們已經開始逐步棄用UnityScript,未來只會保留C#作為Unity程式語言。

目前只有3.6%的Unity專案使用UnityScript進行開發,代表維護UnityScript將會影響Unity對新程式語言功能的支援進度,所以我們決定將棄用UnityScript。


棄用UnityScript的原因

每次Unity決定放棄支援一些功能,都會先瞭解這些決定可能會為開發者帶來的不便。所以我們必須確保這些決定有其進行的理由,這對我們很重要。Unity腳本程式設計正在經歷重大升級,其中一些重要功能包括:
  • 核心執行版本升級,支援使用.NET 4.6與C# 6
  • JobSystem,支援編寫多執行緒程式,且避免互相競爭與鎖死
  • NativeArray類型,支援建立及使用大型陣列,它們擁有原生程式控制的存儲區域,以便於對記憶體分配擁有更多控制權,不必再擔心GC回收機制
  • 支援控制腳本編譯,以便自訂將哪些腳本整合進程式集

這些只是一小部分,我們還將繼續支援一些新功能,並計畫開啟一些腳本專案。除了這些專案之外,我們也正利用最為合適的語言結構,來開放更多引擎底層API。

目前UnityScript與C#在功能與效能不相上下,當然C#可以實現的內容UnityScript也能達到同樣的效果。但說到開發生態,顯然C#是贏家,不僅僅是因為C#教學與範例網路上到處都是,C#語言也有豐富的工具,例如Visual Studio提供的代碼重構與智慧提示。或許你會質疑Unity Script可以用就好了,也不需要那麼多花俏的工具。

技術永遠是不斷發展的,隨著我們升級執行核心支援最新的C#版本時,就會出現一些C#簡單實現但UnityScript無法輕易實現或者不支援的功能。比如說目前UnityScript就不支援為方法參數(method parameters)提供預設值,並且C#語言支援的功能越來越多,例如ref return等都會造成同樣問題。目前我們暫未在API中使用這些功能,但出於效能與API結構設計考慮,我們也希望趕快讓這些新功能上線。

或者我們也可以選擇花時間來彌補UnityScript所欠缺的功能,但時間成本昂貴,把時間花在維護UnityScript就代表無法進行其它工作,好比加新功能或修Bug等。這還不包括在執行核心中支援UnityScript需要的工作,以及文件製作更新工作等等。

所以我們換了個方向思考,如果棄用UnityScript,會有多少開發者被影響呢?我們統計結果如下:

  • 截止目前使用Unity 5.6的專案中,包含至少一個.js檔的專案占14.6%。這個比例看起來相當高,但進一步分析資料,看看各專案中.js檔案與總檔案數(.js檔案 + .cs檔案)占比,會有新發現。
  • 也就是說,85.4%的Unity專案完全使用C#語言,根本不包含任何UnityScript腳本。
  • 9.5%的Unity專案大量使用C#語言,其中也包含一些UnityScript腳本,但腳本數量占腳本總數不足10%。還有1.5%的Unity專案所包含的UnityScript腳本占專案腳本總數在10~20%之間。
  • 3.6%的Unity專案使用UnityScript語言的腳本占腳本總數20%以上。
  • 僅有0.8%的Unity專案完全使用UnityScript。

這些資料顯示,大多數的Unity開發者都不是UnityScript的重度使用者。甚至專案所包含的UnityScript也並非實際使用的腳本,可能只是Asset Store某個套件的範例程式,對實際專案並無影響。所以我們棄用UnityScript計畫的第一步,就是從Asset Store開發者著手,先移除所有發佈套件裡的UnityScript腳本。

對於那3.6%使用較多UnityScript的Unity專案,以及那0.8%完全使用UnityScript的專案,我們表達深切的歉意。我們明白這樣的決定會對你產生影響,我們正製作一些轉換措施來嘗試讓整個轉換過程更加流暢,也希望大家能夠理解並支持我們所做出的決定。

棄用計畫

當然我們會一步一步捨棄UnityScript,而不是一刀切斷。主要計畫步驟如下:

首先,從6月開始我們已經修訂了Asset Store的套件審核條款,拒絕接受程式有包含UnityScript代碼的套件。所有新送審的套件都必須使用C#程式(在執行此項規定前我們已與開發者有過許多討論與溝通)。很快我們將會對Asset Store現有的套件進行檢查,如果有UnityScript則會通知發行商將程式碼轉為C#。如果一段時間後程式沒轉換為C#,套件將會從Asset Store下架。

再來,可能你已經注意到了,Unity 2017.2測試版的Create Assets選單下已經找不到Javascript(即UnityScript)。目前我們只移除了功能表上的建立功能,Unity編輯器仍然支援UnityScript,您仍然可以從編輯器之外創建UnityScript檔(例如MonoDevelop)。這麼做的原因是想確保新手開發者不會選用UnityScript,以免浪費學習成本。

另外,我們正在開發UnityScript自動轉換為C#的工具。目前該工具已有些成果,但還沒到我們滿意的地步。我們也還沒決定是否將工具整合到Unity編輯器或單獨開源提供。無論以何種方式,這個工具都會在今年年底Unity 2017.2正式發佈時提供大家使用。後續也會單獨介紹這個工具,請大家保持關注。

最後,我們也會繼續分析資料,我們希望能看到Unity專案都可以儘快切換至C#,尤其是那些UnityScript腳本數量少於10%的那些專案,因為需要移植的程式相對較少。但如果分析結果轉換未達預期,我們會暫停計畫並調查原因。在徹底棄用UnityScript之前,我們將確保不遺漏任何重要資訊。

一旦確認UnityScript使用率到一個低點之後,我們就會剝離UnityScript編譯器,不再將.js檔識別為腳本。也會從文檔中移除UnityScript程式範例,腳本更新器也將不再支援UnityScript。

如果有單獨需求,開發者仍可從Unity的GitHub中下載UnityScript編譯器,我們不會接受任何推送請求,但您可以建立自己的分支實現你的需求。


關於Boo

在歷史紀錄上Unity曾在2014年宣佈放棄支援Boo語言。但Boo編譯器仍存在現在的編輯器中,只因為UnityScript會用到了Boo的執行庫,並且UnityScript編譯器本身就是用Boo語言編寫的。所以雖然我們沒提但你仍可在Unity專案中使用.boo文件。

但是剝離UnityScript支援之後,就代表Boo編譯器也會被移除。目前所有使用Unity 5.6的專案中僅有0.2%的專案包含了.boo檔,僅有0.006%的專案擁有3個以上.boo檔案。


結論

希望本篇文章有清楚解釋了棄用UnityScript將會為大家帶來的影響,我們會依照流程:通知大家我們的計畫->推動Asset Store套件與編輯器UI更新,最後依照實際使用率的分析資料來決定是否執行。

放棄某個功能看起來像是退步,但這也是提高Unity開發效率的必經之路。我們希望集中精力為大家儘快修復現有的問題並製作新功能,也希望大家能夠理解並支持我們的決定。

2017年8月7日 星期一

最佳實踐 - Unity碰撞效能優化

作者:William Armstrong 原文
潤稿:Kelvin Lo

Unity有一個名為Spotlight的團隊,有一群優秀的Unity開發人員一起研究並試著不斷的打破Unity的極限。針對各種複雜圖形效能和設計問題,我們不斷嘗試各種新的解決方案。

這個系列的文章會探討我們在和客戶合作時遇到的一些常見的問題。這些都是我們的合作團隊辛苦得出的經驗和教訓,我們很榮幸能夠和大家分享這些智慧。

這些問題很多只會出現在發佈到主機遊戲、手機遊戲、或者處理大量遊戲內容時才會出現。如果能在開發早期就將這些問題考慮進去,那麼開發過程就會更輕鬆,而遊戲也會做得更好。

較大的問題

有時候我們追蹤某個物理效能問題時,會歸咎到某一個問題的物件資源或設定上。常常查看Profiler並和前次執行結果比對,是發現這些問題的最佳方式。如果能儘早發現效能的減退,就可以透過查看最近所做的變更找到問題所在。

雖然運算單一簡單的物理
節(physics joint)速度很快,但背後的運算卻很複雜。本質上一個關節就是一套由剛體的位置、速度、加速度、旋轉等資訊組成的方程式。如果建立了一個帶有許多不同剛體的物件,而這些剛體又各自包含了許多會互相碰撞的關節。為了不讓這些關節不會互相穿透,要滿足這些關節的碰撞運算代價會非常大。

所以設計這種複雜關節時,要慎重考慮所需關節數量,碰撞類型以及必要的剛體數量。你可以用圖層(Layer)來過濾不必要的碰撞,也要謹慎評估哪些關節要打開"Allow Collision"來計算碰撞。透過限制關節的活動範圍可以減少碰撞檢測的量。或調整關節使它和其他關節不會發生碰撞。透過將關節和剛體作為插值方法的控制點來減少它們的使用數量。

使用Profiler可以看到某個特定時間啟動的剛體總數。觀察Rigidbody count的值
,尤其是當剛體互相接近時,會對效能產生很大的影響。在執行時放置或產生物件時,這個數字很容易會膨脹到超出預期。這就好比一罐汽水罐的碰撞很容易做,但如果罐子做成Prefab疊成超市裡的飲料塔,碰撞計算就很容易出大問題。


採用MeshCollider要非常小心,通常為了方便,開發者很容易會拿模型網格直接作為碰撞網格,但這可能會引起嚴重的效能下降,而且還不易察覺。PhsyX你給他甚麼他就算甚麼,所以如果一個小物體身上有高模或不合身的碰撞體,這時可能還好,但是如果將MeshCollider放大到容易被RayCast偵測運算的環境中,你會發現效能會驟然下降。如果非要用MeshCollider,建議是對於所有可能會有碰撞運算的物體,另外製作一組低模的碰撞網格。如果網格有問題又沒時間製作自訂網格,可以將MeshCollider設定裡的Convex打勾,並調整SkinWidth來取得一個比較適合的低面碰撞網格。

擁有多個重疊碰撞器的影子戰術NPC

較小的問題

通常開發者都會明智的避開不做大或慢的事情。但很多常見的情況是,專案所做的一系列理性決策,每個都一點一滴無形的影響了PhysX的效能。尤其是製作大規模遊戲時很容易出現這種狀況。例如你用5個方塊做關卡AI測試時表現很OK。但相同的AI放到實際的關卡時,大量的Physics.Update運算造成效能不好,而你卻不知道問題在哪? 


"明明測試都很OK的,為甚麼上正式版就壞了?" -- by 工程師


你有做足夠的測試了嗎?
可以從ProjectSettings->Physics裡找到碰撞圖層過濾(Layer Collision Matrix)來檢查是否存在不必要的碰撞規劃。透過打勾/取消打勾那些方塊,可以控制碰撞體之間的碰撞行為。這個功能會在碰撞發生前先進行計算,進而避開不必要的碰撞計算。許多遊戲使用”Is Trigger”為true的大型碰撞器來檢測角色或其他物件,這通常被稱為觸發器。通常這些觸發器會設定和預設圖層或所有物件發生碰撞計算。透過將角色放到特定層,並將這些觸發器放到某個只會與角色層發生碰撞計算的層,可以避免大體積碰撞器與複雜的世界網格或地形網格發生碰撞計算。

觸發器(Trigger)是否是使所有物件慢下來的原因?
當碰撞器屬性的“Is Trigger”設為true,它依然是一個需要進行碰撞計算的碰撞器。因此移動一個觸發器和移動一個碰撞器所產生的消耗都是一樣的,會需要做很多工作來發送碰撞和重疊事件。如果要在每幀都移動觸發器,確保它只對必須的物件進行碰撞計算。盡可能將觸發器做小,並將相似或重疊的觸發器分組。

我們常見到專案在NPC上使用多個大型觸發器用來檢查是否有互動。NPC的每種類型的遊戲物件都會有自己的碰撞器,以及一堆放在OnCollision中的程式,用來找到正確的互動物件。通常將多個觸發器合併一起會比較快,然後在OnCollision函式中用Tag或Layer或距離來進行過濾。很多情況下可以透過完全繞過碰撞計算來獲得更好的效能。而不必每幀都對球體與世界的碰撞進行計算,而是將所有希望存取的物件註冊到一個共用管理器,然後讓NPC對所有已註冊物件做簡單的距離計算。如果有一小堆需要檢測的潛在目標。這種方法要比對世界中所有的碰撞做檢測效能要高的多。

物件在Hierarchy裡的結構會導致PhysX做了額外的工作嗎?
在《ReCore》這個遊戲裡,我們發現場景中圓形旋轉平臺上的每一個平台都擁有自己的剛體。這導致了每個平台都會與其他平台發生碰撞檢測。將所有的平臺歸於同一父物件之下,將剛體賦予父物件,讓父物件進行旋轉,在Physics.Update中節省了大量的時間。
注意:將一個共用剛體下的碰撞器合併會增加Raycast對它或形狀投射檢測的開銷。

從另一個方面來說,如果一個共用的剛體父物件下已經有幾個碰撞器,你需要非常小心,不要讓它們做相對於父物件的移動。因為任何時候剛體改變形狀,會造成中心的質量和慣性張量(Inertial Tensor)
都必須重新計算,這需要大量的時間。當多個剛體連接到動畫角色的四肢時,常常會出現這種問題。可以通過設定自己的質心來關閉它。只要遊戲物件的形狀不要變化太大就沒問題。

這堆小小的系統問題累積起來是很可怕的,所以在開發中要時刻牢記,並在一開始就做好計畫規避這些問題。如果突然發現Physics時間出現一個巨大峰值,那就分析下當下的修改有哪些並思考對應方式。

最後感謝Mimimi Productions和Armature Studio 讓我們用他們的遊戲作為例子。我們還會為大家分享更多最佳實踐系列文章。

2017年7月31日 星期一

Unity 5 Shader系統程式介紹

Unity在Shader(著色器)開發方面提供了很大的靈活性。有些工具需要你編寫一個“合適”的自訂著色器(合適意思就是無法在節點編輯器裡完成,必須要寫程式),相較來說麻煩程度可算是相當輕量。不過,完全基於Deferred渲染器,代表我們無法像在Forward著色器中那樣可以放開手腳蠻幹,必須受限於g-buffer中包含的那些無法進行更改的資訊。

所以說如果你對Unity 5的標準著色器滿意,想拿來加點東西,比如一個額外的著色器屬性或修改某些功能,或重新製作你自己的著色器系統,需要牽涉到陰影、全域光照、光照貼圖、直接光照等等。

最主要的問題是,無法對標準著色器進行編輯。你無法直接修改標準著色器的程式。你得先下載你所安裝的Unity版本的對應著色器原始碼。

如何獲得標準著色器程式碼

具體做法是確認你所安裝的Unity 5版本,然後訪問Unity Download Archive網頁,在下拉清單中選擇平台(Win/Mac)的“Built in Shaders”,就可以下載zip檔。

概述

Zip檔中包含了標準著色器的完整原始碼,包括特殊的視圖UI以及它包含的所有內容共四個資料夾:
  • CGIncludes
  • DefaultResources
  • DefaultResourcesExtra
  • Editor

Editor僅包含實現標準著色器檢視視圖UI的.cs文件。
CGIncludes裡面包含了所有其他著色器所需要的函數。我們會仔細研究它們,因為我們將會用到那些函數。
DefaultResources和DefaultResourcesExtra 包含了許多適用於不同情況的著色器。

下面來學習如何解讀標準著色器,然後依次查看各個子系統,直接光照、陰影、全域光照等等。本文以Unity 5.4版本為例,建議大家採用Unity 5.3或以上版本,因為新版Unity將光照模型(BRDF)從Phong改為了GGX。Phong簡單快速,各向同性,但是表現能力有限;GGX較為複雜,支援各向異性,並且有接近現實世界的高光效果,表現能力強。這是一個很大的改進,使我們可以製作更多有趣的範例。

追蹤pragma

回到著色器程式碼。從一個簡單的標準著色器開始:DefaultResourcesExtra\Standard.shader。

打開這個檔案後會發現,它是一個Surface著色器,包含一個Properties部分以及不同的著色器pass。它針對
DeferredForward渲染器還有不同的pass。

讓我們分析
Forward渲染器(Forward渲染器有兩個Pass,Base Pass針對第一個光源,另一個Add Pass針對所有其他光源)的Base Pass:


[C#]

// ------------------------------------------------------------------
        //  Base forward pass (directional light, emission, lightmaps, ...)
        Pass
        {
            Name "FORWARD"
            Tags { "LightMode" = "ForwardBase" }
            Blend [_SrcBlend] [_DstBlend]
            ZWrite [_ZWrite]
            CGPROGRAM
            #pragma target 3.0
            // -------------------------------------
            #pragma shader_feature _NORMALMAP
            #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
            #pragma shader_feature _EMISSION
            #pragma shader_feature _METALLICGLOSSMAP
            #pragma shader_feature ___ _DETAIL_MULX2
            #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
            #pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF
            #pragma shader_feature _ _GLOSSYREFLECTIONS_OFF
            #pragma shader_feature _PARALLAXMAP
            #pragma multi_compile_fwdbase
            #pragma multi_compile_fog
            #pragma vertex vertBase
            #pragma fragment fragBase
            #include "UnityStandardCoreForward.cginc"
            ENDCG
        }



如你所見,它基本上由一堆pragma和定義組成。在ubershader樣式中,那些pragma啟動了在不同包含檔中的程式碼片段。因此要瞭解這個pass中實際發生的事情,必須打開CGIncludes\UnityStandardCoreForward.cginc,並逐一查看每個程式碼片段中的每個pragma。這個過程太長,需要太多解釋很多,所以現在還是讓我們專注於尋找基本函數,即主要的光照計算過程發生的地方。

CGIncludes\UnityStandardCoreForward.cginc的作用僅僅是將在其他cginclude中包含的東西連在一起。在這裡,它負責根據定義UNITY_STANDARD_SIMPLE,設定好要使用的頂點與片段函數。

下面看看更簡單的那個CGIncludes\UnityStandardCoreForwardSimple.cginc。它很“簡單”,因為它不支持PARALLAXMAP、DIRLIGHTMAPCOMBINED、DIRLIGHTMAP_SEPARATE,因此解讀起來也相對簡單。

基礎函數與結構體

最後,這個檔裡還有一些函數與結構體,基礎的有以下這些:
  • struct VertexOutputBaseSimple,這個資料結構用於保存從頂點著色器向片段著色器傳送的資料。
  • vertForwardBaseSimple 是在每個頂點上都會執行的函數,填充 VertexOutputBaseSimple結構體。
  • fragForwardBaseSimpleInternal,正向渲染器中,接受頂點輸出結構體,並計算第一個光源的函數。

片段函數

它回傳一個向量,其中包含四個half精度浮點數(一個顏色和透明度),它接受一個VertexOutputBaseSimple結構體:

[C#]

half4 fragForwardBaseSimpleInternal (VertexOutputBaseSimple i) 
{
    FragmentCommonData s = FragmentSetupSimple(i);
    UnityLight mainLight = MainLightSimple(i, s);  
    half atten = SHADOW_ATTENUATION(i);
    half occlusion = Occlusion(i.tex.xy);
    half rl = dot(REFLECTVEC_FOR_SPECULAR(i, s), LightDirForSpecular(i, mainLight));
    UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);
    half3 attenuatedLightColor = gi.light.color * mainLight.ndotl;
    half3 c = BRDF3_Indirect(s.diffColor, s.specColor, gi.indirect, PerVertexGrazingTerm(i, s), PerVertexFresnelTerm(i));
    c += BRDF3DirectSimple(s.diffColor, s.specColor, s.oneMinusRoughness, rl) * attenuatedLightColor;
    c += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);
    c += Emission(i.tex.xy);
    UNITY_APPLY_FOG(i.fogCoord, c);
    return OutputForward (half4(c, 1), s.alpha);
}


從程式碼中可以看到,它收集了所需的資訊,並對直接與間接光的貢獻、霧、衰減、自發光和遮蔽進行了計算。

這些過程已被封裝,因此我們需要依次查看這些函數,才能瞭解它們的實際用途,以及程式碼的具體作用。

追蹤更多的函數

對光源進行實際計算的函數並不在此檔中,部分在CGIncludes/UnityStandardCore.cginc中:
  • MainLight (實際上用於 UnityStandardCoreForward.cginc中的MainLightSimple)

[C#]

UnityLight MainLightSimple(VertexOutputBaseSimple i, FragmentCommonData s) 
{
    UnityLight mainLight = MainLight(s.normalWorld);
    #if defined(LIGHTMAP_OFF) && defined(_NORMALMAP)
        mainLight.ndotl = LambertTerm(s.tangentSpaceNormal, i.tangentSpaceLightDir);
    #endif
    return mainLight;
}



我們能看到那裡對LambertTerm進行了計算,但僅在光照貼圖關閉且法線貼圖打開時會這樣。

CGIncludes/UnityStandardBRDF.cginc:

  • BRDF3DirectSimple (使用BRDF3Direct)


[C#]

half3 BRDF3_Direct(half3 diffColor, half3 specColor, half rlPow4, half oneMinusRoughness) 
{
    half LUT_RANGE = 16.0; // must match range in NHxRoughness() function in GeneratedTextures.cpp
    // Lookup texture to save instructions
    half specular = tex2D(unity_NHxRoughness, half2(rlPow4, 1-oneMinusRoughness)).UNITY_ATTEN_CHANNEL * LUT_RANGE;
#if defined(_SPECULARHIGHLIGHTS_OFF)
    specular = 0.0;
#endif


它看起來在使用查表法計算鏡面反射的貢獻。

  • LambertTerm, 導向到DotClamped


[C#]

inline half DotClamped (half3 a, half3 b) 
{
    #if (SHADER_TARGET < 30 || defined(SHADER_API_PS3))
        return saturate(dot(a, b));
    #else
        return max(0.0h, dot(a, b));
    #endif
}



從MainLightSimple我們得知,傳入的參數是N和L。所以片段函數首先設定好片段,計算主光源ndotl、衰減、遮蔽、全域光照以及燈光顏色。然後計算最終光線的所有貢獻,並將直接、間接、全域光照加總後再計算霧。

如你所見,著色器在光照計算方面相當少,只使用了一個Lambert演算法,並查了一下表。它不像基於物理的著色器使用的那麼多,這很可能是最精簡的標準著色器版本了。



重新回到Standard.shader,這次在UnityStandardCoreForward.shader中,我們選擇另一個“不簡單”的那個分支。它將我們引向UnityStandardCore.shader,而我們感興趣的是fragForwardBaseInternal函數。


[C#]

half4 fragForwardBaseInternal (VertexOutputForwardBase i) 
{
    FRAGMENT_SETUP(s)
#if UNITY_OPTIMIZE_TEXCUBELOD
    s.reflUVW       = i.reflUVW;
#endif
 
    UnityLight mainLight = MainLight (s.normalWorld);
    half atten = SHADOW_ATTENUATION(i);
 
 
    half occlusion = Occlusion(i.tex.xy);
    UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);
 
    half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);
    c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);
    c.rgb += Emission(i.tex.xy);
 
    UNITY_APPLY_FOG(i.fogCoord, c.rgb);
    return OutputForward (c, s.alpha);
}


拿來做參考的簡單版本:


[C#]

half3 c = BRDF3_Indirect(s.diffColor, s.specColor, gi.indirect, PerVertexGrazingTerm(i, s), PerVertexFresnelTerm(i)); 
    c += BRDF3DirectSimple(s.diffColor, s.specColor, s.oneMinusRoughness, rl) * attenuatedLightColor;
    c += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);


與上一節中的版本不同,最終的顏色由對UNITY_BRDF_PBS、UNITY_BRDF_GI和Emission的呼叫結果相加得出。

Emission與簡單版本中的相同。UNITY_BRDF_PBS和UNITY_BRDF_GI是包含檔中定義的函式別名。在下面這些包含檔中進行查找:


[C#]

#include "UnityCG.cginc"
#include "UnityShaderVariables.cginc"
#include "UnityInstancing.cginc"
#include "UnityStandardConfig.cginc"
#include "UnityStandardInput.cginc"
#include "UnityPBSLighting.cginc"
#include "UnityStandardUtils.cginc"
#include "UnityStandardBRDF.cginc"
 
#include "AutoLight.cginc"


UnityStandardBRDF和UnityPBSLighting看起來最像,所以先查看它們。它們就在UnityPBSLighting.cginc中,不同的著色器目標會選擇不同的函式。

選擇BRDF1_Unity_PBS,它就在UnityStandardBRDF.cginc中,它看起來是最逼真的可用BRDF,而BRDF3_Unity_PBS則是消耗最低的版本。

如你所見,這是個大函式,因此跳過一些與優化相關的細節,依次逐塊的進行講解,首先從這個非常有用的注釋開始:



[C#]

// Main Physically Based BRDF
// Derived from Disney work and based on Torrance-Sparrow micro-facet model
//
//   BRDF = kD / pi + kS * (D * V * F) / 4
//   I = BRDF * NdotL
//
// * NDF (depending on UNITY_BRDF_GGX):
//  a) Normalized BlinnPhong
//  b) GGX
// * Smith for Visiblity term
// * Schlick approximation for Fresnel


注解給出了使用的公式,以及引用與作用。NDF(法線分佈函數)有多個選擇,但這裡僅介紹GGX,因為我覺得它更好。

下面對注解中的公式進行簡單的介紹:

  • kD: 漫反射率
  • pi: π常量
  • kS: 鏡面反射率
  • D: 法線分佈
  • V: 幾何可見度係數
  • F: 菲涅爾反射率

自訂光照函數:


[C#]

half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, 
    half3 normal, half3 viewDir,
    UnityLight light, UnityIndirect gi)
{
    half roughness = 1-oneMinusRoughness;


將光滑度轉換為粗糙度。


[C#]

half3 halfDir = Unity_SafeNormalize (light.dir + viewDir);


half向量。

正確處理NdotV(查看檔中的注解):


[C#]

half nl = DotClamped(normal, light.dir);
 
half nh = BlinnTerm (normal, halfDir);
half nv = DotClamped(normal, viewDir);
 
half lv = DotClamped (light.dir, viewDir);
half lh = DotClamped (light.dir, halfDir);


計算 V 和 D:

[C#]

half V = SmithJointGGXVisibilityTerm (nl, nv, roughness);
   half D = GGXTerm (nh, roughness);


根據Disney BRDF,計算漫反射項,以及鏡面反射係數:


[C#]

half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5);
   half specularTerm = (V * D) * (UNITY_PI/4); // Torrance-Sparrow model, Fresnel is applied later (for optimization reasons)
   //HACK (see file for more comments)
   specularTerm = max(0, specularTerm * nl);
   half diffuseTerm = disneyDiffuse * nl;
 
   // surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(realRoughness^2+1)
   half realRoughness = roughness*roughness;       // need to square perceptual roughness
   half surfaceReduction = 1.0 / (realRoughness*realRoughness + 1.0);          // fade \in [0.5;1]
 
   half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));


將所有的加總,包括全域光照貢獻:

[C#]

 half3 color =    diffColor * (gi.diffuse + light.color * diffuseTerm)
                    + specularTerm * light.color * FresnelTerm (specColor, lh)
                    + surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);
 
    return half4(color, 1);
}


以上就是光照函數的全部。下面來深入介紹全域光照對最終結果的貢獻。

本節我們將介紹全域光照貢獻的計算方式。過程有些麻煩,因為進行關鍵計算的程式碼隱匿在品質選擇層的層層定義之後。

所以讓我們查看下所有與全域光照有關的函數和結構體,它們就位於我們前面三節提及的代碼中。

在UnityStandardCore.cginc中, fragForwardBaseInternal:



[C#] 


UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight); 
half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect); 

    c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);

在我們的片段Forward基本函數中,FragmentGI被用於計算全域光照資料:“gi”,它被傳遞給UNITY_BRDF_PBS 和UNITY_BRDF_GI (它們的定義分別對應著不同的品質級別)。

在UnityStandardBRDF.cginc中, BRDF1_Unity_PBS:



[C#]

half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, 
    half3 normal, half3 viewDir,
    UnityLight light, UnityIndirect gi)
{
[...]
    half3 color =    diffColor * (gi.diffuse + light.color * diffuseTerm)
                    + specularTerm * light.color * FresnelTerm (specColor, lh)
                    + surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);
    return half4(color, 1);
}


這是UNITY_BRDF_PBS部分,它接受gi,用它來計算著色圖元的顏色。

以下兩個定義至少需要定義一個:

  • LIGHTMAP_ON
  • DYNAMICLIGHTMAP_ON

還有一堆額外的定義,用來控制程式的跳轉,或決定函數的選擇:
  • DIRLIGHTMAP_SEPARATE
  • DIRLIGHTMAP_COMBINED
  • UNITY_BRDF_PBS_LIGHTMAP_INDIRECT
  • UNITY_BRDF_GI
  • UNITY_SHOULD_SAMPLE_SH
  • UNITY_SPECCUBE_BLENDING
  • UNITY_SPECCUBE_BOX_PROJECTION
  • _GLOSSYREFLECTIONS_OFF
  • UNITY_SPECCUBE_BOX_PROJECTION

全域光照資料的運作基本是這樣的,從基本結構體流向與定義相關的函數:

  • 結構體UnityGI (在UnityLightingCommon.cginc中) 保存著多個UnityLight,取決於光照貼圖的類型
  • 結構體UnityGIInput (在UnityLightingCommon.cginc中) 保存著計算GI所需的其他不同資訊,被用於許多函式中
  • 函式UNITY_BRDF_GI (在UnityPBSLighting.cginc中) 在fragForwardBaseInternal 中用於計算對BRDF的間接貢獻(通過呼叫BRDF_Unity_Indirect)
  • 函數BRDF_Unity_Indirect(在UnityPBSLighting.cginc中)將UNITY_BRDF_PBS_LIGHTMAP_INDIRECT 的結果與傳入的colour相加
  • 函數UNITY_BRDF_PBS_LIGHTMAP_INDIRECT (在UnityPBSLighting.cginc中) 被定義為BRDF2_Unity_PBS (但一條注釋說也可以使用BRDF1_Unity_PBS ,以獲得更佳品質)
  • 函式BRDF2_Unity_PBS 或BRDF1_Unity_PBS,我們在前面一節中見過。這裡用於計算間接貢獻
  • 函數FragmentGI (在UnityStandardCore.cginc中) 填充必要的資料,包括來自反射探針的資料,然後傳遞給UnityGlobalIllumination
  • 函式UnityGlobalIllumination:(4個版本,不同的簽名)傳遞資料給UnityGI_Base 和UnityGI_IndirectSpecular
  • 函式UnityGI_Base(在UnityGlobalIllumination.cginc中)對光照貼圖進行採樣和解碼,混合即時衰減和應用遮蔽
  • 函數UnityGI_IndirectSpecular(在UnityGlobalIllumination.cginc中),計算反射,對盒型投影進行矯正(如果已啟動),處理遮擋

對於瞭解全貌同樣有用的東西:
  • 結構體UnityIndirect(在UnityLightingCommon.cginc中) 僅包含一個漫反射和一個鏡面反射顏色
  • 結構體UnityLight(在UnityLightingCommon.cginc中)保存光源的顏色、方向和NdotL
  • 貼圖立方體unity_SpecCube0 和unity_SpecCube1: 反射探針
  • 結構體Unity_GlossyEnvironmentData: 保存粗糙度和反射UV
  • 函數ResetUnityGI: 清空一個UnityGI結構體
  • 函數ResetUnityLight: 清空一個UnityLight 結構體
  • 函數ShadeSHPerPixel: 對每個圖元進行Spherical Harmonics採樣
這些知識應該已足以讓你在修改標準著色器時,不會意外的將全域光照玩壞XD。

關於我自己

我的相片
Unity台灣官方部落格 請上Facebook搜尋Unity Taiwan取得Unity中文的最新資訊