分類目錄歸檔:圖形學

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方面的工作--
閱讀全文 [...]

OpenShader月報 #1

TL;DR: 棄坑啦!!!

好吧這是開玩笑的,雖然沉寂了很長時間,怎麼看都像是"好難啊不做了"的樣子,但項目確實還在繼續開發中.本來說好1月或2月發一個預覽版本,然而實際上...實際上從1月初到3月初這段時間我基本沒怎麼碰OpenShader的代碼,一方面是家裡有事,另一方面當時我在摸魚兩個即興項目(還特么是兩個??),等想起來"喲我還有一個大坑沒填"時已經是3月中旬了,所以說現在寫一篇月報其實挺符合現實情況.

那麼,首先是關於之前開的那些坑填的狀況:

  • 池化VBO&MultiDraw (90%)
    池化VBO和MultiDraw已經基本完成了,目前現存的問題是內存佔用稍微有點大,似乎還存在內存顛簸的問題,不過這些都是可以優化的. 從表現來看,池化VBO是相當成功的,磚塊渲染的開銷一下子從之前的第一驟降到了第三第四,甚至低於天空渲染和雲渲染的開銷...(誰想得到天空渲染怎麼會有那麼大的開銷??),其實這也不意外,一個池化的VBO能承載16個Chunk(是的,16x256x16的那玩意,不是16x16x16的RenderChunk)的頂點數據,並用1次綁定加1到4次DrawCall(取決於這些Chunk的位置,大多數情況下是1~2次,極端情況會是4次)將它們全部渲染出來,在過去這需要16次綁定和16次DrawCall,而且實際情況會比這還要多 --- RenderChunk只有16x16x16,在山地之類的立體地形中渲染1個Chunk需要渲染多個RenderChunk --- 不過也有可能比這少,畢竟有些RenderChunk可能不可見.但無論如何,總體開銷都不可能比池化VBO+MultiDraw要低. 在實現VBO內存池時我遇到了個吔屎的問題,我忘了OpenGL中大部分DrawCall類指令的偏移量單位是頂點而不是字節...這意味着頂點大小不同的頂點數據不能存入同一個內存池,因為數據的偏移量必須是頂點大小的整倍數,否則在DrawCall中沒法指定偏移量,而頂點大小不同的數據存在一起時對齊會變得非常麻煩,因此最後我設計成使用多個內存池來存儲16個Chunk的數據,每個內存池只存儲特定一種大小的數據,問題解決了,雖然浪費的內存變得更多了... 不過還有一個小問題是現在KHR_Debug_Callback返回的Debug信息中經常會狂刷"Pixel-path performance warning: Pixel transfer is synchronized with 3D rendering." 主要發生在內存池擴容刪除舊緩存時,這是什麼鬼...
  • 指令隊列 (100%)
    指令隊列順利完工了,但是表現的卻不是很好,性能感覺提升了不到10%...而且Debug變得異常艱難,因為報錯後只知道是在哪種指令中出錯了,甚至都不知道是哪個Stage提交的哪個指令...不過也不意外,Nvidia在AZDO(Approaching Zero Driver Overhead)那篇presentation中提到他們用軟件實現的指令隊列的性能提升也只有14%. 現在只能期待後繼的優化中將儘可能多的工作塞到指令準備時並行執行了,畢竟目前渲染準備階段佔用的時間也挺多的,如果能砍掉這部分時間(無論是優化還是並行執行)那對性能也是有很大的提升.
  • Early-Clear (0% 蛤蛤蛤)
    這一個完全沒弄,主要是暫時沒有特別大的需求,但今後肯定會有的.

