最近在工做中愈來愈多地接觸到一些3D以及相比常見特性更酷炫的效果,所以萌發了想要本身從0開始打造一個渲染引擎的念頭,一方面是爲了更好地實現公司業務的需求,另外一方面則是能夠學到整個渲染流水線上的方方面面。html
由於以前作的大部分都是在特效這塊,對OpenGL會比較熟悉一些,可是放大到渲染引擎上就不少方面不熟悉了,甚至有些是徹底0基礎,在給本身今年定下這麼一個目標時也是一頭霧水,有種瞬間不知道該向哪裏邁出第一步的感受。所以這幾天基本都在尋找和渲染引擎甚至遊戲引擎相關的一些基本信息和資料,本身給本身提問題,再從問題出發去尋找答案,一步步地去理清本身的思路,從而造成了這一份文章。git
文章裏面的不少內容都是本身整理各位大神的文章/回答中的摘要而來(全部引用均會註明出處,在此對各位大神表示感謝和仰望),並且只靠一篇文章是確定不足以徹底講述清楚整個引擎所須要瞭解到的內容的,只是從我本身的角度來講,這篇文章之於以後構建渲染引擎之路來講,無異於撥雲見日的做用。github
另外這系列文章假定各位對OpenGL具備必定的基礎,所以不會過多地介紹OpenGL的一些基礎知識如渲染流程、着色器用法等。web
且不論遊戲引擎這樣的龐然巨物,就只說渲染引擎所須要涉及的內容就很是,你可能要考慮怎麼去封裝渲染API,還要考慮怎麼去整合各種效果系統,第一版效果出來了,要不要考慮搞搞Vulkan搞搞Metal,什麼樣的方案纔是最佳的等等。編程
所以若是沒有一個方向就開始尋找資料的話和大海撈針沒有什麼區別,也容易由於一時間接收的資訊過多進而打擊信心和興趣,所以在開始找資料以前我給本身定了幾個問題,以這個幾個問題爲方向去找資料爲本身解答:canvas
而在尋找答案的過程當中,天然而然就也會接觸到其餘方面的內容,好比一些本身以前見都沒見過的名詞,或者是大多數引擎都在用的成熟方案等等,這些也都會一併記錄下來。數組
其實這篇文章是有一個檢驗收穫的方式的,這個方式在一開始是沒有的,可是在找資料的過程當中看到知乎大佬的回答有所感悟,在這篇文章結束時再回過頭來看看下面這句話,若是大體明白了,那麼這篇文章就起到做用了:緩存
而這段話正是這一輪資料搜尋下來我的以爲最精煉的對渲染流程的描述了,至於裏面具體各個名詞所指則會在後面的內容進行補充,固然各個渲染引擎的流程在細節之處甚至一些環節上都會作出有所區別,這些則會在後面針對一些參考引擎作分析的時候再逐步整理出來。bash
那麼,啓程!數據結構
先貼一下找到的 知乎上的回答 :
是否是有種看一眼就想放棄的衝動 ?(╯°Д°)╯︵ ┻━┻
可是正如咱們在平常開發中忽然遇到一個新的技術需求的時候同樣,通常咱們也不會把一個技術學到精通來纔開始落地,而是會先預研一下方案,作下對比,掌握好基礎,對一些風險提早作好功課,保證好大方向上的正確性,而後就會開始逐步落地,在過程當中慢慢打磨。
類比到上面的內容也是如此,按照我本身的想法,我以爲首先C++的基本語法、內存管理、標準庫的使用等基礎內容以及OpenGL的渲染管線和基本的glsl、矩陣這些是在開始以前必需要先打好底子的,不然可能會連參考第三方開源引擎都成問題,更不要說本身去寫了,而在本身寫項目或學習第三方開源項目的過程當中則能夠對遇到的不明白或者不徹底理解的內容進行進一步的學習,不求快但求吃透每一點,以這樣的方式去持續擴大或加深本身的技能,在這個過程當中儘可能以點及面,每個細節儘量多去了解一些邊邊角角涉及到的地方,以便本身可以儘快完成一個又一個的閉環,最終對整個領域的理解愈來愈清晰。
其次就是合理安排出一部分時間來進行理論方面的系統學習,好比計算機圖形學等,系統性的學習我的仍是比較推薦經過書籍來進行,雖然週期更長,可是可讓你對這個領域的方方面面可以有一個更清晰的認識,從而造成一個更大的閉環。
而至於dx、metal、編譯知識這些若是暫時不須要作這方面的需求的話倒不是很是關鍵,能夠放到上面的技能學習以後,甚至可能很長一段時間你都不會使用到,固然也可能你如今是須要用dx而不須要opengl,那麼就是dx的基礎知識是必須的opengl排到後面,總之因人而異吧。
這部份內容是我在尋找渲染流程過程當中的額外收穫,雖說的是演化史,可是裏面的內容正好是對 啓程
章節裏對渲染引擎主要流程的描述的擴展,經過對多種着色方式的瞭解,能夠間接對渲染流程裏的幾個主要步驟有一個初步的感知。
該章節的內容起源是3D渲染引擎着色方式的演化史,原文對各個着色方式的介紹比較簡明扼要,在閱讀完這篇文章後我又本身去對裏面的各個名詞和許多不理解的地方進行了搜索,所以大綱上仍是會按照這篇文章的幾大部分進行,在內容裏面再補上本身找到的額外的資料,主要來源於 實時渲染中經常使用的幾種Rendering Path 和其餘針對其中細節講解的文章。
這部分我的認爲在這篇文章的階段不須要深究裏面的實現細節,這個能夠在後續分析開源引擎流程以及自主實現的時候去深刻研究,在這裏更多地是對這些Render path
有一個印象而且知道他們的大體渲染原理、彼此之間的區別以及能解決什麼樣的問題,從而能在後面的引擎中因地制宜地使用不一樣的着色方式。
Rendering Path
其實指的就是渲染場景中光照的方式。因爲場景中的光源可能不少,甚至是動態的光源。因此怎麼在速度和效果上達到一個最好的結果確實很困難。以當今的顯卡發展爲契機,人們才衍生出了這麼多的 Rendering Path 來處理各類光照。
在介紹各類光照渲染方式以前,首先必須介紹一下現代的圖形渲染管線。這是下面提到的幾種 Rendering Path 的技術基礎。
現代的渲染管線也稱爲可編程管線(Programmable Pipeline)
,簡單點 說就是將之前固定管線寫死的部分(好比頂點的處理,像素顏色的處理等等)變成在 GPU 上能夠進行用戶自定義編程的部分,好處就是用戶能夠自由發揮的空間增大,缺點就是必須用戶本身實現不少功能。
下面簡單介紹下可編程管線的流程。以 OpenGL 繪製一個三角形舉例。首先用戶指定三 個頂點傳給 Vertex Shader
。而後用戶能夠選擇是否進行Tessellation Shader
(曲面細分可能會用到)和 Geometry Shader
(能夠在 GPU 上增刪幾何信息)。緊接着進行光柵化
,再將光柵化後的結果傳給 Fragment Shader
進行 pixel
級別的處理。 最後將處理的像素傳給 FrameBuffer
並顯示到屏幕上。
名詞解釋
Geometry:即咱們所要渲染的一個幾何圖形
Vertex Shader:頂點着色器,處理每一個頂點,將頂點的空間位置投影在屏幕上,即計算頂點的二維座標。
Tessellation Shader:曲面細分着色器,是一個可選的着色器,用於細分圖元
Geometry Shader:幾何着色器,是一個可選的着色器,用於逐圖元的着色,能夠產生更多的圖元
Fragment Shader:片斷着色器,也稱爲像素着色器(Pixel Shader),用於計算「片斷」的顏色和其它屬性,此處的「片斷」一般是指單獨的像素
後面文章內容中出現的VS、TS、GS、FS(PS)即對應上圖中的Vertex Shader、Tessellation Shader、Geometry Shader、Fragment Shader
FrameBuffer:幀緩衝存儲器,簡稱幀緩存或顯存,它是屏幕所顯示畫面的一個直接映象,又稱爲位映射圖(Bit Map)或光柵。幀緩存的每一存儲單元對應屏幕上的一個像素,整個幀緩存對應一幀圖像。
這是最初始的渲染方式,原理是以mesh
爲單位進行渲染,在光柵化後,對每一個PS
進行計算時,根據光照
進行着色計算,因此這種方式稱爲前向着色
。
Forward Rendering 是絕大數引擎都含有的一種渲染方式。要使用 Forward Rendering,通常在 Vertex Shader
或 Fragment Shader
階段對每一個頂點或每一個像素進行光照計算,而且是對每一個光源進行計算產生最終結果。下面是 Forward Rendering 的核心僞代碼:
For each light:
For each object affected by the light:
framebuffer += object * light
複製代碼
好比在 Unity3D 4.x 引擎中,對於下圖中的圓圈(表示一個 Geometry
),進行 Forward Rendering 處理:
將獲得下面的處理結果:
也就是說,對於 ABCD 四個光源咱們在 Fragment Shader 中咱們對每一個 pixel 處理光照, 對於 DEFG 光源咱們在 Vertex Shader 中對每一個 vertex 處理光照,而對於 GH 光源,咱們採用球調和(SH)函數進行處理。
這種方式存在如下弊端:
所以,Deferred rendering就應運而生了。
很明顯,對於 Forward Rendering,光源數量對計算複雜度影響巨大,因此比較適合戶外這種光源較少的場景(通常只有太陽光)。
可是對於多光源,咱們使用 Forward Rendering 的效率會極其低下。由於若是在 Vertex Shader 中計算光照,其複雜度將是
O(num_geometry_vertexes ∗ num_lights)
,而若是在 Fragment Shader 中計算光照,其複雜度爲O(num_geometry_fragments ∗ num_lights)
。可見光源數目和複雜度是成線性增加的
。對此,咱們須要進行必要的優化。好比
多在 Vertex Shader 中進行光照處理,由於有一個幾何體有 10000 個頂點,那麼對於 n 個光源,至少要在 Vertex Shader 中計算 10000n 次。而對於在 Fragment Shader 中進行處理,這種消耗會更多,由於對於一個普通的 1024x768 屏幕,將近有 8 百萬的像素 要處理。因此若是頂點數小於像素個數的話,儘可能在 Vertex shader 中進行光照。
若是要在 fragment shader 中處理光照,咱們大可沒必要對每一個光源進行計算時,把全部像素都對該光源進行處理一次。由於每一個光源都有其本身的做用區域。好比點光源的做用區域是一個球體,而平行光的做用區域就是整個空間了。對於不在此光照做用區域的像素就不進行處理。可是這樣作的話,CPU 端的負擔將加劇。
對於某個幾何體,光源對其做用的程度是不一樣,因此有些做用程度特別小的光源能夠不進行考慮。典型的例子就是 Unity 中只考慮重要程度最大的 4 個光源。
名詞解釋
Mesh:網格,任何一個模型都是由若干網格面組成,而每個面又有若干個三角形組成,也就是說,模型是由若干個三角形面組成的
Deferred Rendering(延遲渲染)顧名思義,就是將光照處理這一步驟延遲一段時間再處理。
具體作法就是將光照放在已經將三維物體生成二維圖片以後進行處理。也就是說將物空間
的光照處理放到了像空間
進行處理。要作到這一步,須要一個重要的輔助工具——G-Buffer
。
G-Buffer 主要是用來存儲每一個像素對應的 Position,Normal,Diffuse Color 和其餘 Material parameters
。根據這些信息,咱們就能夠在像空間中對每一個像素進行光照處理。
下面是 Deferred Rendering 的核心僞代碼。
For each object:
Render to multiple targets
For each light:
Apply light as a 2D postprocess
複製代碼
這種渲染方式相比 Forward rendering 就是在渲染mesh
時,並不進行光照計算,而是按照如下步驟進行:
將深度、法線、Diffuse、Specular等材質屬性
分別輸出到GBuffer
裏(其實就是幾張RT
)
而後GBuffer裏的深度和法線信息,累加全部光照的強度到一張光照強度RT
上
根據GBuffer裏的Diffuse和Specular信息,以及光照強度RT,進行着色計算
名詞解釋
GBuffer:指Geometry Buffer,亦即「物體緩衝」。區別於普通的僅將顏色渲染到紋理中,G-Buffer指包含顏色、法線、世界空間座標的緩衝區,亦即指包含顏色、法線、世界空間座標的紋理。因爲G-Buffer須要的向量長度超出一般紋理能包含的向量的長度,一般在遊戲開發中,使用多渲染目標技術來生成G-Buffer,即在一次繪製中將顏色、法線、世界空間座標分別渲染到三張浮點紋理中
下面簡單舉個例子:
首先咱們用存儲各類信息的紋理圖。好比下面這張 Depth Buffer
,主要是用來肯定該像 素距離視點的遠近的。
根據反射光的密度/強度分度圖來計算反射效果。
下圖表示法向數據,這個很關鍵。進行光照計算最重要的一組數據。
下圖使用了 Diffuse Color Buffer。
這是使用 Deferred Rendering 最終的結果。
Deferred rendering 的最大的優點就是將光源的數目和場景中物體的數目在複雜度層面上徹底分開,也就是說場景中無論是一個三角形仍是一百萬個三角形,最後的複雜度不會隨光源數目變化而產生巨大變化。從上面的僞代碼能夠看出 Deferred rendering 的複雜度爲 O(screen_resolution + num_lights)
。
這種渲染方式也有一些弊端:
名詞解釋
MSAA、FXAA、Temporal AA都是抗鋸齒(Anti-Aliasing)技術,鋸齒的來源是由於場景的定義在三維空間中是連續的,而最終顯示的像素則是一個離散的二維數組。因此判斷一個點到底沒有被某個像素覆蓋的時候單純是一個「有」或者「沒有"問題,丟失了連續性的信息,致使鋸齒。
具體區別可見FXAA、FSAA與MSAA有什麼區別?
Deferred Rendering 侷限性是顯而易見的。好比我在 G-Buffer 存儲如下數據:
這樣的話,對於一個普通的 1024x768 的屏幕分辨率。總共得使用 1024x768x128bit=20MB, 對於目前的動則上 GB 的顯卡內存可能不算什麼,可是使用 G-Buffer 耗費的顯存仍是不少的。一方面,對於低端顯卡,這麼大的顯卡內存確實很耗費資源;另外一方面,若是要渲染更酷的特效,使用的 G-Buffer 大小將增長,而且其增長的幅度也是很可觀的;而且存取 G-Buffer 耗費的帶寬也是一個不可忽視的缺陷。
對於 Deferred Rendering 的優化也是一個頗有挑戰的問題。 下面簡單介紹幾種下降 Deferred Rendering 存取帶寬的方式。最簡單也是最容易想到的就是將存取的 G-Buffer 數據結構最小化,這也就衍生除了 Light Pre-Pass
方法。另外一種方式是將多個光照組成一組,而後一塊兒處理,這種方法衍生了 Tile-based deferred Rendering
。
這個技術是CryTek這個團隊(該團隊開發了CryENGINE遊戲引擎,即下面簡稱的CE,若是還不熟悉的話,那麼這個團隊開發了《孤島危機》、《孤島驚魂》等遊戲)原創的,由 Wolfgang Engel 在他的 博客 中提到的,也用於解決Deferred rendering
渲染方式裏的第一個弊端。原理跟Deferred rendering
差很少,只是有幾處不一樣:
GBuffer中只有深度(Z)和法線(Normal)數據,對比 Deferred Rendering,少了 Diffuse Color, Specular Color 以及對應位置的材質索引值
在 FS 階段利用上面的 G-Buffer 計算出所必須的 Light properties,好比 Normal * LightDir, LightColor, Specular 等 Light properties,將這些計算出的光照進行 alpha-blend
並存入 LightBuffer
(就是用來存儲 Light properties 的 buffer)
着色過程不是Deferred rendering中相似於後處理的方式,而是渲染mesh,即將結果送到 Forward rendering 渲染方式計算最後的光照效果
相對於傳統的 Deferred Render,使用 Light Pre-Pass 能夠對每一個不一樣的幾何體使用不一樣 的 Shader 進行渲染,因此每一個物體的 Material properties 將有更多變化。這裏咱們能夠看出對於傳統的 Deferred Rendering,它的第二步是遍歷每一個光源,這樣就增長了光源設置的靈活性,而 Light Pre-Pass 第三步使用的實際上是 Forward rendering,因此能夠對每一個 mesh 設置其材質,這二者是相輔相成的,有利有弊。
另外一個 Light Pre-Pass 的優勢是在使用 MSAA 上頗有利。雖然並非 100%使用上了 MSAA(除非使用 DX10/11 的特性),可是因爲使用了 Z 值和 Normal 值,就能夠很容易找到邊緣,並進行採樣。
下面這兩張圖,上邊是使用傳統 Deferred Render 繪製的,下邊是使用 Light Pre-Pass 繪 制的。這兩張圖在效果上不該該有太大區別。
其實這種方式也有弊端:
因爲不透明物體在主視口中被渲染了兩次,會大幅增長渲染批次,不過好在CE對狀態切換管理的很是好,因此渲染批次的承載力很高
因爲某些特殊材質須要對光照進行特殊處理,好比說樹葉的背光面也會有必定的光照,因此這種方式也不太完美
印象裏貌似CE對主光,例如太陽光,不累加進光照強度RT,而是着色時單獨處理,這樣的話效果會提高很多,至少室外場景是徹底可以解決問題的;而對於點光源比較多的室內場景,主光着色好看了就會效果很好了,畢竟其餘光照的影響佔比比較小。
這個方案是對Deferred rendering
渲染方式裏的第三個弊端進行優化的。原理就是:
這樣就能減小對光照強度RT上某個像素頻繁讀寫的次數。
TBDR 主要思想就是將屏幕分紅一個個小塊 tile
,而後根據這些 Depth 求得每一個 tile 的 bounding box
。對每一個 tile 的 bounding box 和 light 進行求交,這樣就獲得了對該 tile 有做用 的 light 的序列。最後根據獲得的序列計算所在 tile 的光照效果。
對比 Deferred Render,以前是對每一個光源求取其做用區域 light volume
,而後決定其做用的的 pixel,也就是說每一個光源要求取一次。而使用 TBDR,只要遍歷每一個 pixel,讓其所屬 tile 與光線求交,來計算做用其上的 light,並利用 G-Buffer 進行 Shading。一方面這樣作減小 了所需考慮的光源個數,另外一方面與傳統的 Deferred Rendering 相比,減小了存取的帶寬。
在 一篇文章 中提到目前全部的移動設備都使用的是 Tile-Based Deferred Rendering(TBDR) 的渲染架構,,裏面還說起了使用TBDR的一些注意事項,感興趣的能夠看看,以及 針對移動端TBDR架構GPU特性的渲染優化 ,移動GPU渲染原理的流派——IMR、TBR及TBDR
名詞解釋
tile:區塊,即將須要渲染的畫面分紅一個個的區塊
bounding box:邊界框,是一個矩形框,能夠由矩形左上角的xx和yy軸座標與右下角的xx和yy軸座標肯定。從技術上講,邊界框是包含一個物體的最小矩形
light volume:體積光,散射是一種很是美麗的天然現象,在天然界中光穿過潮溼或者含有雜質的介質時產生散射,散射的光線進入人眼,讓這些介質看起來像攏住了光線同樣,也就是所謂的體積光。可見 遊戲開發相關實時渲染技術之體積光
爲了解決Deferred lighting
裏面的第一個弊端,從CE3的某個版本開始,換成了這種方式。理由是,對於大多數物體來講,Deferred rendering
的方式就很好了,而對於特殊材質,則使用Deferred lighting
的方式。這樣,既能保持很好的渲染效果,又能避免渲染批次激增。
更詳細的內容可見 Hybrid-Deferred-Rendering.pdf
有時候,你轉了很大一個圈之後,發現又回到了原點。
好,那這就到了終極方式了——前向着色
的改進版。這個方案是ATI(著名顯卡生產商,06年被AMD收購)發明的,已經應用於Ogre 2.1(開源的面向對象的3D引擎)。UE4(大名鼎鼎的虛幻引擎)正在針對VR研發前向着色,不知道是否是也是這個。
原理也很簡單:
先用Tile-based deferred rendering
裏的方式計算好每一個區域受哪些光照影響
而後像傳統的前向着色同樣渲染每一個mesh——固然,要去光照列表裏查找影響當前區域的全部光照,並着色
這種方式只有上述提到的一個缺點,那就是可能和Deferred lighting
同樣須要渲染兩遍場景,不過之後應該會有優化的方案。優勢則有:
渲染效果好
帶寬開銷低,尤爲適用於VR這種每幀須要渲染兩遍場景的應用
可使用硬件支持的MSAA,質量最高。
Forward+的優點還有不少,其實大多就是傳統 Forward Rendering 自己的優點,因此 Forward+更像一個集各類 Rendering Path 優點於一體的 Rendering Path。
Forward+ = Forward + Light Culling
。Forward+ 很相似 Tiled-based Deferred Rendering。 其具體作法就是先對輸入的場景進行 z-prepass,也就是說關閉寫入 color,只向 z-buffer 寫入 z 值。注意此步驟是 Forward+必須的,而其餘渲染方式是可選的。接下來的步驟和 TBDR 很相似,都是劃分 tiles,並計算 bounding box。只不過 TBDR 是在 G-Buffer 中完成這一步驟 的,而 Forward+是根據 Z-Buffer。最後一步其實使用的是 Forward rendering 方式,即在 FS 階段對每一個 pixel 根據其所在 tile 的 light 序列計算光照效果。而 TBDR 使用的是基於 G-Buffer 的 Deferred rendering。實際上,forward+比 deferred 運行的更快。咱們能夠看出因爲 Forward+只要寫深度緩存 就能夠,而 Deferred Rendering 除了深度緩存,還要寫入法向緩存。而在
Light Culling
步驟, Forward+只須要計算出哪些 light 對該 tile 有影響便可。而 Deferred Rendering 還在這一部分把光照處理給作了。而這一部分,Forward+是放在 Shading 階段作的。因此 Shading 階段 Forward+ 耗費更多時間。可是對目前硬件來講,Shading 耗費的時間沒有那麼多。
如下是 Forward+ 與 Deferred Rendering 的對比圖:
感興趣的能夠再額外看看 forward框架的逆襲:解析forward渲染 這篇文章。
名詞解釋
Light Culling:剔除光照
渲染引擎屬於遊戲引擎中的一部分,本章節主要簡要整理一下找到的一些渲染引擎和遊戲引擎,具體內在區別後續進一步深刻了解的時候再整理補上。
在Wiki上也已經有整理了目前爲止市面上已有的大量遊戲引擎:Game Engine
Github上統計的開源遊戲引擎:game-engines
經過上面的調查咱們發現如今市面上的大小引擎數不勝數,一個個地去看的話時間週期估計要以年爲單位,首先咱們要先從自身的需求出發定出一些對參考引擎所須要具有的特性的要求,而後再根據要求來篩選出幾個比較貼合咱們需求的深刻研究。
以我自身的角度出發,我列出來瞭如下一些要求:
我從上面調查後的引擎列表裏整理出瞭如下幾個符合語言、使用人數、持續更新、支持效果等方面都比較符合的引擎來優先做爲研究的對象,後續的分析系列文章也會先以這些引擎來做爲目標:
渲染引擎
遊戲引擎
至此咱們完成了在邁出跨平臺渲染引擎第一步以前的鋪墊工做,咱們梳理了渲染引擎的一個大體流程,以及這個流程裏面的關於 Rendering path 等方面的細節信息,對這些內容有了一個初步的印象,同時列舉了如下使人望而卻步的技能樹,可是咱們能夠一步一步地吃成胖子,重要地是邁出這第一步,最後咱們整理了一下渲染/遊戲引擎列表,並按照自身要求從中梳理了幾個引擎來做爲下一步分析研究的目標。
接下來就是技術活了,下一篇《跨平臺渲染引擎之路:bgfx分析》將針對 bgfx 開始第一步學習研究,分享其內部的渲染流程以及分析思路等。