官术网_书友最值得收藏!

2.4 一些容易困惑的地方

在讀者學習Shader的過程中,會看到一些所謂的專業術語,這些術語的出現頻率很高,以至于如果沒有對其有基本的認識,會使得初學者總是感到非常困惑。本章的最后將闡述其中的一些術語。

2.4.1 什么是OpenGL/DirectX

只要讀者接觸過圖像編程,就一定聽說過OpenGL和DirectX,也一定知道這兩者之間的競爭關系。OpenGL與DirectX之間的競爭以及它們與各個硬件生產商之間的糾葛歷史很有趣,但很可惜這不在本書的討論范圍。本節的目的在于向讀者盡可能通俗地解釋,它們到底是什么,又和之前講到的渲染管線、GPU有什么關系。

我們花了一整個章節的篇幅來講述渲染的概念流水線以及GPU是如何實現這些流水線的,但如果要開發者直接訪問GPU是一件非常麻煩的事情,我們可能需要和各種寄存器、顯存打交道。而圖像編程接口在這些硬件的基礎上實現了一層抽象。

OpenGL和DirectX就是這些圖像應用編程接口,這些接口用于渲染二維或三維圖形。可以說,這些接口架起了上層應用程序和底層GPU的溝通橋梁。一個應用程序向這些接口發送渲染命令,而這些接口會依次向顯卡驅動(Graphics Driver)發送渲染命令,這些顯卡驅動是真正知道如何和GPU通信的角色,正是它們把OpenGL或者DirectX的函數調用翻譯成了GPU能夠聽懂的語言,同時它們也負責把紋理等數據轉換成GPU所支持的格式。一個比喻是,顯卡驅動就是顯卡的操作系統。圖2.18顯示了這樣的關系。

▲圖2.18 CPU、OpenGL/DirectX、顯卡驅動和GPU之間的關系

概括來說,我們的應用程序運行在CPU上。應用程序可以通過調用OpenGL或DirectX的圖形接口將渲染所需的數據,如頂點數據、紋理數據、材質參數等數據存儲在顯存中的特定區域。隨后,開發者可以通過圖像編程接口發出渲染命令,這些渲染命令也被稱為Draw Call,它們將會被顯卡驅動翻譯成GPU能夠理解的代碼,進行真正的繪制。

由圖2.18可以看出,一個顯卡除了有圖像處理單元GPU外,還擁有自己的內存,這個內存通常被稱為顯存Video Random Access Memory, VRAM)。GPU可以在顯存中存儲任何數據,但對于渲染來說一些數據類型是必需的,例如用于屏幕顯示的圖像緩沖、深度緩沖等。

因為顯卡驅動的存在,幾乎所有的GPU都既可以和OpenGL合作,也可以和DirectX一起工作。從顯卡的角度出發,實際上它只需要和顯卡驅動打交道就可以了。而顯卡驅動就好像一個中介者,負責和兩方(圖像編程接口和GPU)打交道。因此,一個顯卡制作商為了讓他們的顯卡可以同時和OpenGL、DirectX合作,就必須提供支持OpenGL和DirectX接口的顯卡驅動。

2.4.2 什么是HLSL、GLSL、CG

我們上面講到了很多可編程的著色器階段,如頂點著色器、片元著色器等。這些著色器的可編程性在于,我們可以使用一種特定的語言來編寫程序,就好比我們可以用C#來寫游戲邏輯一樣。

在可編程管線出現之前,為了編寫著色器代碼,開發者們學習匯編語言。為了給開發者們打開更方便的大門,就出現了更高級的著色語言(Shading Language)。著色語言是專門用于編寫著色器的,常見的著色語言有DirectX的HLSL(High Level Shading Language)、OpenGL的GLSL(OpenGL Shading Language)以及NVIDIA的CG(C for Graphic)。HLSL、GLSL、CG都是“高級(High-Level)”語言,但這種高級是相對于匯編語言來說的,而不是像C#相對于C的高級那樣。這些語言會被編譯成與機器無關的匯編語言,也被稱為中間語言(Intermediate Language, IL)。這些中間語言再交給顯卡驅動來翻譯成真正的機器語言,即GPU可以理解的語言。

對于一個初學者來說,一個最常見的問題就是,他應該選擇哪種語言?

GLSL的優點在于它的跨平臺性,它可以在Windows、Linux、Mac甚至移動平臺等多種平臺上工作,但這種跨平臺性是由于OpenGL沒有提供著色器編譯器,而是由顯卡驅動來完成著色器的編譯工作。也就是說,只要顯卡驅動支持對GLSL的編譯它就可以運行。這種做法的好處在于,由于供應商完全了解自己的硬件構造,他們知道怎樣做可以發揮出最大的作用。換句話說,GLSL是依賴硬件,而非操作系統層級的。但這也意味著GLSL的編譯結果將取決于硬件供應商。要知道,世界上有很多硬件供應商——NVIDIA、ATI等,他們對GLSL的編譯實現不盡相同,這可能會造成編譯結果不一致的情況,因為這完全取決于供應商的做法。