正在進行的工作/已完成的其他工作:

  • 清理臨時代碼 (90%)
    花了幾天清理了很多臨時代碼,主要是ASM那方面,之前在開發時遇到需要修改MC代碼的情況時,就把MC代碼複製到項目中的同名類里,利用Java類加載順序的特性來實現覆蓋原始代碼.現在用ASM徹底重寫了這部分了,為即將到來的開源做好了準備. 至於其他方面的臨時代碼(大量的TODO) 慢慢解決吧...
  • 採樣器 (100%)
    採樣器決定了着色器如何從紋理中獲取紋素,在過去(去年12月的版本)其實已經有採樣器的原型了,但是只能用來控制着色器讀取哪些紋理,而不能控制着色器怎麼讀取,現在開發者可以設置採樣器的參數了,包括紋理過濾、各向異性過濾、Wrap、Mipmap以及是否開啟比較模式.採樣器有什麼用呢? Shadersmod中啟動shadowHardwareFiltering其實就是開啟Shadow map的比較模式. 採樣器還有兩個版本的實現,一個是針對低版本的傳統模式,通過glTexParameter修改紋理的採樣參數來實現;另一個是針對擁有ARB_sampler_object擴展的高版本,通過採樣器對象來實現.
  • 自定義Uniform (50%)
    雖然OpenShader內置了不少Uniform參數,但畢竟總有我想不到的時候,因此總得有一種方法能讓光影包開發者自行設定Uniform,目前的解決方案是讓開發者可以添加一個自定義Uniform,然後監聽更新事件並手動更新Uniform內容,這樣的設計有兩個缺陷,一個是只有Mod式的光影包能寫代碼來處理事件監聽,另一個是MinecraftForge的居然要求事件總線的註冊要在Mod初始化中進行,否則就會拋一個警告信息...我在考慮要不要設計一種DSL讓開發者通過簡單的表達式來描述需要獲取的內容;或者利用Java的ScriptEngine內置JavaScript的特性,讓開發者寫JS腳本去獲取數據.不過這些都無法迴避一個問題,就是運行時Minecraft的字段都是混淆的...如何訪問這些被混淆的字段還是個問題.
  • 圖像統計 (25%)
    簡單地說,圖像統計是用來獲取渲染結果中的某些信息,比如平均亮度(用於HDR)或中央深度(用於DOF)什麼的,這個操作主要面臨的問題是如何將獲取到的結果再遞交回去,比如說將統計到的結果再注入到一個Uniform里,這是最簡單的方式,不過會涉及到CPU-GPU同步的停頓,對於這個問題,國產的開源引擎KlayGE的解決方案是使用計算着色器來統計亮度,然後將結果寫入一個紋理的固定位置(DX的左上角(0,0)),後續處理時就直接從此紋理的固定位置讀數據就行了,整個操作可以一氣呵成,無需CPU從GPU那裡讀回數據,頂多有必要的話加一個屏障保證HDR發生在亮度統計之後即可,寫入紋理在OpenGL中對應的操作是Image Load Store,不過假如讓我來實現這個的話,我可能會選擇SSBO,因為Image Load Store到4.2才進入核心擴展,而4.3就有了SSBO了... 嗯,然後你問沒有計算着色器的辣雞該怎麼辦? 把紋理慢慢讀回內存然後讓CPU慢慢跑吧,同步的停頓想着就感人...
  • 兼容Shadersmod光影包 (60%)
    聽上去像是一個很不可思議的事情,但考慮到OpenShader在設計時就是具有高度可配置性,那麼Shadersmod的光影包不就是一種特殊格式的OpenShader光影包嗎(笑),這個月我主要就是在弄這個,期間也找出並解決了OpenShader不少問題.目前的情況是已經能跑我在光影包教程中寫的那個MyFirstShader了(不過還存在一些小問題),至於別的...我試了一下SEUS,看上去能跑,但是總感覺有些差別,特別是炫光效果經常會閃爍,估計是紋理採樣參數有差別;體積雲也會有斷層,估計是噪音圖的Wrap沒有設置成REPEAT而導致;流體磚塊的Attribute注入也跪了,滑稽 23333
  • 無盡的Bug... (1%)
    還有很多問題需要修復,有些是缺乏邊界條件時的處理,正常使用沒問題,但如果使用姿勢稍有不對就 --- DUANG! 還有一些就是徹頭徹尾的Bug,比如原版MC的RenderChunk載入是使用一個優先隊列,距離玩家最近的未加載RenderChunk永遠是會被最優先處理的,這保證了玩家在移動時自己身旁的RenderChunk總是被加載出來的,而現在這個設定貌似壞掉了,變成了一個先入先出隊列,結果就是當玩家都已經跑進未加載區域了,那邊還在吭哧吭哧地加載舊區塊...
  • 開源! \o/ \o/ \o/
    說好了的開源項目,怎麼遲遲沒有開源呢? 最大的阻力:臨時代碼已經被消滅了,我打算等Shadersmod兼容差不多實現了後就傳到Github上,啊當然是創一個新倉庫了,我怎麼會把現在的私有倉庫中羞恥的舊代碼亮出來! (笑)

