前言
問(wèn)題是這樣的精续,移動(dòng)端開(kāi)放世界的全局光照(GI)方案應(yīng)該如何實(shí)現(xiàn)?實(shí)時(shí)的光照計(jì)算(Realtime GI)肯定少不了,可是來(lái)自天空盒菩收,地表以及四周山石森林等的環(huán)境光漫反射又如何表現(xiàn)呢雇卷?關(guān)于這點(diǎn)項(xiàng)目組的同學(xué)都缺乏經(jīng)驗(yàn)鬓椭,于是我本著探索精神結(jié)合Unity框架調(diào)研了一番,期間總結(jié)了幾點(diǎn)感覺(jué)對(duì)大家有益的經(jīng)驗(yàn)关划,發(fā)在這里以供查閱小染。
1.1起點(diǎn)
但是在展開(kāi)之前需要先明確下限制條件:
- 首先,平臺(tái)是移動(dòng)端的贮折,也就意味著較低的算力和數(shù)據(jù)帶寬裤翩,那么很多算法復(fù)雜且消耗大量資源的方案就不用考慮了(光追?)
- 其次是開(kāi)放世界调榄,簡(jiǎn)言之場(chǎng)景數(shù)百甚至數(shù)千倍于傳統(tǒng)踊赠,那么為了不讓執(zhí)行烘焙的設(shè)備暴斃,就得著手給單次烘焙降降壓:降低品質(zhì)+分治每庆。
- 再然后是前文沒(méi)有提到的筐带,游戲時(shí)間需要支持日夜交替(Time of Day)和/或天氣系統(tǒng)(Weather System)。
1.2最簡(jiǎn)單的方案
OK缤灵,現(xiàn)在我們圍繞Unity引擎來(lái)考量下能有哪些方案可用吧伦籍。
Unity有一套完整的GI方案,可以在Window -> Rendering -> LightSetting中找到它的設(shè)計(jì)面板凤价。這套官方的方案總的來(lái)說(shuō)比較復(fù)雜鸽斟,因?yàn)樗m應(yīng)不同用戶的需求,比如依據(jù)不同的設(shè)置利诺,GI會(huì)在動(dòng)態(tài)和靜態(tài)之間富蓄,細(xì)節(jié)豐富程度和擬真程度上形成區(qū)別,而且越是好的效果慢逾,對(duì)存儲(chǔ)和計(jì)算的開(kāi)銷越大立倍∶鸷欤可以概括起的說(shuō),Unity通過(guò)烘焙(基于PBR的預(yù)計(jì)算)的方式口注,重建了場(chǎng)景中各個(gè)表面的光照數(shù)據(jù)变擒,并分類存儲(chǔ)到諸如Lightmap、ShadowMask寝志、LightProbe等數(shù)據(jù)載體中娇斑,在運(yùn)行的時(shí)候,再快速反解出這些光照數(shù)據(jù)材部,以供頂點(diǎn)和片元使用毫缆。具體細(xì)節(jié)內(nèi)容不是這篇博文的重點(diǎn),就不再單獨(dú)深入下去了乐导,之后有機(jī)會(huì)再單開(kāi)一篇介紹這套GI方案苦丁,這里丟一個(gè)個(gè)人覺(jué)得比較好的官方解釋文檔。
基于Unity提供的方案來(lái)說(shuō)物臂,什么是最省時(shí)省力的做法呢旺拉?我覺(jué)得是放棄使用任何額外的預(yù)計(jì)算光照,只在計(jì)算物體表面顏色時(shí)棵磷,附帶一層定制的環(huán)境光底色(Ambient)蛾狗,或者復(fù)雜一些,為角色的頭頂+側(cè)面+腳底3個(gè)方向各附加一層專門的環(huán)境光底色泽本,這種做法的復(fù)雜點(diǎn)在于需要利用角色模型的法線紋理淘太,過(guò)濾出朝上的部分,朝上的反方向部分规丽,以及其余所有部分作為遮罩蒲牧。雖然效果比預(yù)計(jì)算GI顯得單調(diào)了些,也不支持隨場(chǎng)景變化赌莺,但是貴在節(jié)約性能冰抢,操作簡(jiǎn)便。所以只要美術(shù)同學(xué)不在意這部分模糊的環(huán)境光艘狭,那么大可以省去烘焙的部分挎扰。
1.3 Lightmap的取舍和原因
說(shuō)到Unity全局光照方案,就繞不過(guò)光照貼圖(Lightmap)巢音。Lightmap的本質(zhì)是一系列的等尺寸的貼圖紋理遵倦,紋理上存儲(chǔ)的是經(jīng)過(guò)預(yù)計(jì)算后得到的物體表面光照信息(一般包含了直接光照和間接光照的效果)。更加形象點(diǎn)官撼,Lightmap上存儲(chǔ)的是所有參與烘焙的物體的表面顏色在2D空間上展開(kāi)的貼圖梧躺,運(yùn)行時(shí)配合著物體第二套UV以及一份特有的參數(shù)(縮放+下標(biāo)),直接讀取貼圖上的顏色進(jìn)行顯示傲绣÷痈纾可以說(shuō)在處理室內(nèi)靜態(tài)場(chǎng)景時(shí)巩踏,使用Lightmap會(huì)帶來(lái)性能和效果上的巨大優(yōu)化,然而對(duì)于空間龐大的室外場(chǎng)景來(lái)說(shuō)续搀,我們有太多需要烘焙的物體塞琼,如果把一個(gè)開(kāi)放世界看做一個(gè)場(chǎng)景,那么無(wú)腦烘焙的結(jié)果必然是海量的光照貼圖(假如沒(méi)爆內(nèi)存的話)禁舷。那么分場(chǎng)景烘焙+運(yùn)行時(shí)動(dòng)態(tài)載入的方法可行么彪杉?首先回答是可行,雖然在Unity的這套框架里L(fēng)ightmap是跟著場(chǎng)景(Scene)走的牵咙,但是不妨礙我們每次只Load一小塊地塊的Lighting Data Asset到當(dāng)前的全局場(chǎng)景在讶,然后可以參考這篇博文的方法,流式的加載場(chǎng)景上的物體霜大,只要這些物體預(yù)設(shè)了修正過(guò)的UV2,就能正確的采樣到Lightmap革答。然而如博文最后所說(shuō)的战坤,這種方法的最大問(wèn)題是人為打斷了Unity對(duì)資源的合批,導(dǎo)致包體膨脹残拐,且由于Mesh不同途茫,會(huì)影響到渲染的合批(URP是否有影響待考),影響性能溪食。從另一方面說(shuō)囊卜,烘焙好的表面貼圖靈活性也不夠,不適合有強(qiáng)烈的動(dòng)態(tài)的明暗變化的場(chǎng)景错沃,所以綜合考量下來(lái)栅组,我們決定棄用Lightmap。
2.0 總體GI方案
使用實(shí)時(shí)計(jì)算的直接光照和陰影枢析,再輔以lightProbe補(bǔ)全間接光照玉掸。
2.1 切割場(chǎng)景
鑒于超大地圖的特效,需要分場(chǎng)景烘焙醒叁,建議拆分出的子場(chǎng)景地塊邊長(zhǎng)相等且場(chǎng)景與場(chǎng)景之間大小也相等司浪,能給予以后管理和加載數(shù)據(jù)不少便利。以當(dāng)前DEMO為例把沼,其Base場(chǎng)景在加載余下9塊子場(chǎng)景后啊易,可以視為一個(gè)9宮格,每個(gè)宮格都是一塊正方形平面外加4個(gè)幾何物件組成饮睬。
如下圖所示租谈,一塊拆分好的子場(chǎng)景占示例場(chǎng)景的1/9:
在當(dāng)前示例中,場(chǎng)景由全局主場(chǎng)景Base和9個(gè)子場(chǎng)景Test_0 ~ Test_8構(gòu)成:
關(guān)于場(chǎng)景上物件续捂,一般情況下所有靜態(tài)物體的Mesh Renderer組件中需要勾選 Contribute GI垦垂,不過(guò)決定哪個(gè)物體要貢獻(xiàn)GI哪個(gè)不要的應(yīng)該是美術(shù)同學(xué)宦搬,而且最好是它們?cè)跇?gòu)建模型的時(shí)候就打上Tag,當(dāng)導(dǎo)入U(xiǎn)nity后由腳本識(shí)別Tag劫拗,自動(dòng)同步到烘焙屬性設(shè)置去间校;另一方面因?yàn)槲覀儾恍枰猯ightmap,可以在Receive GI下拉欄中页慷,選擇Light Probes(而不是lightmap)憔足,這樣做可以一定程度的加速烘焙。
2.2 布置探針
場(chǎng)景拆分完成后我們需要找一個(gè)空?qǐng)鼍白鳛楹姹河弥鲌?chǎng)景酒繁,然后把所有子場(chǎng)景拖入Base Scene(以Additive模式追加打開(kāi)場(chǎng)景)
需要注意的是滓彰,主光源只保留一份即可,建議使用主場(chǎng)景中的方向光作為主光源州袒,刪除或者失活子場(chǎng)景中相對(duì)應(yīng)的方向光揭绑。當(dāng)上述準(zhǔn)備完成,接下來(lái)才是布置探針郎哭,示例DEMO中探針是布置在主場(chǎng)景中的他匪,因?yàn)榇姹鹤訄?chǎng)景是以Additive模式打開(kāi)的,光照烘焙時(shí)探針對(duì)象被放置在哪個(gè)場(chǎng)景并不影響烘焙效果夸研。
放置探針有一些比較通用的建議邦蜜,比如:不要將探針?lè)胖迷谖矬w內(nèi)部,不要將所有探針?lè)胖迷谝粋€(gè)平面上亥至,在稀疏空間上可以少布置探針悼沈,光照變化豐富的地方最好多布置探針等等。但是不論如何姐扮,面對(duì)超大地圖絮供,最好還是采用腳本布設(shè)+手工后期調(diào)整的方式比較靠譜。這里推薦一些工具供大家參考:
2.3 烘焙 x N
DEMO場(chǎng)景比較簡(jiǎn)單溶握,所以直接手K了一組Light Probe Group杯缺,我們以烘焙9宮格左下角子場(chǎng)景為例,擺放好探針的效果如圖
可以看到睡榆,我們的探針覆蓋了這塊子場(chǎng)景的全部區(qū)域還有余量萍肆,這很好理解,因?yàn)槲磥?lái)在運(yùn)行時(shí)我們會(huì)按照角色是否踏入子場(chǎng)景地塊邊界來(lái)決定替換新舊探針組胀屿,留一些余量可以減少這種探針切換時(shí)帶來(lái)的可能的環(huán)境光跳變塘揣,同時(shí)也為我們做“延后切換”提供了數(shù)據(jù)保障。
在按下Generate Lighting按鈕前宿崭,我們還有一件非城渍。可以做的事情:失活掉當(dāng)前光照探針沒(méi)有觸及到的場(chǎng)景,如下圖所示:
可以想象,這些被unload的場(chǎng)景對(duì)當(dāng)前光照探針的貢獻(xiàn)微乎其微奖蔓,將其暫時(shí)卸載以提高光照烘焙的速度赞草,降低烘焙時(shí)的內(nèi)存開(kāi)銷,才能使得大場(chǎng)景烘焙成為可能吆鹤。只不過(guò)這樣的單場(chǎng)景烘焙要執(zhí)行N次厨疙,需要反復(fù)加載和卸載一些場(chǎng)景,所以建議工程化后用腳本替代這些手操比較好疑务。
2.4 導(dǎo)出lightProbes.Asset
當(dāng)每烘焙完成一個(gè)子場(chǎng)景沾凄,都要即時(shí)從LightmapSettings中導(dǎo)出并保存探針數(shù)據(jù):
AssetDatabase.CreateAsset(Instantiate(LightmapSettings.lightProbes), defaultPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
導(dǎo)出后保存為Asset資源,以地塊編號(hào)輔助命名:
這些保存的Asset資源分別記錄了每次光照烘焙得到的探針數(shù)據(jù)知允,可以用記事本打開(kāi)查看其中數(shù)據(jù):
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!258 &25800000
LightProbes:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: lightProbe_0
m_Data:
m_Tetrahedralization:
m_Tetrahedra:
- indices[0]: 47
indices[1]: 8
indices[2]: 19
indices[3]: 17
neighbors[0]: 114
neighbors[1]: 93
neighbors[2]: 148
neighbors[3]: 132
matrix:
e00: 0.07278019
e01: 0
e02: 0
e03: 0
e10: -0
e11: -0
e12: -0.07358351
e13: 0
e20: -0.0727802
e21: -0.16666667
e22: -0
e23: 0
...
m_ProbeSets:
- m_Hash:
serializedVersion: 2
Hash: a59bcf93405ea129891b07a70413d3af
m_Offset: 0
m_Size: 72
m_Positions:
- {x: -17.24, y: 8, z: -46.59}
- {x: -17.24, y: 8, z: -16.67}
- {x: -17.24, y: 2, z: -46.59}
- {x: -17.24, y: 2, z: -16.67}
- {x: -17.24, y: 8, z: -27}
...
m_BakedCoefficients:
- sh[ 0]: 0.2021866
sh[ 1]: -0.027633084
sh[ 2]: 0.0001353545
sh[ 3]: 0.014740911
sh[ 4]: -0.004248155
sh[ 5]: -0.0082279835
sh[ 6]: 0.010135704
sh[ 7]: -0.015571148
sh[ 8]: 0.04651142
sh[ 9]: 0.27547473
sh[10]: -0.02017048
sh[11]: -0.016495945
sh[12]: 0.062124733
sh[13]: -0.025204908
sh[14]: -0.0073637315
sh[15]: 0.010615408
sh[16]: -0.06704397
sh[17]: 0.05697039
sh[18]: 0.31635872
sh[19]: 0.05582881
sh[20]: -0.00720126
sh[21]: 0.009610832
sh[22]: 0.008846933
sh[23]: -0.034047075
sh[24]: 0.018298429
sh[25]: 0.002981733
sh[26]: 0.035521053
...
m_BakedLightOcclusion:
- m_ProbeOcclusionLightIndex: ffffffffffffffffffffffffffffffff
m_Occlusion:
- 0
- 0
- 0
- 0
...
除去頭部的基礎(chǔ)信息外撒蟀,數(shù)據(jù)主要分為4個(gè)部分:
- 第一部分m_Tetrahedralization記錄了四面體網(wǎng)絡(luò),以便運(yùn)行時(shí)快速定位熱點(diǎn)區(qū)域温鸽;
- 第二部分是m_Positions保屯,自然是存放每個(gè)Probe的空間位置信息;
- 第三部分是m_BakedCoefficients涤垫,既存放了我們通常認(rèn)識(shí)中LightProbe應(yīng)該存放的球諧系數(shù)配椭,每個(gè)共3*9=27個(gè)浮點(diǎn)數(shù)值;
- 最后一部分叫m_BakedLightOcclusion雹姊,存放的可能是一些遮擋關(guān)系,方便Unity做快速剔除用(待考)衡楞。
3.1 分地塊(Tile)動(dòng)態(tài)加載
我們按照地塊(Tile)生成LightProbes資源吱雏,在運(yùn)行時(shí)則依據(jù)角色/攝像機(jī)當(dāng)前所屬地塊,動(dòng)態(tài)的加載 -> 替換光照探針資源瘾境。具體算法可以參考以前介紹過(guò)的9宮格系統(tǒng)歧杏,默認(rèn)當(dāng)前攝像機(jī)處于中心,需要提前異步得加載周圍8塊地塊對(duì)應(yīng)的LightProbes資源迷守,確保當(dāng)需要切換資源時(shí)犬绒,資源總是在手邊可用。
具體切換操作非常簡(jiǎn)單兑凿,如下:
LightmapSettings.lightProbes = usedLightProbes[curIndex];
只需要將預(yù)加載好的對(duì)應(yīng)場(chǎng)景LightProbes對(duì)象替換全局對(duì)象LightmapSettings的lightProbes變量即可凯力,無(wú)需添加或切換場(chǎng)景。
3.2 地塊銜接處的處理
細(xì)化一下切換LightProbes資源邏輯礼华,既所謂的“延后切換”:為了避免因?yàn)榻巧诘貕K交界處來(lái)回移動(dòng)咐鹤,而頻繁觸發(fā)切換操作的弊端,我們規(guī)定只有當(dāng)角色越過(guò)了原本地塊邊界一定距離(gap)后才會(huì)觸發(fā)刷新和切換圣絮,只要觸發(fā)切換時(shí)角色所在位置任然被上一個(gè)場(chǎng)景的LightProbes覆蓋祈惶,就不會(huì)導(dǎo)致環(huán)境色跳變,同時(shí)由于增加了gap,角色來(lái)回運(yùn)動(dòng)時(shí)必須要滿足 2 x gap 的距離才會(huì)再次觸發(fā)切換捧请,從而降低了頻率凡涩。
3.3 TOD和探針的明暗變化
Time of Day(簡(jiǎn)稱TOD)要求游戲光照能夠隨時(shí)間變化而變化,以適應(yīng)不同時(shí)間段天光輻照度的要求疹蛉,例如表現(xiàn)黎明時(shí)分的醬紫色或日落時(shí)刻的橙紅色活箕。為了達(dá)到目的,我們首先想到的是由天空盒控制主光源(日光)的光強(qiáng)和顏色氧吐,但是這只影響直接光照讹蘑,間接光照來(lái)自預(yù)烘焙的光照探針,而烘焙時(shí)日光使用了什么強(qiáng)度/顏色筑舅,探針中就存儲(chǔ)了對(duì)應(yīng)的間接光漫反射信息座慰。
如果不考慮代價(jià),為達(dá)到最真實(shí)效果翠拣,我們需要在每一次日照發(fā)生變化時(shí)都烘焙一份探針信息版仔,運(yùn)行時(shí)排著隊(duì)的替換使用。
從另一方面考慮误墓,如果要求代價(jià)最小蛮粮,那么我們可以只烘焙一份探針信息,運(yùn)行時(shí)只簡(jiǎn)單調(diào)整探針光照信息的強(qiáng)度谜慌。
最后平衡一下這兩種極端情況然想,我們可以烘焙若干個(gè)有代表性的時(shí)間點(diǎn),運(yùn)行到下一個(gè)時(shí)間段才觸發(fā)光照探針的替換欣范,銜接的部分則可以通過(guò)球諧系數(shù)或者光照強(qiáng)度的調(diào)節(jié)变泄,盡可能讓2套探針的光照信息在替換的時(shí)刻保持一致,從而減小視覺(jué)上的跳變感恼琼。
3.4 LightProbes 強(qiáng)度設(shè)置注意點(diǎn)
本節(jié)以調(diào)節(jié)探針光照強(qiáng)度為例妨蛹,一起來(lái)看看Unity為用戶暴露出了哪些接口。首先我們可以在:
LightmapSettings.lightProbes.bakedProbes
中獲取到一組探針信息隊(duì)列晴竞,隊(duì)列中的元素是一種叫 SphericalHarmonicsL2 的類蛙卤,意思是二階球諧函數(shù),存儲(chǔ)了來(lái)自L0的1個(gè)噩死,來(lái)自L1的3個(gè)以及來(lái)自L2的5個(gè)颤难,一共1 + 3 + 5 = 9
組系數(shù),每組系數(shù)都需要代表一種RGB色彩已维,所以系數(shù)總合還要再乘以3個(gè)通道乐严,最終得到 3 * 9 = 27
個(gè)浮點(diǎn)數(shù)。 我們?cè)谠L問(wèn)類 SphericalHarmonicsL2 時(shí)衣摩,可以采用如下方法獲取到這27個(gè)球諧系數(shù):
for (int j = 0; j < 3; j++)
{
for (int k = 0; k < 9; k++)
{
Debug.Log(sh[j, k]);
}
}
特別注意一點(diǎn)昂验,雖然Unity允許我們通過(guò)下標(biāo)的方式直接訪問(wèn)甚至修改這些球諧系數(shù)捂敌,但是至少在Unity2019版本上,這種修改方式不會(huì)在運(yùn)行時(shí)產(chǎn)生任何效果既琴,這與網(wǎng)上的大部分參考示例(詳見(jiàn)參考2占婉、參考3)顯示的結(jié)果不一致!推測(cè)原因是Unity在某個(gè)版本之后關(guān)閉了直接手K系數(shù)的通道甫恩,取而代之的是一些新的API逆济。
說(shuō)到API,因?yàn)樾薷墓庹仗结様?shù)據(jù)主要就是修改SphericalHarmonicsL2磺箕,我們?cè)賮?lái)看看這個(gè)類有哪些接口:
public struct SphericalHarmonicsL2 : IEquatable<SphericalHarmonicsL2>
{
public float this[int rgb, int coefficient] { get; set; }
public void AddAmbientLight(Color color);
public void AddDirectionalLight(Vector3 direction, Color color, float intensity);
public void Clear();
public override bool Equals(object other);
public bool Equals(SphericalHarmonicsL2 other);
public void Evaluate(Vector3[] directions, Color[] results);
public override int GetHashCode();
public static SphericalHarmonicsL2 operator +(SphericalHarmonicsL2 lhs, SphericalHarmonicsL2 rhs);
public static SphericalHarmonicsL2 operator *(SphericalHarmonicsL2 lhs, float rhs);
public static SphericalHarmonicsL2 operator *(float lhs, SphericalHarmonicsL2 rhs);
public static bool operator ==(SphericalHarmonicsL2 lhs, SphericalHarmonicsL2 rhs);
public static bool operator !=(SphericalHarmonicsL2 lhs, SphericalHarmonicsL2 rhs);
}
Unity給的官方示例里使用的是 AddAmbientLight 和 AddDirectionalLight 這兩個(gè)接口奖慌,它們分別提供了在運(yùn)行時(shí)動(dòng)態(tài)設(shè)置Ambient和DirectionLight這兩項(xiàng)參數(shù)的能力唐断,但是個(gè)人感覺(jué)不適合我們預(yù)想的應(yīng)用場(chǎng)景啡莉,因?yàn)閺脑O(shè)置參數(shù)到轉(zhuǎn)化為27個(gè)系數(shù)需要比較復(fù)雜的數(shù)學(xué)運(yùn)算强窖,這是一處額外的CPU消耗盅抚,且我們也無(wú)法簡(jiǎn)單準(zhǔn)確地給出每一個(gè)探針點(diǎn)的對(duì)應(yīng)環(huán)境光和方向光,這些參數(shù)本身應(yīng)當(dāng)是預(yù)計(jì)算結(jié)果富雅,不應(yīng)在運(yùn)行時(shí)現(xiàn)場(chǎng)運(yùn)算索烹,消耗更多的計(jì)算資源届案。
余下比較有意思的是2個(gè)算符重載屠列,一個(gè)是“*”一個(gè)是“+”啦逆。
先說(shuō)星號(hào),經(jīng)過(guò)測(cè)試笛洛,發(fā)現(xiàn)參與運(yùn)算的浮點(diǎn)數(shù)起到了控制探針光強(qiáng)的作用夏志,變化是線性的,乘子為0則全黑苛让,乘子為1則維持本色盲镶。
其次是加號(hào),可以猜想是通過(guò)某種算法蝌诡,將參與相加的兩組球諧函數(shù)系數(shù)混淆起來(lái),這點(diǎn)比較有意思枫吧,因?yàn)槲覀兛梢岳眠@種簡(jiǎn)單的加法混淆浦旱,將一種SH狀態(tài)漸漸的轉(zhuǎn)化到另一種SH狀態(tài),從而完成烘焙數(shù)據(jù)之間的無(wú)縫轉(zhuǎn)換九杂。
為簡(jiǎn)單起見(jiàn)颁湖,這里先用星號(hào)算符對(duì)lightProbes進(jìn)行強(qiáng)度控制,代碼參考如下:
void Start()
{
...
LightmapSettings.lightProbes = Instantiate(originLightProbes[curIndex]);
}
void Update()
{
...
var bakedProbes = LightmapSettings.lightProbes.bakedProbes;
var origin = originLightProbes[curIndex].bakedProbes;
for (int i = 0; i < bakedProbes.Length; i++)
{
bakedProbes[i] = origin[i] * intensity;
}
LightmapSettings.lightProbes.bakedProbes = bakedProbes;
}
這里需要強(qiáng)調(diào)一點(diǎn)例隆,就是任何對(duì)bakedProbes的修改甥捺,都會(huì)影響到Asset資源的數(shù)值,所以務(wù)必只對(duì)實(shí)例化后的bakedProbes進(jìn)行修改镀层,參考Start方法中調(diào)用的實(shí)例化函數(shù)镰禾。另外不難發(fā)現(xiàn),在Update方法中 bakedProbes[i] = origin[i] * intensity;
使用的乘法算子已經(jīng)是被Unity重載過(guò)的了。
4.1 優(yōu)化
上述實(shí)踐完成后吴侦,我們已經(jīng)有了一套初步的GI方案來(lái)適配TOD屋休,但是通過(guò)控制探針光強(qiáng)的方法過(guò)于簡(jiǎn)單和缺乏靈活性。舉個(gè)例子备韧,只控制光強(qiáng)因子的前提下劫樟,我們會(huì)烘焙一套正午時(shí)分的環(huán)境光資源,然后設(shè)計(jì)一條類似正弦曲線织堂,讓光強(qiáng)因子在黎明時(shí)分從0開(kāi)始增長(zhǎng)叠艳,直到正午達(dá)到峰值1,然后緩慢落回0易阳,此時(shí)正值夜幕降臨附较。這種設(shè)計(jì)也許能對(duì)付下簡(jiǎn)單的晝夜交替的變化,但是當(dāng)遇到諸如在夜晚發(fā)光螢石或者光源闽烙,那么周圍的環(huán)境光就會(huì)穿幫(此時(shí)環(huán)境光強(qiáng)為0)翅睛。一種可行的解決辦法是預(yù)先烘焙多組探針資源,然后設(shè)法在它們之間順滑地切換黑竞,后續(xù)我們將基于這個(gè)假設(shè)來(lái)討論如何優(yōu)化表現(xiàn)效果捕发。
優(yōu)化的另一方面是性能開(kāi)銷,在保證效果的前提下很魂,我們會(huì)討論如何降低計(jì)算復(fù)雜度扎酷,降低內(nèi)存開(kāi)銷等影響幀率的部分。
4.2 效果優(yōu)化
要實(shí)現(xiàn)在兩套資源 lightProbesA
和 lightProbesB
間順滑過(guò)度遏匆,無(wú)論一開(kāi)始基于哪一個(gè)lightProbes法挨,只通過(guò)調(diào)節(jié)其光照強(qiáng)度是無(wú)論如何也做不到順滑切換的,就像你不可能通過(guò)調(diào)整一把紅色手電筒的強(qiáng)度幅聘,自然過(guò)度到綠色手電筒的效果(前提是切換點(diǎn)兩者的光強(qiáng)都不是0)凡纳。那么什么方法可以呢?答案是按比例插值帝蒿。假設(shè)我們一開(kāi)始基于lightProbesA
的數(shù)據(jù)顯示環(huán)境光荐糜,首先我們想辦法預(yù)計(jì)算兩道資源的差異:
delta = lightProbesB - lightProbesA
然后我們還需要一個(gè)系數(shù)來(lái)控制插值的百分比:
intensity = (cur_time - start_time_A) / (start_time_B - start_time_A)
那么最后過(guò)渡段某個(gè)時(shí)刻的環(huán)境光插值可以作如下表示:
usedLightProbe = lightProbesA + delta * intensity
這里面使用到的‘+’和‘*’都是Unity重載過(guò)的算符
具體到Unity工程中,簡(jiǎn)化后的計(jì)算delta
的代碼可以參考如下:
delta = new SphericalHarmonicsL2[probes[0].count];
for (int i = 0; i < probes[0].count; i++)
{
SphericalHarmonicsL2 shA = probes[0].bakedProbes[i];
SphericalHarmonicsL2 shB = probes[1].bakedProbes[i];
SphericalHarmonicsL2 d = new SphericalHarmonicsL2();
for (int j = 0; j < 3; j++)
{
for (int k = 0; k < 9; k++)
{
d[j, k] = shB[j, k] - shA[j, k];
}
}
delta[i] = d;
}
而使用插值修改lightProbes可以參考如下示例:
public void ChangeProbes()
{
float intensity = (Mathf.Sin(Time.time / 2.0f) + 1f) / 2.0f; // 0 ~ 1
var bakedProbes = LightmapSettings.lightProbes.bakedProbes;
var originProbes = probes[0].bakedProbes;
for (int i = 0; i < bakedProbes.Length; i++)
{
bakedProbes[i] = originProbes[i] + delta[i] * intensity;
}
LightmapSettings.lightProbes.bakedProbes = bakedProbes;
}
4.3 分幀更新
之前提及過(guò)葛超,修改每個(gè)光照探針只涉及到27個(gè)浮點(diǎn)數(shù)的加法和乘法操作暴氏,且由于我們區(qū)分了地塊,每次只工作在一個(gè)相對(duì)小規(guī)模的光照探針網(wǎng)絡(luò)中绣张,奈何光照探針的數(shù)目很可能任然巨大答渔,如果每一幀都要執(zhí)行數(shù)千上萬(wàn)次乘法操作,所消耗的CPU資源不可小視侥涵。 事實(shí)上沼撕,當(dāng)采用上一節(jié)的ChangeProbes
方法負(fù)責(zé)刷新探針數(shù)據(jù)并做profiler后得到下圖結(jié)果:
可見(jiàn)紅色方框內(nèi)產(chǎn)生了數(shù)百次memcpy和算術(shù)操作宋雏,此外箭頭標(biāo)記處顯示有大量?jī)?nèi)存Alloc,這個(gè)后面討論端朵。
這個(gè)問(wèn)題的解決方案很簡(jiǎn)單好芭,分幀更新即可,我們每幀不必遍歷所有激活的探針節(jié)點(diǎn)冲呢,而是把存放探針數(shù)組的容器定義為一個(gè)回環(huán)buffer( 或者叫 ring buffer )舍败,每一次只遍歷從起始位置開(kāi)始往后的N個(gè)節(jié)點(diǎn),待遍歷完成后再重設(shè)一下起始位置即可敬拓。
public void ChangeProbesPartial()
{
float intensity = (Mathf.Sin(Time.time / 2.0f) + 1f) / 2.0f;
var bakedProbes = LightmapSettings.lightProbes.bakedProbes;
var originProbes = probes[0].bakedProbes;
var totalSize = probes[0].count;
int ct = MaxNumberPerFrame;
int i = startIndex;
while (ct-- > 0)
{
bakedProbes[i] = originProbes[i] + delta[i] * intensity;
i = ++i % totalSize;
}
startIndex = i;
LightmapSettings.lightProbes.bakedProbes = bakedProbes;
}
這樣修改后邻薯,更新頻率就變成一個(gè)可控的參數(shù)N。通過(guò)Profiler查看結(jié)果如下圖:
可見(jiàn)紅框中的調(diào)用次數(shù)下降了一個(gè)數(shù)量級(jí)乘凸,CPU資源消耗幾乎歸零厕诡,且顯示效果絲毫沒(méi)有影響。
4.4 內(nèi)存優(yōu)化
參考上圖中箭頭處的內(nèi)存消耗(主要來(lái)自Alloc)不難發(fā)現(xiàn)Unity底層對(duì)LightProbes的get_bakedProbes()
操作會(huì)產(chǎn)生值拷貝营勤,具體到Unity工程代碼參考如下:
var bakedProbes = LightmapSettings.lightProbes.bakedProbes; //觸發(fā)get操作
var originProbes = probes[0].bakedProbes; //也會(huì)觸發(fā)get操作
為了緩解頻繁(每幀)拷貝bakedProbes這種蠢事灵嫌,我嘗試了一種更加懶惰的更新策略,代碼如下:
public void ChangeProbesPartialLazy()
{
float intensity = (Mathf.Sin(Time.time / 3.0f) + 1f) / 2.0f;
if (startIndex < MaxNumberPerFrame)
{
workOn = LightmapSettings.lightProbes.bakedProbes;
}
var totalSize = probes[0].count;
int ct = MaxNumberPerFrame;
int i = startIndex;
while (ct-- > 0)
{
workOn[i] = origin[i] + delta[i] * intensity;
i = ++i % totalSize;
}
startIndex = i;
if (startIndex < MaxNumberPerFrame)
{
LightmapSettings.lightProbes.bakedProbes = workOn;
}
}
簡(jiǎn)述下邏輯葛作,我們只在完成一次循環(huán)后(完整遍歷了lightProbes隊(duì)列)才設(shè)置 + 獲取一次Unity托管的bakedProbes資源寿羞。整個(gè)過(guò)程就像打快照一樣,一旦得到快照赂蠢,在接下來(lái)的幾幀或者數(shù)十幀內(nèi)都是基于當(dāng)前快照內(nèi)容進(jìn)行修改绪穆,等到快照完成更新后再一次性提交給Unity用來(lái)刷新顯示。對(duì)于ChangeProbesPartialLazy
再次Profiler后得到下圖結(jié)論:
可以看到虱岂,大多數(shù)幀內(nèi)來(lái)自方法內(nèi)部的Alloc消失了玖院,并且實(shí)際表現(xiàn)上仍然看不出區(qū)別:
5.1 測(cè)試數(shù)據(jù)
說(shuō)明:表格展示了隨著烘焙探針數(shù)量的倍增,其資源大小第岖,系統(tǒng)耗時(shí)的成長(zhǎng)關(guān)系难菌。
以下所有數(shù)據(jù)取自 WIN7系統(tǒng) Core I7-6700 @ 3.7G 兼容機(jī)平臺(tái) (X2 ~ 2.5 Snapdragon 845 @2018年旗艦)
項(xiàng)目\探針數(shù) | 72 | 144 | 288 | 576 | 1152 | 2304 | 4608 |
---|---|---|---|---|---|---|---|
Size(KB) | 203 | 428 | 896 | 1836 | 3719 | 8053 | 17084 |
Instantiate Cost (ms) | 0.08 | 0.15 | 0.25 | 0.50 | 1.07 | 3.00 | 5.59 |
Set_Probes Cost (ms) | 0.0034 | 0.0047 | 0.0069 | 0.0148 | 0.0298 | 0.0533 | 0.1405 |