分類目錄歸檔:Java

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

FGOW與FMM停止維護

準確說這並不意外,最近幾個月如果你使用過FGOW和FMM鏡像的話,肯定已經察覺到各種問題諸如無法下載之類的,事實上,FMM最後一次維護是在今年3月左右;而FGOW? 我都忘了上一次更新FGOW是什麼時候了...

FGOW(ForgeGradle on Wall)與FMM(Forge Maven Mirror)開發於2014年5月,前者是用於解決一些ForgeGradle的缺陷,比如無法設置Maven源和其他必要文件的下載鏈接,無法像MCP那樣將MC源碼部署到開發目錄;後者是一個非官方的Forge Maven源,搭建在SAE上. 這兩個的組合曾經是很有效的,那麼為什麼突然就藥丸了呢?
  • FGOW最初的設計並沒有什麼可擴展性,只是一個編碼粗暴的針對FG1.1的小工具,而現在FG已經更新到了2.3...在許許多多的底層變動後,要更新FGOW可能跟重寫一遍差不多了...
  • FMM搭建在SAE上,用過SAE的都知道它有一些坑爹限制,比如FetchUrl的8MB大小限制,最初這個問題不嚴重,畢竟鮮有哪個庫能超過8MB,但後來FG打包附帶了原本要獨立下載的第三方庫,體積直接猛增到了13MB,SAE的文檔也誠不欺我,說8MB上限就是8MB上限,一字節也不帶多下的,所以FMM的大文件(準確的說,只有FG的各個快照)一直都是在後台手動更新的...
  • 缺乏存在的意義,FGOW除了重設下載鏈接外,還有個重要功能是部署MC源碼,然而後來有小夥伴指出其實可以直接在項目中建一個同包同名同內容的類,這樣根據類加載順序,實際載入遊戲中的是項目中可供我們隨意修改的那個類,由此一來,FGOW的那個功能就沒什麼用了.
  • 同上,既然FMM沒了,那麼FGOW也卵用了,思前想後,我覺得還是掛代理能一勞永逸地解決所有問題...而zzzz正好又提供了一個公用的SS賬號用來構建Forge工作目錄,所以我就可以棄坑了 (逃)
  • 缺乏足夠的精力去繼續維護--- 呸,直說吧,就是我太懶了 ?
  • SAE是要花錢的,當初FMM剛剛上線時SAE的價格還很實惠,我2012年註冊時送的2000雲豆能用十幾年,然而自從SAE改了Mysql收費政策後,大概1000雲豆只夠用10天...當然這並不是主要原因,一個月30塊錢還是氪的起的.
  • 並沒有大量的關於紫sama藍sama幽幽sama覺sama戀sama和瑪艾露貝莉x蓮子的福利!