差不多就是這些,我去肝畢設去了(又是棄坑一段時間的節奏?),學校突然把畢設報告從6月提到了5月,藥丸藥丸啊 ?

4.26更新

基本能跑SEUS 10.1了,除了沒有DOF (因為還沒實現圖像統計) 但是有個奇怪的問題就是亮的吃屎...想不出這是什麼原因啊...

4.28更新

能跑SEUS10.2了,SEUS11.0也差不多不過水麵有些問題.10.2屏幕過亮的問題是eyeBrightness弄錯了,此外還修掉了一些奇怪的問題,不過依然有一些光影會跪的比較慘,群眾喜聞樂見的Chocapic13 V6天空渲染會莫名其妙地壞掉.

另外還有一個問題可能要在很久之後修復,甚至是不修復...就是矩陣精度問題,Minecraft一直是直接用glTranslate、glRotate之類的GL函數直接操作OpenGL矩陣,而OpenShader則提供了一個封裝(通過ASM魔改1.8新增的GlStateManager來實現,具體原理篇幅有限先跳過了 233),對矩陣的操作會先緩存在應用層,到下一次DrawCall之前再通過glLoadMatrix更新到驅動,這個可以降低GL函數調用的開銷,然而帶來的問題是OpenShader的矩陣精度好像差了一點點...

這"一點點"對常見的矩陣(模型視圖矩陣和投影矩陣)沒有影響,但對法線矩陣(gl_NormalMatrix,還記得我在Shadersmod教程的附錄里提到的計算方法嗎?模型視圖矩陣的左上3x3的逆的轉置)有着微妙的影響,由GL函數得到的模型視圖矩陣算出的法線矩陣在乘完法線後,似乎能近乎神奇地保證法線的單位長度依然保持在1左右,至少是小於等於1;而OpenShader的矩陣呢,乘完後長度稍微大了那麼一丁點...非常非常小的一丁點,但在某些魯棒性較差的算法里就會跪了,比如說我的那個Shadersmod教程里的MyFirstShader...

MyFirstShader里用到了一個將vec3格式的法線編碼成vec2格式的算法:

vec2 normalEncode(vec3 n) {
vec2 enc = normalize(n.xy) * (sqrt(-n.z*0.5+0.5));
enc = enc*0.5+0.5;
return enc;
}

...

vec2 normal = normalEncode(gl_NormalMatrix * gl_Normal);

最要命的地方在於那個sqrt(-n.z*0.5+0.5),它假定法線的長度一定不超過1,因此z一定是介於[-1,1]的,由此它才可以放心大膽地計算sqrt(-n.z*0.5+0.5)而不用擔心-n.z*0.5+0.5小於0時sqrt蹦個NaN,但是由於矩陣精度問題,這裡的法線長度超過1了,於是n.z=1.00001, -n.z*0.5+0.5=-0.000005, sqrt(-0.000005)=NAN,完.IEEE754規定NaN參與的運算都會變成NaN,就像JS中的undefined那樣毒性十足,於是整個算法就跪了,編碼出的法線在特定的角度(一般是平面垂直玩家視線時)會毫無徵兆地突然崩掉.解決的方法其實非常簡單,給gl_NormalMatrix * gl_Normal套個normalize就行了,幸運的是市面上幾乎所有光影包在獲取法線時都是用normalize(gl_NormalMatrix * gl_Normal) (你問我的那個教程光影? 它可不算"流入市面"哦?),因此基本不用擔心這個問題,所以我也不太急着修復它...(更何況我也不知道如何修復 hehe)

4.29更新

試了一下發現Chocapic13 V3也會跪掉,原因是Chocapic13 V3使用了一個Shadersmod非常罕見的特性:DRAWBUFFERS中允許空白佔位符,比如DRAWBUFFERS:NNN1N2就是gl_FragData[3]向colortex1輸出顏色,gl_FragData[5]向colortex2輸出顏色,而gl_FragData[0]、[1]、[2]和[4]不向任何RenderTarget輸出.我當時沒料到還有這種操作 233 V6跪掉的原因是Optifine內置的新版Shadersmod似乎支持在一個pass中向一個緩衝同時即讀即寫了,奇怪的是它的文檔中卻明確提到"Writing to color attachments that the composite shader also reads from will generate artifacts" 難道他自己搞了大新聞結果忘了更新文檔了嗎 ? 其實針對即讀即寫的情況啟用Ping-pong我之前已經打算把它作為OpenShader相對Shadersmod而言的新特性了,不過因為需求不大一直沒實現,現在看來是被Optifine將了一軍了 233