而對于HLSL,是由微軟控制著色器的編譯,就算使用了不同的硬件,同一個著色器的編譯結果也是一樣的(前提是版本相同)。但也因此支持HLSL的平臺相對比較有限,幾乎完全是微軟自已的產品,如Windows、Xbox 360、PS3等。這是因為在其他平臺上沒有可以編譯HLSL的編譯器。

CG則是真正意義上的跨平臺。它會根據平臺的不同,編譯成相應的中間語言。CG語言的跨平臺性很大原因取決于與微軟的合作,這也導致CG語言的語法和HLSL非常相像,CG語言可以無縫移植成HLSL代碼。但缺點是可能無法完全發揮出OpenGL的最新特性。

對于Unity平臺,我們同樣可以選擇使用哪種語言。在Unity Shader中,我們可以選擇使用“CG/HLSL”或者“GLSL”。帶引號是因為Unity里的這些著色語言并不是真正意義上的對應的著色語言,盡管它們的語法幾乎一樣。以Unity CG為例,你有時會發現有些CG語法在Unity Shader中是不支持的。關于Unity Shader和真正的CG/HLSL、GLSL之間的關系我們會在3.6節中講到。

2.4.3 什么是Draw Call

在前面的章節中,我們已經了解了Draw Call的含義。Draw Call本身的含義很簡單,就是CPU調用圖像編程接口,如OpenGL中的glDrawElements命令或者DirectX中的DrawIndexedPrimitive命令,以命令GPU進行渲染的操作。

一個常見的誤區是,Draw Call中造成性能問題的元兇是GPU,認為GPU上的狀態切換是耗時的,其實不是的,真正“拖后腿”其實的是CPU。

在深入理解Draw Call之前,我們先來看一下CPU和GPU之間的流水線化是怎么實現的,即它們是如何相互獨立一起工作的。

問題一:CPU和GPU是如何實現并行工作的?

如果沒有流水線化,那么CPU需要等到GPU完成上一個渲染任務才能再次發送渲染命令。但這種方法顯然會造成效率低下。因此,就像在本章一開頭講到的老王的洋娃娃工廠一樣,我們需要讓CPU和GPU可以并行工作。而解決方法就是使用一個命令緩沖區Command Buffer)。

命令緩沖區包含了一個命令隊列,由CPU向其中添加命令,而由GPU從中讀取命令,添加和讀取的過程是互相獨立的。命令緩沖區使得CPU和GPU可以相互獨立工作。當CPU需要渲染一些對象時,它可以向命令緩沖區中添加命令,而當GPU完成了上一次的渲染任務后,它就可以從命令隊列中再取出一個命令并執行它。

命令緩沖區中的命令有很多種類,而Draw Call是其中一種,其他命令還有改變渲染狀態等(例如改變使用的著色器,使用不同的紋理等)。圖2.19顯示了這樣一個例子。

▲圖2.19 命令緩沖區。CPU通過圖像編程接口向命令緩沖區中添加命令,而GPU從中讀取命令并執行。黃色方框內的命令就是Draw Call,而紅色方框內的命令用于改變渲染狀態。我們使用紅色方框來表示改變渲染狀態的命令,是因為這些命令往往更加耗時

問題二:為什么Draw Call多了會影響幀率?

我們先來做一個實驗:請創建10000個小文件,每個文件的大小為1KB,然后把它們從一個文件夾復制到另一個文件夾。你會發現,盡管這些文件的空間總和不超過10MB,但要花費很長時間。現在,我們再來創建一個單獨的文件,它的大小是10MB,然后也把它從一個文件夾復制到另一個文件夾。而這次復制的時間卻少很多!這是為什么呢?明明它們所包含的內容大小是一樣的。原因在于,每一個復制動作需要很多額外的操作,例如分配內存、創建各種元數據等。如你所見,這些操作將造成很多額外的性能開銷,如果我們復制了很多小文件,那么這個開銷將會很大。

渲染的過程雖然和上面的實驗有很大不同,但從感性角度上是很類似的。在每次調用Draw Call之前,CPU需要向GPU發送很多內容,包括數據、狀態和命令等。在這一階段,CPU需要完成很多工作,例如檢查渲染狀態等。而一旦CPU完成了這些準備工作,GPU就可以開始本次的渲染。GPU的渲染能力是很強的,渲染200個還是2000個三角網格通常沒有什么區別,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call的數量太多,CPU就會把大量時間花費在提交Draw Call上,造成CPU的過載。圖2.20顯示了這樣一個例子。