那麼現在FGOW和FMM沒了,又該如何配置開發環境呢,之前提到了zzzz搭建了一個公共SS賬號,賬號在他的MCMod教程中(https://fmltutor.ustc-zzzz.net/1.1-配置你的工作環境.html). SS我覺得應該是現在碼農們常備的"工具"了,假如你沒有的話...就去下一個吧! 但是不要用他的鏈接里的SSR,前幾天SSR的作者搞了個大新聞,被婊到刪庫退圈了 (滑稽) 閱讀全文 [...]

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月衛星能不能落地吧! 新年快樂 🙂
閱讀全文 [...]

使用SIMD+CriticalNative在Java中加速矩陣運算

對於遊戲開發來說,一個健壯高效的數學庫是必不可少的,特別是對於3D遊戲而言,動作系統在計算骨骼動畫時會進行數量可觀的矩陣乘法或求逆運算;渲染系統也需要頻繁計算變換矩陣.雖然一次矩陣運算消耗的時間可能不多,但對於分秒必爭的遊戲渲染來說想要力爭60fps、死守30fps底線就勢必不能放過任何一個免費提升性能的機會. 閱讀全文 [...]

從1到100 - 模塊化的跨平台程序

前幾天完成了被當做作業的小程序,名字相當掩人耳目:Finite Digit Summator,一定程度上是向Digital Differential Analyzer致敬,項目被我扔到了Github上,本身並沒有太大應用價值,除了那兩幅從東方AA摘下來的字符畫,以及一黑黑了兩個遊戲的梗.

從設計上,它的項目結構很大程度上參考了我以前的項目,以一個核心模塊囊括主要功能,然後以多個針對不同平台的子模塊負責將功能封裝並展現給用戶,事實上,除了網頁版有一個功能是通過JS重新實現了一遍以外,幾乎所有的使用了兩遍以上的功能都被集成在了核心模塊中,因此可以說下一階段的目標"實現模塊化"我已經完成一半了(笑),剩下的看上去無非是將之前沒來得及上線的安卓端做完,修修Bug,刷刷單元測試之類的.

聽上去通過模塊化來實現跨平台就像當年老一輩眼中實現共產主義一樣簡單,然而一個實際的跨平台項目想要通過模塊化來實現在設計上卻是困難重重,最主要的問題在於硬件的局限性和需求的不同.還記得剛才說的"以前的項目"不? 2個月前的寒假時我開了一個新坑,用Java復刻(或者叫抄襲?取決於你怎麼看待"yet another alternative implementation"這種東西...)一個Era的開源跨平台版,什麼是Era?我放個截圖你大概就能知道是什麼東西了...

era 閱讀全文 [...]

在Gradle中集成Javacc

有時我們會希望在項目中使用一些腳本語言、DSL或特殊格式的配置文件什麼的,雖然已經有一些現成的方案,比如使用Java內置的JS引擎,或者使用LuaJ、Groovy之類的外部庫,但這些不是局限性略大,就是需要附帶龐大的庫,比如FML就附帶了一個Scala運行時庫(以及一個編譯器!),而實際上MC現在又有多少個用Scala寫的Mod呢?看Kotlin最近勢頭這麼火,估計過幾天他們就得附帶一個Kotlin庫了吧...言歸正題,這個時候我們就需要一個自行設計的腳本語言或者配置文件格式了,然而手寫一個Parser確實有一定難度,不過好在市面上有一類神奇的東西:"編譯器編譯器" 閱讀全文 [...]

MCMod教程開始恢復更新

MC1.9馬上就要發布了,按照"總是差一個版本"的慣例(這是哪的慣例啊),教程準備從1.7更新到1.8了 ? 剛才看了眼第一篇教程,文中介紹的Eclipse居然還是4.3...現在第一篇教程已經更新了,最近那麼多人抱怨沒法配置開發環境,現在看來一點也不奇怪(捂臉,那篇實在太陳舊了).

想看看當初給MC1.2寫ModLoader教程時的原始手稿,結果發現找不到了...這種東西還真能丟啊. 閱讀全文 [...]

FGOW1.2.1和FMMv4

Forge在更新到1.8.8之後FGOW1.2.0就不能用了,於是自然而然地就有了FGOW1.2.1,新版本在功能上沒有變化,只是支持了使用ForgeGradle2.1的Forge1.8.8和1.8.9.

下載地址:
SkyDrive:http://1drv.ms/21gcxy5
Dropbox:https://www.dropbox.com/s/ekig3gjx32uz3qp/fgow-1.2.1.jar?dl=0
百度網盤:http://pan.baidu.com/s/1geoIkin


此外,ForgeMavenMirror,也就是我們喜聞樂見的ForgeMaven倉庫鏡像,也更新到v4版本了.
更新內容包括:

  • 緩存了2.0、2.5和2.7的gradle文件,下載地址為"http://forgemavenmirror.sinaapp.com/gradle/gradle-[版本號]-bin.zip",啟用它們的方式是修改Forge(其實現在應該叫MDK了)目錄下的gradle/wrapper/gradle-wrapper.properties文件,將"distributionUrl="後面的下載地址改為鏡像的地址.我之前沒有弄這個是因為我不贊同這樣做,Gradle的文件策略相當有問題,它是根據下載地址的Hash來識別版本的,這意味着不同下載地址的同一版本Gradle(甚至是同一個地址的https和http下載鏈接)會被識別為不同文件,你知道我的機器上已經有4個版本的Gradle-2.7-bin了嗎?也許他們認為多版本並存很有意義,但我覺得僅憑下載地址來區分的多版本除了虐待硬盤以外毫無意義.不過現在考慮到Gradle已經成了GFW的重點關照對象之一,https鏈接幾乎已經連不通了,這裡還是提供了Gradle的緩存.
  • 增加了大量緩存,現在FMM已經可以代替所有的倉庫了!對,你可以刪掉除FMM以外的所有倉庫,經過實測1.8.9可以在只有FMM倉庫和本地Forge緩存目錄的情況下配置.
  • 智能重定向,過去FMM在失敗時只會重定向到Forge的倉庫(files.minecraft.net),現在FMM會重定向到"最有可能"的倉庫,此外,由於Oschina的Maven鏡像復活了,對於Maven中央倉庫的資源會重定向到Oschina的鏡像.
  • 可選的快速失敗,如果你不想要重定向功能的話,可以使用"http://forgemavenmirror.sinaapp.com/mavenff"這個倉庫,它會在沒找到緩存的情況下直接返回404,而不是重定向,這對於想要繼續混合使用其他倉庫的人來說很有用.
  • maven-metadata.xml緩存會在每天(北京時間凌晨1點)更新一次.因此,現在快照版本(Snapshot)又會被緩存了(之前由於maven-metadata.xml不會自動更新的問題,一度取消了快照版本的緩存).
  • 一些細微的優化.
閱讀全文 [...]