閱讀全文 [...]

OpenShader月報 #0

雖然沒有做paperwork的習慣,但我覺得在這時候還是寫一篇總結這個月工作進展的文章比較好,畢竟衛星放出來如果沒時不時地搞點大新聞的話總會讓人覺得棄坑了是吧 233
  • RenderChunk更新優化
    眾所周知MC的地圖是以16x256x16的Chunk - 即所謂的區塊 - 為存儲單位,但在渲染時是以16x16x16的RenderChunk為單位進行更新和渲染,問題在於RenderChunk的更新並不是很快,在i5-4590的機器上大概需要6ms每單位,雖然MC已經引入了多線程更新RenderChunk的設計,但這隻局限於由地圖加載導致的更新,由磚塊變化產生的更新依然要求單線程更新,這就導致了當發生大規模的磚塊更新時遊戲幀率會驟然下降,比如大規模流水或接入高頻紅石電路的紅石燈等.
    RenderChunk的更新操作實際上是遍歷區塊中16x16x16的部分逐磚塊地進行可見性判斷,然後將磚塊的模型面填充入頂點緩衝中,其中獲取光照度和獲取磚塊的IBlockState這兩步操作開銷莫名的大,而在原版的更新流程中它們的調用又十分頻繁,因此在此做一個簡單的緩存就能戲劇性地加快原版更新速度.
    然而Forge還增加了它自己的更新流程,被稱為ForgeLightPileline,顧名思義,它是解決在複雜模型下原版光照錯誤的問題,在默認情況下它是取代原版流程的,它的速度跟未經優化的原版差不多,然而它的可優化空間實在不大,一堆堆的三維數組像節日綵球一樣在程序里傳來傳去還要不停地拆包打包可真沒有什麼優化手段,但是沒有它又還真不行,因此這裡採用的措施是增加一個判斷器根據被渲染的磚塊的模型類型來判斷該採用哪個更新流程,毫無疑問這一步並不快,因此加入了一個LUT緩存,採用打表查表的方式來加速判斷.
    此外還有一個被臨時取消掉了的優化,是讓上文提到的發生變化的磚塊也進行多線程更新,這一項優化效果斐然,但是卻存在兩個巨大的缺陷,一個是在首次進入遊戲時有大概5%的概率因為某個競態條件而陷入卡死 - 提交入隊列的更新任務莫名其妙地沒有被執行,這個問題我調查了幾天也沒有結果;如果說它可以通過一個條件變量強制在首次更新時採用單線程來"解決"的話,下一個問題根本無法繞過,在更新區塊時,某些BlockRenderLayer的變化要到下一幀才會反饋,首先要解釋一下BlockRenderLayer(以下簡稱layer),從1.8開始MC中的磚塊被區分為4種layer: SOLID(無Alpha測試,有Mipmap)、CUTOUT_MIPPED(有Alpha測試,有Mipmap)、CUTOUT(有Alpha測試,無Mipmap)和TRANSLUCENT(開啟混合). 在渲染時MC會將按照這4個layer逐層渲染磚塊,原因很顯然,不同的layer需要不同的渲染狀態.而在引入多線程更新後遇到的問題就是,當區塊發生更新時,某些layer中的頂點變化總會慢一幀才能表現出來.
    比如,草地的layer是CUTOUT_MIPPED,石頭和泥土地是SOLID,當你敲碎一個放置在草地旁的石頭後,石頭破碎的地方不會被填上草地,而且會露出一個空洞,這個空洞直到下一幀時才會補上,然而奇怪的是設斷點會發現更新只發生了一次並且是發生在敲碎石頭的那一幀,這難以解釋為何一次更新的效果直到下一幀才會生效,在更新完畢後將數據從客戶端(CPU/內存)上傳到服務器(GPU/顯存)的操作無論是單線程版本還是多線程版本都會被留在主線程進行,或許是由於內存可見性什麼的問題,但不管怎麼說,具體原因始終未能確定,因此只能暫時取消掉多線程更新這個優化.
    圖:在擊碎磚塊的瞬間留下的空洞,這個空洞在下一幀即被填上了
  • 自定義磚塊貼圖
    Shadersmod增加了法線貼圖"_N"和高光貼圖"_S"兩種額外的磚塊貼圖,現在OpenShader也完成了新增磚塊貼圖的功能了,開發者可以自定義要載入的貼圖的後綴名以及當無貼圖時的默認值,比如法線的默認值可以採用0xFF7F7FFF(BGRA格式的(1, 0.5, 0.5, 1)). 不過顯然在切換光影包時重新載入一遍貼圖會很慢,因此Mod提供了一個選項是提前緩存_N和_S兩種後綴的貼圖並且在切換光影包時不卸載它們.
  • 界面&選項
    現在已經有了一個類似Shadersmod那樣的光影包菜單,並且實現了切換或重載,理論上講Mod現在已經可以正常使用了.
  • 多線程渲染準備
    在進行正式渲染之前,MC需要先準備渲染數據,比如計算鏡頭數據、對場景做視錐裁剪等,這一部分在之前是單線程完成的,現在這一步是以Pass為單位進行多線程處理,當Pass數量小於等於CPU核數時,渲染準備所需的時間就從"所有Pass準備時間之和"變成了"最慢的Pass的準備時間",也算是省出了一些時間吧...
