架構(gòu)深度拆解:從剔除調(diào)度到資源管理實踐)
系列第一篇寫完評論區(qū)一直有人催更渲染這塊。確實渲染系統(tǒng)是整個引擎里最“外露”的東西——玩家不會夸你物理算得有多準(zhǔn)、網(wǎng)絡(luò)同步有多穩(wěn)但渲染卡一幀、陰影閃一下、遠(yuǎn)處貼圖糊一片那都是一眼就能看見的事。這篇我就把渲染系統(tǒng)架構(gòu)從整體設(shè)計到落地細(xì)節(jié)完整拆一遍會涉及一個自研引擎實際開發(fā)過程中的大量取舍記錄和踩坑實錄。無論你是打算從頭寫一個引擎還是想搞懂商業(yè)引擎內(nèi)部渲染模塊的運作邏輯這篇都值得看完。1. 渲染系統(tǒng)架構(gòu)的第一性問題它到底管什么1.1 職責(zé)邊界先畫清楚很多初學(xué)者拿到引擎源碼第一反應(yīng)是去找DrawCall在哪、Shader在哪。其實渲染系統(tǒng)的架構(gòu)設(shè)計最先要搞清楚的根本不是畫什么、怎么畫而是邊界。渲染系統(tǒng)要管的不是“把三角形畫出來”這么簡單。完整職責(zé)范圍包括場景數(shù)據(jù)組織哪些物體存在、在哪、可見性判定攝像機(jī)能看到誰、資源生命周期頂點緩沖、紋理、材質(zhì)上傳下載的時機(jī)、渲染狀態(tài)切換深度測試、混合模式、視口、光照和陰影的計算組織、后處理鏈路拼接以及最底層與GPU驅(qū)動打交道時提交命令和同步的方式。這些職責(zé)如果全部塞到一套代碼里前期開發(fā)確實快但越往后越痛苦。我第一版原型就是這么干的所有渲染調(diào)用直接在游戲邏輯里寫死繪制狀態(tài)隨手改資源加載堵塞主線程。結(jié)果就是調(diào)試任何一條渲染鏈路都要在幾千行業(yè)務(wù)邏輯和底層API調(diào)用中間來回跳轉(zhuǎn)。后來重構(gòu)時第一件事就是把渲染系統(tǒng)的邊界畫清楚邏輯層只負(fù)責(zé)“要畫的東西和它們的屬性”渲染層只負(fù)責(zé)“怎么高效地畫出來”。這個切割是整個架構(gòu)調(diào)整中性價比最高的一步。1.2 幀循環(huán)與線程模型是地基渲染系統(tǒng)架構(gòu)里最容易被低估的部分是幀循環(huán)和線程模型。一個幀里邏輯要更新、物理要計算、動畫要采樣、渲染要提交它們的時間關(guān)系怎么排直接決定你后續(xù)所有功能好不好做。最常見的組織方式是三線程模型主線程游戲邏輯、渲染線程命令錄制與提交、GPU異步執(zhí)行。主線程和渲染線程之間通過命令緩沖Command Buffer通信。主線程把這一幀要畫的內(nèi)容打包成命令寫進(jìn)緩沖渲染線程讀出來翻譯成底層API調(diào)用提交給驅(qū)動GPU實際執(zhí)行。兩個線程流水線并行主線程跑到第N2幀時渲染線程可能還在處理第N1幀GPU執(zhí)行的是第N幀。這個錯位設(shè)計換來的是CPU可支配時間的大幅提升代價是畫面顯示會有一個固定的幾幀延遲。這個延遲對單機(jī)玩家、對輸入響應(yīng)敏感的對戰(zhàn)游戲來說處理方式完全不同。我們當(dāng)時的取舍是主線程提前預(yù)測一幀的相機(jī)狀態(tài)來渲染減少操作延遲感同時保留一個可配置的幀延遲開關(guān)方便調(diào)試時候改成同步模式。這個開關(guān)后來幫了大忙很多渲染bug在同步模式下幾秒鐘就能定位異步模式下得靠一堆日志和時間戳去猜。1.3 架構(gòu)選型的根本矛盾只有一組渲染系統(tǒng)面向的硬件能力差異極大。集顯機(jī)器上可能只有幾十個DrawCall的預(yù)算而獨顯機(jī)器上千個都不帶喘的。這個矛盾決定了架構(gòu)選型的核心你沒法一套邏輯吃遍所有設(shè)備必須在“保表現(xiàn)”和“保幀率”之間做動態(tài)平衡。業(yè)界普遍的解法是渲染質(zhì)量分級Scalability定義一組從低到高的渲染特性檔位比如陰影分辨率、動態(tài)光源數(shù)量、抗鋸齒方案、體積霧開關(guān)、貼圖最大分辨率等。每個檔位對應(yīng)一套參數(shù)組合。運行時讀設(shè)備信息給出一個建議檔位玩家可以手動調(diào)。架構(gòu)上要保證這些檔位的切換在運行時是可熱切換的不要走初始化大重置的路子。這個設(shè)計最容易被忽略的是不同檔位之間的狀態(tài)一致性。你從高畫質(zhì)切到低畫質(zhì)場景里所有物體的材質(zhì)實例、燈光陰影的渲染目標(biāo)和紋理池都要跟著換。如果資源系統(tǒng)沒有做好引用計數(shù)和版本控制熱切換必然出現(xiàn)紋理黑塊、陰影閃爍之類的怪毛病。我們第一版切檔位直接崩了就是因為舊資源還沒釋放、新資源已經(jīng)綁定了同一個槽位。后來統(tǒng)一在渲染資源管理器里加了一層代際編號Generation老資源延遲到GPU讀完當(dāng)前幀再回收問題才根治。2. 場景數(shù)據(jù)組織與剔除系統(tǒng)決定渲染架構(gòu)的上限2.1 場景結(jié)構(gòu)別一上來就做場景圖很多人一說場景管理第一反應(yīng)是“場景圖Scene Graph”——游戲?qū)ο髵煸趯蛹壒?jié)點上父節(jié)點移動子節(jié)點跟著動。這個概念用在編輯器里很舒服但用在渲染系統(tǒng)里如果你直接把場景圖當(dāng)渲染數(shù)據(jù)源用后面遲早要重構(gòu)。原因在于場景圖是面向編輯的層級結(jié)構(gòu)渲染系統(tǒng)要的是面向查詢的扁平結(jié)構(gòu)。渲染時最頻繁的操作是“給我視錐體里所有帶MeshRenderer的物體”而不是“從根節(jié)點遞歸遍歷找所有葉子”。如果渲染每幀都去遞歸遍歷場景圖層級深一點、節(jié)點多一點CPU時間就全燒在遍歷上了GPU早就閑著等了。我們最終采用的方案是雙結(jié)構(gòu)場景圖服務(wù)編輯器、邏輯和動畫系統(tǒng)渲染系統(tǒng)維護(hù)一份獨立的扁平渲染列表Render List。這個列表按材質(zhì)、Mesh、渲染隊列排序組織成適合批處理和剔除的緊湊結(jié)構(gòu)。每次場景圖發(fā)生了空間變化移動、增刪通過事件系統(tǒng)通知渲染層更新對應(yīng)條目而不是每幀全量同步。這個雙結(jié)構(gòu)的代價是兩套數(shù)據(jù)要維護(hù)一致性但換來的是渲染側(cè)查詢和剔除的高效完全值得。2.2 視錐體剔除之外必須上遮擋剔除場景里的物體不可能全在畫面里第一步能做的是視錐體剔除把六個裁剪面算出來凡是包圍盒完全在六個面之外的物體直接不畫。這個是渲染系統(tǒng)的基礎(chǔ)操作絕大多數(shù)引擎都能做到。但視錐體剔除只解決“不在畫面里”的問題解決不了“在畫面里但被墻擋住”的問題。一棟樓后面藏著一整條街的物體視錐體全都在但像素一個都看不見——大量性能就浪費在看不到的地方。遮擋剔除的方案有好幾種硬件遮擋查詢Occlusion Query、軟件光柵化遮擋剔除、基于PVS潛在可見集的預(yù)計算剔除。實際項目里我推薦先用軟件遮擋剔除配合層級Z緩沖Hierarchical Z-Buffer。做法是每幀先用極低分辨率把場景里的大型遮擋物比如建筑、地形、大體積的墻光柵化出一個深度圖然后用這個深度圖去測試其他物體包圍盒的投影是否被完全遮擋完全被遮擋的直接剔除。這個方案對城市、室內(nèi)場景收益極其明顯。我們做的一個比較復(fù)雜的開放關(guān)卡純視錐體剔除后DrawCall大概兩千多加上軟件遮擋剔除后直接壓到五百以下幀時間從22毫秒降到11毫秒畫面一點沒變。遮擋剔除也是分層做不要對每個小物件都做精確測試先把場景按區(qū)域劃分區(qū)域級別的遮擋測試結(jié)果直接決定整塊區(qū)域是否丟棄。2.3 剔除結(jié)果要變成渲染任務(wù)而不是逐物體繪制剔除系統(tǒng)輸出的是“這幀要畫哪些物體”的清單但這份清單不能直接拿去畫。直接逐物體繪制意味著每個物體都要單獨提交一次綁定Shader、設(shè)置紋理、設(shè)置變換矩陣的流程DrawCall爆炸不說狀態(tài)切換也炸。正確的做法是把可見物體清單交給渲染隊列Render Queue由隊列管理器按照“減少狀態(tài)切換、最大化合批”的原則重排。排序鍵Sort Key通常是一個64位整數(shù)高位是渲染通道不透明、透明、天空盒、后處理等中間是材質(zhì)ID低位是物體在場景里的深度或距離。這樣排序一次同材質(zhì)、同通道的物體自然被排到一起批處理的機(jī)會就來了。批處理Batching分靜態(tài)和動態(tài)靜態(tài)合批針對不動的物體在加載階段把共享同一材質(zhì)的多個物體合并成一個大的頂點緩沖徹底消掉它們之間的DrawCall動態(tài)合批針對小物體每幀嘗試把符合條件的幾個物體打包到一個DrawCall里。這里有個坑動態(tài)合批不是穩(wěn)賺不賠的合批時要拷貝頂點數(shù)據(jù)到臨時緩沖這個拷貝本身有CPU開銷物體頂點多了或者合批數(shù)量上去了省下的DrawCall開銷還不一定夠拷貝的。我們在項目里實際測試頂點數(shù)超過某個閾值的Mesh動態(tài)合批反而更慢。所以合批策略一定要有數(shù)據(jù)支撐的開關(guān)控制不能“開了合批就萬事大吉”。3. 命令緩沖與資源管理渲染系統(tǒng)的質(zhì)感和底氣3.1 命令緩沖不是簡單的命令列表前面提到主線程和渲染線程通過命令緩沖通信。聽著簡單實際做起來很多細(xì)節(jié)。命令緩沖的本質(zhì)是一塊環(huán)形分配的內(nèi)存池里面裝著一條條命令每一條由一個命令頭命令類型、數(shù)據(jù)長度和一段載荷數(shù)據(jù)組成。主線程寫入渲染線程讀取兩者通過幀同步點協(xié)調(diào)不能出現(xiàn)渲染線程讀了一半主線程又回頭覆寫這塊內(nèi)存的情況。常用做法是幀內(nèi)雙緩沖或三緩沖。主線程和渲染線程各自持有這一幀的寫指針和讀指針靠幀序號來同步。幀序號有點像一個“信號燈”渲染線程處理完第N幀的命令后告訴內(nèi)存分配器第N幀的緩沖塊可以回收了主線程這才允許覆蓋那塊的寫入。這個機(jī)制寫起來不復(fù)雜但一旦出問題就是極其惡性的隨機(jī)崩潰數(shù)據(jù)一半新一半舊查都沒法查。我們后來給每一塊緩沖頭部都加了一個magic number和幀ID內(nèi)存回收前做校驗定位過好幾回這類bug?,F(xiàn)代引擎進(jìn)一步把命令緩沖抽象成Render Graph把整幀的渲染過程描述成一張有依賴關(guān)系的異步任務(wù)圖。每個渲染操作Pass聲明自己讀哪個資源、寫哪個目標(biāo)系統(tǒng)根據(jù)依賴自動安排執(zhí)行順序自動合并或跳過無效的Pass還能自動優(yōu)化資源生命周期某個臨時RT只在兩個Pass之間用到系統(tǒng)自動復(fù)用內(nèi)存。這套方案把引擎渲染架構(gòu)的復(fù)雜度提升了一個量級但換來的效率和可控性確實好。第一版不用從Render Graph開始從普通命令緩沖起步按需演進(jìn)這是我個人的經(jīng)驗。3.2 GPU資源管理生命周期比繪制本身更容易出事渲染系統(tǒng)里80%的麻煩不在繪制邏輯在資源生命周期。紋理、頂點緩沖、索引緩沖、渲染目標(biāo)、Shader程序這些都是GPU資源都有創(chuàng)建、上傳、綁定、釋放的過程。資源管理架構(gòu)設(shè)計不好的話跑起來就是紋理花屏、顯存泄漏、設(shè)備丟失。我見過最離譜的問題是顯存泄漏——每幀都創(chuàng)建一個小紋理但從不釋放跑二十分鐘顯存爆了整個進(jìn)程被驅(qū)動殺穿。設(shè)計資源管理系統(tǒng)核心是三條原則所有權(quán)清晰每個GPU資源有且只有一個owner通常是資源緩存的某一項、引用計數(shù)完整GPU資源使用方必須登記引用包含渲染線程和主線程兩邊的引用、延遲回收標(biāo)記刪除的資源等到GPU確定不再引用它時才真正釋放。紋理流送Texture Streaming是個重要課題。一張4K的紋理可能在顯存里占近一個G但畫面上可能只占幾百個像素沒必要全部駐留。紋理流送系統(tǒng)按相機(jī)距離和屏幕占比動態(tài)加載高分辨率mipmap或降級成低分辨率版本同時控制異步上傳不要卡主線程。實現(xiàn)時有一個關(guān)鍵點流送異步加載完成的回調(diào)和渲染幀的同步。我們踩過紋理閃成粉紅色的坑原因就是低分辨率mip還沒加載完渲染線程就把材質(zhì)對象標(biāo)記成“已就緒”。后面加了一個“就緒狀態(tài)按版本校驗”的邏輯只有GPU確認(rèn)用上當(dāng)前版本后渲染才切換到新狀態(tài)。3.3 材質(zhì)與Shader系統(tǒng)別把參數(shù)管理變成災(zāi)難材質(zhì)系統(tǒng)負(fù)責(zé)把Shader程序和一組參數(shù)顏色值、紋理、浮點數(shù)、矩陣組合成一個可渲染的實例。架構(gòu)上的難點有兩個一是參數(shù)怎么高效地上傳到GPU二是Shader變體Variant怎么管理避免爆炸。參數(shù)上傳要區(qū)分常變化參數(shù)每一幀都可能變比如世界矩陣、時間、相機(jī)位置和材質(zhì)級參數(shù)同一個材質(zhì)的所有物體共享比如反照率、粗糙度。材質(zhì)級參數(shù)打包成常量緩沖Constant Buffer一幀只需要上傳一次甚至只在創(chuàng)建時上傳一次而常變化參數(shù)按對象上傳。如果這兩類混淆會造成大量的冗余上傳驅(qū)動層會瘋狂反復(fù)傳相同的常量GPU雖然能扛住但CPU開銷白白浪費。Shader變體的管理是另一個隱藏的坑。一個材質(zhì)Shader光照模型開關(guān)、陰影開關(guān)、霧效開關(guān)、貼圖數(shù)量組合起來輕則幾十個變體重則上萬個。每個變體編譯一次加載時間會被拖慢到讓你懷疑人生首幀出現(xiàn)掉幀和卡頓基本都跟這有關(guān)。我們處理方式是盡量讓Shader程序在運行時通過統(tǒng)一的參數(shù)接口處理可選功能而不是每個功能都開一個編譯宏分支。編譯宏控制在個位數(shù)所有引擎內(nèi)置Shader預(yù)編譯加載階段只綁定Program不觸發(fā)即時編譯。遇到剛啟動時“瓷磚般的卡頓”基本都是沒做預(yù)編譯。4. 光照、陰影和后處理表現(xiàn)力的架構(gòu)支撐4.1 前向與延遲渲染的抉擇不是技術(shù)偏好是產(chǎn)品取向渲染架構(gòu)繞不開前向渲染和延遲渲染的選擇。前向渲染逐物體計算光照光源多了DrawCall和計算量線性上漲延遲渲染先把物體的幾何信息位置、法線、反照率、粗糙度等寫進(jìn)多張G-Buffer紋理然后統(tǒng)一對屏幕上的每個像素算光照光源數(shù)量和像素發(fā)生了關(guān)系而不是物體光源再多只要在屏幕空間算一次。手游和VR強(qiáng)烈偏愛前向渲染因為G-Buffer的多渲染目標(biāo)MRT在移動端帶寬開銷很肉痛。PC上的大型場景則更多考慮延遲渲染因為動態(tài)光源數(shù)量可能幾十上百個。但延遲渲染也有代價MSAA很難直接用于邊緣抗鋸齒、透明物體沒法正常走G-Buffer流程透明的半透明效果依然得用前向渲染補(bǔ)一道、帶寬開銷在低配機(jī)上撐不住。我們最后的架構(gòu)是混合管線的可配置方案不透明物體默認(rèn)走延遲渲染主光路透明物體和特殊效果水面、粒子、貼花走前向渲染的補(bǔ)充Pass。整個管線在引擎初始化時根據(jù)當(dāng)前設(shè)備特性和用戶畫質(zhì)檔位決定走哪條路徑核心光照計算代碼共用同一套光照模型函數(shù)避免兩套光照算法養(yǎng)出兩套效果不一致的“陰陽臉”。這個結(jié)構(gòu)維護(hù)成本高一些但保證了跨平臺表現(xiàn)的可控性。4.2 陰影系統(tǒng)架構(gòu)實時陰影是塊硬骨頭陰影做得好的引擎給人的第一感受是“畫面立體了”。但實時陰影系統(tǒng)架構(gòu)牽扯的東西遠(yuǎn)比多畫一張深度圖復(fù)雜。主流方案是陰影貼圖Shadow Map制從光源視角渲染場景得到深度圖然后主渲染時比較像素的深度和陰影貼圖深度判斷是否在陰影里。這個方案簡單可靠但陰影鋸齒、暗側(cè)閃爍Shadow Acne、彼得潘現(xiàn)象Peter Panning都是需要系統(tǒng)處理的。陰影架構(gòu)真正難的在于大世界里的陰影裁剪與精度分配。一個巨大的場景一張2048分辨率的陰影貼圖要覆蓋整個場景那每像素對應(yīng)的世界尺寸會非常大近處陰影全是鋸齒和浮動。業(yè)界普遍的方案是級聯(lián)陰影貼圖CSM把視錐體從近到遠(yuǎn)切成幾段每段分配一張獨立陰影貼圖近處段分辨率高、遠(yuǎn)處段分辨率低。級聯(lián)數(shù)量一般3到4級每級一張深度圖級別之間的交界處要做過渡否則能看到明顯的“陰影邊界條”。實現(xiàn)CSM時最容易出問題的是跟光相關(guān)的抖動和過渡。我們第一版里陰影在相機(jī)移動時產(chǎn)生高頻抖動排查半天發(fā)現(xiàn)是因為陰影貼圖的相機(jī)矩陣每一幀都會漂移導(dǎo)致陰影紋樣跟著跳。解決辦法是把光源在場景空間的位置按陰影貼圖紋素尺寸對齊Texel Snap讓光源移動時陰影波動以“整像素”為單位發(fā)生變化。這個細(xì)節(jié)在后來的項目里幫了大忙值得專門記下。4.3 后處理鏈路設(shè)計與Render Graph天然的緣分現(xiàn)在游戲的畫面表現(xiàn)一半靠后處理色調(diào)映射Tonemapping、泛光Bloom、景深Depth of Field、環(huán)境光遮蔽Ambient Occlusion、動態(tài)模糊Motion Blur、抗鋸齒TAA一條鏈路串起來。后處理架構(gòu)設(shè)計最容易造成的坑是每加一個后處理效果就多一次全屏RT切換。全屏RT切換開銷其實是很大的更高分辨率下一次Copy和Clear的代價甚至比整場景渲染還高。解決思路是把后處理組織成一條鏈上一個效果的輸出直接作為下一個效果的輸入避免中間反復(fù)拷貝回主幀緩沖。更進(jìn)一步多個效果可以上Merge到同一個Pass里比如Bloom的閾值提取和降采樣可以在一次計算里完成AO和深度重建可以共享中間數(shù)據(jù)。這就是為什么我說Render Graph后處理和它天生契合。渲染圖能在啟動時把整幀的Pass依賴打通自動把全屏讀寫的節(jié)點連接成一條高效的鏈路甚至能檢測到某個Pass沒有實際效果就讓系統(tǒng)自動跳過。如果嫌Render Graph太重退而求其次也要實現(xiàn)一個輕量級的后處理Pass調(diào)度器把上面的依賴關(guān)系用結(jié)構(gòu)體表示出來運行時按序執(zhí)行不要用硬編碼if-else把鏈路寫死在代碼里。5. 渲染系統(tǒng)的排障心得與防崩設(shè)計5.1 幀時間數(shù)據(jù)拆解定位瓶頸的唯一標(biāo)準(zhǔn)答案渲染系統(tǒng)出性能問題最怕的是靠肉眼猜。一說是“場景太復(fù)雜”一說是“Shader太貴”最后發(fā)現(xiàn)都不是。我處理性能問題時第一件事永遠(yuǎn)是把幀時間拆開CPU主線程耗時、渲染線程耗時、GPU耗時分別統(tǒng)計。大多數(shù)引擎Debug工具都能做到但重要的是數(shù)據(jù)期望。如果主線程耗時遠(yuǎn)高于GPU那瓶頸在邏輯層而不是渲染加再強(qiáng)的顯卡都沒用。如果GPU時間高但主線程閑得很那不是換CPU能解決的。拆解之后進(jìn)一步把GPU時間細(xì)分到Pass級別天空盒用了多少、場景主體多少、后處理多少、陰影幾個級聯(lián)各多少。我們遇到過反直覺的情況大量時間不在畫場景上而在全屏后處理的Bloom降采樣上。因為場景面數(shù)很低反而沒事干后處理把全屏反復(fù)掃了好幾遍。把Bloom閾值和降采樣次數(shù)調(diào)優(yōu)以后整體幀率直接提升了20%。沒有這層數(shù)據(jù)拆解這個瓶頸靠肉眼幾乎不可能找到。推薦一套做法引擎內(nèi)置一個幀調(diào)試覆蓋層熱鍵打開后直接在畫面上顯示每個Pass的耗時柱狀圖能把“哪些Pass占了時間”一目了然地呈現(xiàn)。調(diào)試這類東西讓開發(fā)者“肉眼可見”比任何日志都高效。5.2 資源生命周期崩潰的防御性設(shè)計渲染系統(tǒng)的崩潰絕大多數(shù)逃不出幾個類型GPU資源被釋放后還在用、渲染線程訪問了主線程管理的被銷毀對象、命令緩沖被覆蓋讀取。這三類問題都有共性時序競爭。調(diào)試競爭問題沒法純靠看代碼解決必須在架構(gòu)層面做防御。第一道防線是所有GPU資源對象都用Handle句柄而不是裸指針。Handle是一個全局資源表里的索引釋放時把表項標(biāo)記為“懸空”任何訪問到懸空項的操作都在Debug模式下立即報錯而不是等到野指針導(dǎo)致隨機(jī)崩潰。第二道防線是渲染資源必須上引用計數(shù)并且主線程和渲染線程各持一份引用。線程間轉(zhuǎn)移資源所有權(quán)時必須顯式調(diào)用傳遞接口禁止裸地從一個線程往另一個線程塞資源指針。第三道防線是強(qiáng)制打斷點在特定幀號配置一個“在第X幀后讓渲染系統(tǒng)進(jìn)入半同步模式”讓問題出現(xiàn)時可以穩(wěn)定復(fù)現(xiàn)而不是碰運氣。這些防御聽起來瑣碎但渲染系統(tǒng)后期開發(fā)能不能睡得著覺全看這些。越到后面大家比的不是誰畫面好而是誰在改大功能時不容易炸。架構(gòu)護(hù)住的是長期迭代的穩(wěn)定性這比短期性能更重要。5.3 視覺驗證方法不止是“看著沒問題”渲染系統(tǒng)寫起來最可怕的bug是那種“看著沒大問題但就是哪里不對勁”。比如陰影有一幀延遲、法線方向有微小偏差導(dǎo)致高光點不對、色調(diào)映射在暗部產(chǎn)生色偏這些主觀層面的問題無法用數(shù)字?jǐn)嘌詠碜詣訖z測。我的經(jīng)驗是建立一套視覺參考基線。做法很簡單在固定場景、固定相機(jī)、固定畫質(zhì)設(shè)置的條件下每一版改動都輸出一張參考截圖或者一段固定時長的視頻放進(jìn)版本控制系統(tǒng)里。改動渲染相關(guān)代碼時自動跑一遍基線場景用像素差異比對工具檢測差異。任何意外的差異都會在提交前暴露出來而不是等美術(shù)同事在編輯器里發(fā)現(xiàn)“燈光怎么變得怪怪的”。另外一個針對渲染的實用工具是調(diào)試可視化模式線框模式、僅深度模式、僅法線模式、僅基礎(chǔ)色模式、熱力圖模式顯示DrawCall數(shù)量或過繪制程度的色彩疊加。調(diào)試復(fù)雜渲染問題時能有辦法把“看到的畫面”分解成“自己單獨的一層輸出”來查看定位問題的效率會翻倍上升。比如顯示霧效熱力圖能一眼看出某個區(qū)域霧濃度異常是采樣點分布引起的還是算法錯誤引起的。這些“開關(guān)式”調(diào)試可視化做起來成本不高但幾乎是渲染系統(tǒng)架構(gòu)里最值回報的一個功能。最后說一個我個人繞了很多彎路才想明白的體會渲染系統(tǒng)的架構(gòu)永遠(yuǎn)是“先跑通再飛”。第一版就想著一次性把Render Graph、延遲管線、級聯(lián)陰影、紋理流送全部做齊跨越太高結(jié)果調(diào)試時排查問題非常痛苦。更可行的路徑是用最小的管線先把全流程跑通從畫一個三角形開始到畫場景到加后處理每一步都保持整個鏈路是通的、可見的、可調(diào)試的然后逐層疊加復(fù)雜度。渲染系統(tǒng)這東西看起來是“只要最后效果好就行”但其實每一步的中間狀態(tài)都需要肉眼嚴(yán)格驗證否則疊加到最后你分辨不出是哪一層出現(xiàn)了問題。架構(gòu)設(shè)計先解決“讓錯誤無處藏身”再談“讓性能翻倍”順序別反。如果后續(xù)有機(jī)會我打算接著寫一篇文章聊渲染系統(tǒng)之外的資源管理和場景加載架構(gòu)有具體疑問的歡迎留言交流。