OpenShader月(nian)報 #2

咕咕,咕咕咕
(ge le, qi keng le)

時隔了很久之後...終於有了第三篇月報,在上一篇中我說到5月要肝畢設,那麼,之後的時間? 之後的時間確實沒有認真肝OpenShader,有很長一段時間在摸魚,所以這段時間的進度怎麼看都不像半年時間該做出來的...

  • Shadow Caster的裁剪

    在之前,陰影渲染也佔據了不少性能開銷,就像以前我提到在早期版本測試時使用3個陰影圖的CSM時性能就急劇下降了,雖然池化VBO有效降低了渲染時的Drawcall開銷,但在MC準備渲染數據時的開銷依然不少,因此最好的辦法就是在Shadow Caster中也進行一次裁剪,去除掉不能對玩家的視角形成陰影的物體和地形.

    關於裁剪體的計算,先將玩家鏡頭的視錐體朝向光源鏡頭的近裁面做一次投影,在投影得到的二維圖形中找出最外層的邊,每一個邊沿着光源鏡頭的視線方向延伸得到面後,這些面就成了構成裁剪體的裁面.在這個裁剪體範圍之內的物體大部分都能對玩家視角內行成陰影.如果說還有什麼不完善的話,主要是剛才得到的裁剪體不包括近裁面和遠裁面,對太陽光來說近裁面是沒有必要的,而不計算遠裁面就導致了一些本來不會貢獻陰影的物體也被渲染了,會增加一些不必要的開銷,一個未來的改進是選擇玩家鏡頭的視錐體中,正面朝向光源的那些裁剪面,用這些裁面再做一次裁剪,就能去除掉那些false positive了.

    舉例,比如如果這張圖是一個渲染場景,中間高亮的橙色椎體是玩家鏡頭的視錐體,左上方為環境光源及其方向,圓錐、圓柱、稜角球和方塊分別是4個可渲染的物體,那麼在無Shadow Caster的裁剪時,這4個物體全部需要渲染.

    使用現在的視錐構建方法時,圓柱被剔除掉了,因為它即不在玩家視線內,也不會給玩家視線內的物體貢獻陰影,然而方塊仍然會被渲染,儘管它不會貢獻陰影.

    最終目標是製作一個這樣的裁剪體,保留下的物體只有圓錐 - 它會給玩家視線內貢獻陰影,以及稜角球 - 它在玩家視線內.

  • 重新設計的多線程渲染流程
    顯然,在OpenGL中不會有真正意義上的高性能多線程渲染(瘋狂切context確實可以多線程,但它不夠高性能),因此我們設計的多線程渲染流程也和其他的同類產品一樣,努力壓榨出更多的可在CPU端並行處理的東西,比如裁剪、收集渲染信息等,然後將它們分派到子線程上執行,主線程只保留最必要的工作:執行渲染指令.
    那麼,我們先來看看上次月報(17年5月,應該叫半年報了?)時的渲染流程:

    在舊的渲染流程中,整個渲染被分為準備階段和渲染階段,在準備階段程序會收集並配置渲染信息,主要工作分為鏡頭(計算每一個攝像機的位置和MVP矩陣)、地形(統計哪些RenderChunk需要被渲染)和實體(統計哪些實體和TileEntity需要被渲染)這三部分,此三部分有順序依賴,不可調換順序(地形需要鏡頭的信息來進行視錐裁剪,實體需要知道哪些區塊要被渲染). 在舊設計中這三部分都會在準備階段進行,儘管主線程在處理到每個部分時都會將工作分發給工作線程來進行,從而實現了並行化,但這裡還有一個問題 - 主線程被完全浪費了,事實上渲染階段其實無需等待準備工作全部完成時才進行,有些渲染操作並不涉及到地形和實體,比如天空背景和雲朵;此外渲染階段多線程的利用率非常低 - 只用到了兩個線程. 因此我們重構的目標時儘可能縮短準備階段,並盡量提高渲染階段的多線程化程度,最終重新設計的渲染流程是這樣的:

    在新的渲染流程中,只有鏡頭配置必須在準備階段進行,其餘的工作都是在渲染階段進行,在啟動渲染階段時會有三個線程開始工作,主線程等待並處理渲染指令,一個工作線程生成渲染指令,另一個工作線程會串行處理地形的可見性判斷,為什麼這一步要單線程處理? 這涉及到它的工作方式,地形的可見性判斷是用來找出哪些RenderChunk需要被渲染,主要的手段一是視錐裁剪,二是以玩家所在的RenderChunk為起點做一次廣度優先搜索,為了防止搜索時重複搜索已處理過的節點,MC會在RenderChunk中標記一個時間戳,每次搜索到節點時都會嘗試更新時間戳,如果更新成功(節點時戳比系統時戳舊)那麼這個RenderChunk就是未被處理過的,如果更新失敗(節點時戳與系統時戳一致)那麼這個節點就是已經被處理過的節點,在多遍渲染時程序需要多次進行可見性判斷,比如在渲染陰影時,需要有一個pass處理ShadowMap繪製的地形,一個pass處理玩家視角繪製的地形,如果想在此步驟並行處理的話,就需要把時戳換成一個原子整數,然後為每個pass分配一個bitflag,當此pass處理完一個節點時,就給那個原子整數標記上bitflag,顯然,當所有的pass處理完畢之後還得再將那個原子整數清零,在舊的渲染流程中我就是這樣做的,其結果呢? 嗯...確實是並行化了,但是在性能測試中發現更新原子變量的操作非常的慢,特別是最後重新清零的那個步驟,相比之下,之前串行處理時就不需要這個步驟,因為時戳始終是遞增的,在處理完一個pass後,只要將系統時戳再喜+1就能繼續處理下一個pass. 因此再三斟酌後,我將這一步換回了由一個線程串行處理所有pass的可見性判斷. 但是之後的工作依然是並行處理,每當一個pass完成可見性判斷後,程序就會再給線程池分配兩個新任務 - 生成這個pass的地形渲染指令,以及準備實體的渲染信息.

  • 幀緩衝的Pingpong

    以前我在Shadersmod教程中提到過不要在一個着色器中對一個RenderTarget即讀即寫,因為在Texture Barrier出現之前對一個紋理在讀出的同時進行寫入屬於未定義的行為,其結果是不可預料的,因此一般採用的策略是為一個需要即讀即寫的RenderTarget準備兩個紋理,一個紋理作為讀紋理,一個作為寫紋理,每次運行完渲染後交換兩個紋理的職責,原本讀的下次變成寫的,原本寫的下次變成讀的,這個方法被稱為Pingpong. Shadersmod沒有提供Pingpong功能,因此我們只能自行迴避即讀即寫操作(其實迴避的是異處讀寫,在片元着色器中原地讀寫的話在大部分顯卡上實際上是允許的). 而OpenShader提供了Pingpong功能,在默認配置下程序會自動檢測每個stage的幀緩衝和綁定的紋理,如果檢測到一個紋理即被綁定到輸入又被綁定到輸出的話,就會自動啟用Pingpong.

  • 圖像統計 (WIP)

    圖像統計之前我們已經介紹過了,主要是給像HDR之類的後處理特效用,不過...現在的OpenShader的圖像統計功能非常菜雞,僅僅只是達到了Shadersmod的水平,換句話說,就是只提供了獲取屏幕中央深度的功能... 這個功能其實很雞肋,想知道深度緩衝中央的值,直接給我texture(gdepth, vec(0.5, 0.5)).r不就得了嘛! 將這個值做成一個uniform除了卡流水線以外沒任何用處,儘管中央深度可以通過將其替換為紋理採樣來規避(怎麼替換將是下一期月報的內容(豹笑)),但像統計全屏幕平均亮度這樣的操作就只能靠實打實的圖像統計來實現了,因此圖像統計這方面還有不少工作要做...

  • RenderChunk渲染參數緩存 (WIP)

    這個...應該是最讓我崩潰的部分,大部分功能在去年8月末就完成了,但由於兩個折磨人的Bug導致它一直到現在都沒徹底完工.

    簡單地說,在之前準備渲染參數時需要程序逐一獲取每一個RenderChunk的參數,這涉及到了大量的內存訪問,以及隨之而來的cache miss,即使是原本很簡單的操作也會花費大量的時間,渲染參數緩存就是提前將這些參數緩存在一個整數數組中,將所有操作都簡化成數組的複製和整數操作 -- 利用了OpenGL所有對象都以整數句柄的形式存在的特點. 比如渲染一個池化的RenderChunk時需要知道它在哪個VBO中、它的首地址偏移量和頂點數量,這些參數都可以緩存在整數數組中.

    這個優化被證明為是成功的,確實能提高不少性能,然而卻帶來了兩個新bug.

    第一個bug是當玩家向任意一個方向移動一定距離時,就會發現反方向消失的區塊會在面前出現...事先說明,這個bug已經被解決了,某天在我肝了幾宿HOI4後決定再看看能不能解決這個讓我崩潰了兩個多月的bug,結果在靈光一現後(我一直篤信debug的時候靈感是最重要的 233)幾個騷斷點找出原因了,原因是我對MC自帶的RenderChunk表(我對它的稱呼,MCP把那個類稱為ViewFrustum...咳)的理解不充分,我原本以為它會完整地利用所有空間,也就是表中每一項都會是一個可見的RenderChunk,然而實際上MC並沒有充分利用每一個空間,在某些情況下,比如遊戲載入之後玩家向某一個方向持續移動時,會出現某些項中的RenderChunk存在但卻不該被渲染的情況. 原版MC中通過一個RenderChunk中的標記來判斷是否該被渲染,而我在編寫渲染參數緩存時沒能正確緩存這個屬性,結果導致了這個問題.


    遠處那一排非常突兀的地形就是bug所導致的情況,實際上它們應該是在我背後過來的地方的某排區塊.

    而第二個bug...則把我困惑到懷疑人生. 簡單地說,在設計上,當內存池發生變化時,變化的內容(VBO的id、指針偏移量、數據長度)會被立刻刷新入緩存,然而現在存在的問題是在罕見的情況下緩存和實際內容會有一幀的不同步,結果導致因使用了錯誤的參數而渲染出錯.


    出錯的一幀的截屏

    這個bug一直到現在我都沒能解決...最近倒是想出了一個workaround,暫時還沒有實現,看看下一次更新前能不能做完吧 (鴿?)

  • 一些...雜七雜八的東西

    因為沒有changelog,所以更新了什麼東西都是靠翻git的變更記錄來一點點查的(笑),主要是修bug和一些優化,其實現在OpenShader的狀態是三十六拜後就差一哆嗦,然而這一哆嗦就是幾個月都沒哆嗦出來 ? 這段時間一直都在忙別的,看看這倆月能不能把最後一點肝出來吧,下一篇日誌我們會介紹一些native方面的工作--