正在進行的工作:
  • 池化VBO&MultiDraw (60%)
    當我在測試用的光影包中嘗試實現一個樸素三重CSM(不根據視錐做偏移,陰影鏡頭始終對準玩家)時發現性能急劇下降,從Profiler的結果來看一個問題是最大視距的那個陰影鏡頭所需要的準備時間太長了,另一個問題是磚塊繪製過慢,在10視距時,玩家視角與3個ShadowMapping過程總共需要繪製1000+次RenderChunk,繪製一個RenderChunk時還要為每個layer綁定一次VAO再做一次Drawcall,這樣算下來每幀會需要數千次Drawcall和等量的VAO綁定,性能瓶頸妥妥地是在驅動上,事實上確實當顯卡佔用沒增加多少而CPU卻有一個核心已經跑滿了. 因此問題在於如何降低調用數量,N卡有個BindlessMultiDrawIndirect,可以在不綁定VAO/VBO的情況下進行單指令多渲染,但顯然我們不能指望一個供應商獨佔擴展...因此我把注意力集中到了最簡單的MultiDraw上,MultiDraw是個很早的特性,我記得好像是GL1.5時代就有了...它可以一次指令繪製一個幾何體中多個不連續的部分,而缺點是中途不能切換紋理等,顯然這對MC來說不是問題,關鍵在於如何將多個RenderChunk中的幾個layer的頂點數據都整個在一個VBO中.
    我的解決方案是以4x16x4個RenderChunk為一組(也就是覆蓋4x4個Chunk)使用一個手動維護的內存池來共同存儲所有layer的頂點數據,這個內存池的實體是在顯卡顯存中的一個VBO,在客戶端程序會負責空間分配以及數據傳輸等工作,當RenderChunk向內存池首次提交一次數據更新時,內存池會根據數據體積分配一段略大於數據體積的空間,然後返回給提交者一個指針(有意思的是,這個指針是可變值(Mutable),我知道一個可變值指針聽上去即不可思議又噁心,但這確實能省下不少事),同時會在一個SortedSet中記錄已分配的指針,有個例外是空數據提交產生的指針,這種指針不會被插入到那個SortedSet中.
    每一個內存池都要面臨的問題是碎片整理和擴容,理論上這個內存池可以通過glCopySubBuffer來整理碎片,只要將末尾的內存複製到前面的空隙即可(glCopySubBuffer有個特性是複製源範圍和目標範圍不能重合,這導致了複製空隙緊後面的數據到前面來,以"冒泡"的形式整理碎片的方法不能用了),但我暫時還沒實現,現在的做法是在擴容的同時順便進行碎片整理,glBufferData在擴容的時候不會保留舊的數據,因此得先分配一個新的VBO,然後把數據複製過去,再銷毀舊的.在複製時如果遇到空隙的話就會跳過去,同時糾正之後的指針的偏移量(這也是為什麼它是可變值)
  • 指令隊列 (30%)
    鑒於OpenGL要求必須在主線程渲染的特點,壓榨性能的方式之一就是將渲染數據的整理工作通過多線程來處理,主線程只負責指令提交工作,指令隊列響應了這一思想,所有渲染操作都被分類為一些指令,一個阻塞隊列負責緩存這些指令,協線程負責收集數據和生成渲染指令,並將指令送入隊列,主線程從隊列取出指令並執行,這應該可以一定程度上提高性能,畢竟擁有更高的並行度,並且主線程執行的工作更單一,不會受CPU緩存污染的困擾. 現在,唯一的問題在於,從隊列中讀取指令並執行的開銷會抵消這些提升嗎...
  • Early-Clear (0% :P)
    上文提到了渲染和數據準備是分開的,但事實上有一種渲染操作"基本"無需數據 - 清除緩衝,說它"基本"是因為主視角的清除顏色緩衝還依賴於霧顏色,而霧顏色的計算是在相機計算中進行的,因此可以在完成相機計算後主線程提前開始為各個Pass進行緩衝清除,而協線程繼續準備其餘的數據.
  • 很...很多... (紫asdfghjkqwertyuiozxcvbnm%)
    顯然今年沒Demo了! 看看明年1月衛星能不能落地吧! 新年快樂 🙂
