基于LightProbes的動(dòng)態(tài)全局光照(GI)方案探討

前言

問(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)之前需要先明確下限制條件:

  1. 首先,平臺(tái)是移動(dòng)端的贮折,也就意味著較低的算力和數(shù)據(jù)帶寬裤翩,那么很多算法復(fù)雜且消耗大量資源的方案就不用考慮了(光追?)
  2. 其次是開(kāi)放世界调榄,簡(jiǎn)言之場(chǎng)景數(shù)百甚至數(shù)千倍于傳統(tǒng)踊赠,那么為了不讓執(zhí)行烘焙的設(shè)備暴斃,就得著手給單次烘焙降降壓:降低品質(zhì)+分治每庆。
  3. 再然后是前文沒(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è)幾何物件組成饮睬。

Whole Scene.png

如下圖所示租谈,一塊拆分好的子場(chǎng)景占示例場(chǎng)景的1/9:

Single Scene.png

在當(dāng)前示例中,場(chǎng)景由全局主場(chǎng)景Base和9個(gè)子場(chǎng)景Test_0 ~ Test_8構(gòu)成:

Asset View.png

關(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)憔足,這樣做可以一定程度的加速烘焙。

Mesh Renderer.png

2.2 布置探針

場(chǎng)景拆分完成后我們需要找一個(gè)空?qǐng)鼍白鳛楹姹河弥鲌?chǎng)景酒繁,然后把所有子場(chǎng)景拖入Base Scene(以Additive模式追加打開(kāi)場(chǎng)景)

Base Hierarchy.png

需要注意的是滓彰,主光源只保留一份即可,建議使用主場(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)景為例,擺放好探針的效果如圖

Scene Probes.png

可以看到睡榆,我們的探針覆蓋了這塊子場(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)景,如下圖所示:

Scene Probes ready.png

可以想象,這些被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)輔助命名:

Baked Probe Assets.png

這些保存的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給的官方示例里使用的是 AddAmbientLightAddDirectionalLight 這兩個(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)在兩套資源 lightProbesAlightProbesB 間順滑過(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é)果:

ChangeProbes.png

可見(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é)果如下圖:

ChangeProbesPartial.png

可見(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é)論:

ChangeProbesPartialLazy.png

可以看到虱岂,大多數(shù)幀內(nèi)來(lái)自方法內(nèi)部的Alloc消失了玖院,并且實(shí)際表現(xiàn)上仍然看不出區(qū)別:

showcase.gif

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

5.2 參考

  1. LightProbe原理和數(shù)據(jù)結(jié)構(gòu)
  2. How to add / update light probes when using load additive
  3. Light Probe Intensity Adjustment Tool for Unity3D
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蔑滓,隨后出現(xiàn)的幾起案子郊酒,更是在濱河造成了極大的恐慌,老刑警劉巖烫饼,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異试读,居然都是意外死亡杠纵,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門钩骇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)比藻,“玉大人铝量,你說(shuō)我怎么就攤上這事∫祝” “怎么了慢叨?”我有些...
    開(kāi)封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)务蝠。 經(jīng)常有香客問(wèn)我拍谐,道長(zhǎng),這世上最難降的妖魔是什么馏段? 我笑而不...
    開(kāi)封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任轩拨,我火速辦了婚禮,結(jié)果婚禮上院喜,老公的妹妹穿的比我還像新娘亡蓉。我一直安慰自己,他們只是感情好喷舀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布砍濒。 她就那樣靜靜地躺著,像睡著了一般硫麻。 火紅的嫁衣襯著肌膚如雪爸邢。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天庶香,我揣著相機(jī)與錄音甲棍,去河邊找鬼。 笑死赶掖,一個(gè)胖子當(dāng)著我的面吹牛感猛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奢赂,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼陪白,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了膳灶?” 一聲冷哼從身側(cè)響起咱士,我...
    開(kāi)封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎轧钓,沒(méi)想到半個(gè)月后序厉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毕箍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年弛房,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片而柑。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡文捶,死狀恐怖荷逞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粹排,我是刑警寧澤种远,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站顽耳,受9級(jí)特大地震影響坠敷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斧抱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一常拓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辉浦,春花似錦弄抬、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至弛槐,卻和暖如春懊亡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乎串。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工店枣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叹誉。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓鸯两,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親长豁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钧唐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容