▲圖2.20 命令緩沖區中的虛線方框表示GPU已經完成的命令。此時,命令緩沖區中沒有可以執行的命令了,GPU處于空閑狀態,而CPU還沒有準備好下一個渲染命令

問題三:如何減少Draw Call?

盡管減少Draw Call的方法有很多,但我們這里僅討論使用批處理Batching)的方法。

我們講過,提交大量很小的Draw Call會造成CPU的性能瓶頸,即CPU把時間都花費在準備Draw Call的工作上了。那么,一個很顯然的優化想法就是把很多小的DrawCall合并成一個大的Draw Call,這就是批處理的思想。圖2.21顯示了批處理所做的工作。

▲圖2.21 利用批處理,CPU在RAM把多個網格合并成一個更大的網格,再發送給GPU,然后在一個Draw Call中渲染它們。但要注意的是,使用批處理合并的網格將會使用同一種渲染狀態。也就是說,如果網格之間需要使用不同的渲染狀態,那么就無法使用批處理技術

需要注意的是,由于我們需要在CPU的內存中合并網格,而合并的過程是需要消耗時間的。因此,批處理技術更加適合于那些靜態的物體,例如不會移動的大地、石頭等,對于這些靜態物體我們只需要合并一次即可。當然,我們也可以對動態物體進行批處理。但是,由于這些物體是不斷運動的,因此每一幀都需要重新進行合并然后再發送給GPU,這對空間和時間都會造成一定的影響。

在游戲開發過程中,為了減少Draw Call的開銷,有兩點需要注意。

(1)避免使用大量很小的網格。當不可避免地需要使用很小的網格結構時,考慮是否可以合并它們。

(2)避免使用過多的材質。盡量在不同的網格之間共用同一個材質。

在本書的16.4節,我們會繼續闡述如何在Unity中利用批處理技術來進行優化。

2.4.4 什么是固定管線渲染

固定函數的流水線Fixed-Function Pipeline),也簡稱為固定管線,通常是指在較舊的GPU上實現的渲染流水線。這種流水線只給開發者提供一些配置操作,但開發者沒有對流水線階段的完全控制權。

固定管線通常提供了一系列接口,這些接口包含了一個函數入口點(Function Entry Points)集合,這些函數入口點會匹配GPU上的一個特定的邏輯功能。開發者們通過這些接口來控制渲染流水線。換句話說,固定渲染管線是只可配置的管線。一個形象的比喻是,我們在使用固定管線進行渲染時,就好像在控制電路上的多個開關,我們可以選擇打開或者關閉一個開關,但永遠無法控制整個電路的排布。

隨著時代的發展,GPU流水線越來越朝著更高的靈活性和可控性方向發展,可編程渲染管線應運而生。我們在上面看到了許多可編程的流水線階段,如頂點著色器、片元著色器,這些可編程的著色器階段可以說是GPU進化最重要的貢獻。表2.1給出了3種最常見的圖像接口從固定管線向可編程管線進化的版本。

表2.1 3種圖像接口從固定管線向可編程管線進化的版本

在GPU發展的過程中,為了繼續提供固定管線的接口抽象,一些顯卡驅動的開發者們使用了更加通用的著色架構,即使用可編程的管線來模擬固定管線。這是為了在提供可編程渲染管線的同時,可以讓那些已經熟悉了固定管線的開發者們繼續使用固定管線進行渲染。例如,OpenGL 2.0在沒有真正的固定管線的硬件支持下,依靠系統的可編程管線功能來模仿固定管線的處理過程。但隨著GPU的發展,固定管線已經逐漸退出歷史舞臺。例如,OpenGL 3.0是最后既支持可編程管線又完全支持固定管線編程接口的版本,在OpenGL 3.2中,Core Profile就完全移除了固定管線的概念。

因此,如果讀者不是為了對較舊的設備進行兼容,不建議繼續使用固定管線的渲染方式。

主站蜘蛛池模板: 临邑县| 东安县| 巢湖市| 平邑县| 英吉沙县| 甘谷县| 锡林郭勒盟| 邵武市| 望奎县| 德州市| 英超| 砚山县| 东至县| 武冈市| 会宁县| 班玛县| 崇礼县| 柳林县| 德阳市| 南江县| 镇原县| 三穗县| 鹤山市| 绥化市| 通州区| 霍城县| 平安县| 安阳县| 太原市| 余姚市| 乌鲁木齐市| 罗平县| 黄大仙区| 沂水县| 沭阳县| 比如县| 黄龙县| 平湖市| 永寿县| 鹤峰县| 明星|