閱讀全文 [...]

[新坑預告] Yet another shadersmod implementation

最近閉(zhuang)關(si)了幾個月,我並沒有被Hello Games幹掉(順便在這裡祝賀他們沒有跑路而堅持在Steam黑五打折期間出了個主要更新,以及敢於向玩家們彈小窗的勇氣 - 他們是我見過的第二個向玩家彈窗發送更新通知的遊戲廠商,第一個是製作MachineCraft的G2CREW) 這4個月我基本都在過着6點睡2點起(嗯,早晨6點,下午2點)每天見不到幾個小時的太陽的生活,在肝膩了文明V(沒有I)、艦R、崩3、WT後我終於決定該寫點什麼,正好手頭這個秘密開發了5個月的坑終於有些眉目了,因此決定放個預(wei)告(xing),有人說把坑公開出來就能督促自己不棄坑,然而對我這樣的臉皮略厚的人來說好像並沒有什麼用...2014年我公布了AsmEventBus(基於ASM的Java事件總線系統,比Guava那個基於反射的系統要快很多)和一個沒公開名字的Java模塊系統(如果稱它是"類OSGI"顯然有些裝逼,但它做的事確實和OSGI差不多) 然後理所當然地坑了,其實對於前者我挺耿耿於懷的,明明只要再完善一點就是個很好的庫...今年年初在日狗的軟工作業要求下不得不放了個某文字H遊戲引擎的Java開源復刻衛星,然後,嗯,然後沒下文了.寫到這時我自己都忍不住掩面笑了一下,看來羞恥柱這種東西對我這樣倒錯的人來說並沒有什麼監督作用.其實我挺懷念11~12年的那段時光,那段不知失敗為何物敢於寫任何自己想寫的代碼的日子,在11年的最後一個月我在這個房間的同一個角落半生不熟地用C#寫一個文字獵奇遊戲,用慢的吔翔的GDI+在WinForm上畫文字(現在一想其實挺像ERA啊,當時我要知道ERA的存在的話是不是就給ERA寫腳本去了?) 最吃精(?)的是當時我居然在試圖寫一個自己設計的腳本語言的解析器,幸好當時沒做出來,不然這足以讓現在的我感到自愧不如,有人說好的程序員應當在看到自己6個月以前寫的代碼時能發覺自己現在的進步,這麼說我應該儘快刪IDE退圈了. (笑) 不過我倒真的挺懷念那個項目,畢竟能親手(即使是只有文字)肢解幻想鄉的女孩子怎麼想也是一件刺激又有趣的事情,有機會的話我一定要把它復刻出來. (啊呸)

懷古傷今的時間到此為止了,現在說說手頭上這個坑,當時提出做MC光影Mod的復刻這個概念是在什麼時候已經無從考證了,印象中是14年7月我和ici2cc給CustomSteve做光影Mod兼容時第一次提出了自己做光影的想法,不過當時由於很"容易"地完成了CS與光影的兼容工作,因此這個念頭就被打消了.一年半後的16年2月我寫完了光影包教程後和ici侃大山時聊到了光影Mod的種種不足,當時我開玩笑地提出自己也打算寫一個光影Mod,ici沉默半響後問道你是認真的嗎,這時我才開始真正考慮這件事,具體的討論過程記不清了,結論大概是坑太大填不起,而且當時我剛完稿光影Mod教程很累,也並不想就這麼立刻廢掉自己的工作成果,更重要的是我想去填一個自己之前挖出來的大坑(這個坑不想提了...也不用猜,"基本上"從未公布過) 因此這件事就被放下了,時間到了6月,那時不知在哪我看到了一個消息,Continuum光影包的作者在用C++給MC寫一個渲染器,不用說也知道它會支持第三方光影包,當時就把正在補伊里野的天空的我嚇得把播放器關了,為什麼呢,因為當初我被Continuum的作者肛過一次(大霧) 2月初我在撰寫光影包教程的最後兩章時ici彈小窗告訴我"被搶先了",當時嚇得我差點提前去見幽幽子,ici趕在我失神之前發來條鏈接,我趕緊緩過來點開一看是個油土鱉視頻列表,上面三四片Continuum的作者錄製的光影包教程視頻,簡單地看了一下後我半自我安慰地得出了個結論:(局座臉)這教程,飛不起來! 本着公平競爭的原則,這裡貼出視頻列表地址,為什麼那麼說呢,我感覺他過度死扣光照、ToneMapping和PBR,而忽略了對光影Mod特性的介紹,最簡單的例子,除了我的那篇附錄以外還能從哪找到對光影Mod的技術規範文檔呢,恐怕官方Wiki都沒有這麼詳細,然而話是這麼說,但畢竟人家已經搶先發出來了,"第一個光影Mod教程"這個頭銜是搶不到了,只好奮筆疾書去搶"第一個成文的光影Mod教程" (笑) 這也是為什麼我的教程中最後兩章明顯的很潦草的原因 (當然,我寫煩了也是一個原因...) 後來我的教程發出來了,而他的教程棄坑了,我還順手打了兩發對他的黑槍 (誒嘿,我這人咋就這麼愛打黑槍呢) 一個是他那個"號稱世界最強卻實際只是又一個C13衍生品的光影包",一個是他開的"MC的Vulkan渲染器"坑.故事就看似告一段落了,然而我萬萬沒想到的是他那個Vulkan渲染器在知難而退後又蛻變成了"C++寫的OpenGL4.5渲染器",然後又把我肛了一次!被一個人肛兩次這事能忍嗎!

然而當時是考試周,我只能忍下去了 (笑) 考試結束後我開始探究技術可行性(後來證明這幾乎是無用的,所有遇到的問題這時都沒發現) 並於7月的實習第一天在那間位於商住一體樓里的破房子中建了項目的Git倉庫,版本記錄顯示前三天我寫的代碼除了Mod主類和Coremod的LoadingPlugin以外沒有一行保留到了現在 (手動滑稽) 一方面說明了那個"百分之多少(記不清了)的代碼是要在一年內被重構掉"的理論是正確的,另一方面說明了當時我是有多麼低估了問題,雖然那次糟糕的實習讓我失去了去CJ和Jeb見面(對此ici可以吹一輩子(笑))以及勾搭上養豬場的機會並且錯過了魔都THO,但如果說那一個月實習有什麼用的話,那就是讓我塌下心能用當初徒手肝解析器的勁頭從零製作"Yet another shadersmod implementation",不,它不叫"Yasi"或什麼的(雖然現在看起來還挺酷!) 也不叫ShaderCraft云云,我將其命名為OpenShader,因為Open這個詞對於開源程序員來說就如同貓薄荷對貓一樣充滿魅力,顯然這是開源的,不過我將它暫時託管到了私有倉庫中,因為當初我也實在不敢確定它可以完成,畢竟我之前失敗過的太多了,事實上,直到11月初時它甚至還沒法正常運行,而我最初的計劃是10月初給出一個Demo...當初我寫光影包教程時也曾"計劃"在10月初完稿,不過現在看來這次我似乎不必拖到次年2月了,然而Mod維護是一件長期的工作,不是嗎?主要的技術突破都是在11月中完成的,現在它已經實現了:

  • 高度自定義的渲染管線,可自定義每一幀(Frame)渲染時採用的Pass,以及每一Pass包含哪些繪製階段(Stage)
  • 着色器加載系統,包含一個簡單的Includer實現(無需那個ARB擴展).
  • 可自定義的幀緩衝和幀緩衝的掛件(顏色、深度、模板),包括掛件的尺寸和格式.
  • 頂點着色器的頂點屬性(Attribute)注入.
  • 一致變量(Uniform)注入.
  • 優化的GlStateManager,儘可能地減少OpenGL調用.
  • 特性(Feature)系統,用於開啟、關閉或改變一些MC的功能與屬性,比如設置太陽偏斜(光影Mod的sunAngle)
  • 使用VAO渲染區塊 (WOW!) 理論上講90%+的顯卡都支持ARB_vertex_array_object和APPLE_vertex_array_object中的一個,如果有哪個辣雞卡兩個都不支持,那它也不見得跑得動着色器.
  • 支持Mod式和外部文件式的光影包(其實只實現了前者)

當然,它還有一個不短的TODO List,由於沒有寫TODO List的習慣(??)這裡先隨手寫上一些能想到的:

  • 紋理系統,比如光影Mod的載入法線和高光紋理,以及載入外部紋理文件,可以搞基於LUT的顏色校正啦.
  • 用戶界面.
  • 外部文件式的光影包的解析.
  • 資源管理...
  • 條件允許時使用UBO更新一致變量.
  • 世界上有兩群人需要人間會社程序員的人間關懷:烏干達的可憐兒童,和蘋果機用戶.
  • 還有一些瘋狂的念頭,不過都是要在前面這些完成的前提下才有...
  • 大量的,關於紫sama藍sama幽幽sama覺sama戀sama秦心醬和瑪艾露貝莉x蓮子的福利

想說的暫時是這麼多,如果成了的話喜大普奔,再一次棄坑了的話那就又是一次喜聞樂見的自掛城牆,我先睡覺(jue)去了...這裡貼一個Mod式光影包的光影包初始化代碼和渲染管線構建代碼的一部分 (群眾:有毛用啊!) 可以大概了解一下它的API風格,畢竟將來有了外部配置文件式的光影包後,基本上也會跟它差不多:

53057998_p0

不好意思,貼錯了...



20161203071732 20161203065401

這個才對 √

閱讀全文 [...]

[3D圖形學]視錐剔除入門(翻譯)

最近在學3D圖形學...看到篇不錯的視錐剔除入門教程,於是搬來翻譯了...

時至今日,許多剛剛下海的3D引擎程序員仍不了解視錐剔除(Frustum Culling)的重要性和益處,這讓我和我的小夥伴們感到很震♂驚.我在Flipcode論壇中發現儘管網絡上有海量的相關資料,仍有許多人提出對視錐剔除實現的問題.因此我決定撰寫這篇文檔,簡單描繪出我現在所使用的四叉樹剔除引擎(Quad-tree Culled Engine)的工作方式.誠然,市面上有許多種成熟且高效的視錐剔除算法,但我認為這個算法足以用來學習視錐剔除的理論基礎.在正式開始前我還想說明一件事,以前我一直把Frustum(平截頭體)打成Frustrum(截頭錐),為此我沒少被論壇上的人噴.在這裡我承認Frustum是正確的拼寫.對那些以前被我冒犯的人我表示抱歉...你們這群吹毛求疵的傻[嗶-]...

大多數人已經知道什麼是視錐剔除了(譯者:如果你是手滑誤點進來的...視錐剔除是一個圖形渲染前的步驟,用於剔除掉不需要繪製的部分).視錐(準確說是平截頭體Frustum)的形狀酷似一個塔尖被削平了的金字塔,更準確地說,是一個四稜錐的頂點偏下位置被一個裁面(Clipping Plane,見圖1)裁斷.事實上,視錐本身就是由6個面所組成.這6個面被稱為近裁面,遠裁面,上裁面,下裁面,左裁面,右裁面.視錐剪裁僅僅是一個用來判斷物體是否需要被繪製的過程.儘管從本質上講視錐剔除應該是三維層面的,但事實上大多數時候它僅僅需要以純代數的方法便能解決.這也是為什麼我如此推崇視錐剔除的原因,它非常的快(如果算法好的話),而且是在渲染管線(Rendering Pipeline)之前進行的,不像背面剔除(Backface Culling)那樣需要在渲染管線之後一個頂點一個頂點地計算.對於被剪裁掉的物體都不會將其送入顯卡(譯者:那是...被剔除掉的壓根都不用渲染),因此視錐剔除對渲染速度有巨大的改善,畢竟什麼都不渲染是最快的渲染... 閱讀全文 [...]