當前位置:
首頁 > 最新 > Learning OpenGL——OpenGL Model,Pipeline and Practices

Learning OpenGL——OpenGL Model,Pipeline and Practices

Introduction

這半年的時間都在做音視頻合成的相關工作,項目研發過程中也遇到很多問題,但是網上優秀的技術資源也比較少,因此,想系統地學習和整理 OpenGL 圖形圖像處理的相關內容。

本篇文章基本包括以下內容:

OpenGL 的 C/S 模型架構。OpenGL 使用 Client - Server 模型,客戶端(Client)運行在 CPU 上,服務端(Server)運行在 GPU 上,這部分會具體討論 CPU 傳輸數據到 GPU 而造成的性能損耗,以及討論離屏渲染會造成巨大性能開銷的原因。

OpenGL 的 Pipeline 及重要的 Shader 簡介。這一部分主要討論 OpenGL 是以 C/S 為模型設計的架構,在這套架構之上,當數據由 CPU 傳遞給 GPU 之後,GPU 怎樣對 CPU 提供的數據進行加工、處理最終形成 FrameBuffer(幀緩衝),交給屏幕驅動進行屏幕渲染的過程。

OpenGL(ES)編程實踐。用具體的程序代碼來詳細學習和理解什麼是 Shader(著色器),什麼是 VBO(Vertex Buffer Object),什麼是 EBO(Element Buffer Object),什麼是 VAO(Vertex Array Object)。

以上三個部分是本文主要討論的內容,前兩部分用較多的筆墨著重介紹 OpenGL 理論基礎。鄙人認為只有先理解了 OpenGL 的設計理念,才能更好的使用 OpenGL 提供的函數進行圖形處理。閱讀本文可以先帶著這樣幾個問題來進行 OpenGL 的探究:

OpenGL 是按照什麼架構設計的?

什麼是渲染上下文(Context)?

什麼是離屏渲染?

為什麼離屏渲染會造成性能損耗?

什麼是 OpenGL 管線(Pipeline)?

OpenGL 管線主要包含哪些部分?

為什麼說 OpenGL 管線中的著色器(Shader)是可編程管線?

有哪些著色器可以由程序員進行編程?

什麼是 VBO、EBO 和 VAO?

Vertex Buffer Object 的布局格式是怎樣的?

Vertex Array Object 的布局格式是怎樣的?

如何用 OpenGL 畫一個三角形?

如何在三角形的基礎上修改為矩形?

當然以上所有問題會在本文內容中加以詳細解釋,如果對以上問題感興趣,就和我一起開始學習 OpenGL 吧。

註:我使用的是 macOS 系統,所以本文中 OpenGL 開發框架和代碼示例都是基於 Cocoa(Cocoa Touch)框架來編寫的。但 OpenGL 圖形處理的理論是通用的,在學習過程中我也通過學習其他開發平台(Windows、Android、Linux 等)的資料來輔助自己對 OpenGL 原理的理解。


圖 1 屏幕顯像過程圖

在 ibireme 大佬的《iOS 保持界面流暢的技巧》一文中,他是這樣解釋屏幕顯像的過程的:

計算機系統中 CPU、GPU、顯示器是以上面這種方式協同工作的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成後將渲染結果放入幀緩衝區,隨後視頻控制器會按照 VSync 信號逐行讀取幀緩衝區的數據,經過可能的數模轉換傳遞給顯示器顯示。

在圖 1 中我們可以看到數據從 CPU 到 GPU,經過 GPU 的處理形成 FrameBuffer,並交由視頻控制器,最終控制顯示器顯像的過程。而在這整個過程中,CPU 與 GPU 的通訊,GPU 將 CPU 傳遞來的數據處理成 FrameBuffer 這個階段,實際上就是 OpenGL 的任務,即OpenGL 的工作就是將 CPU 傳遞來的渲染數據,作為輸入數據,經過 Pipeline 中的不同 Shader (可編程著色器)處理加工成 FrameBuffer 輸出給底層硬體。所以,簡單講 OpenGL 圖形處理過程就是,丟給 OpenGL 輸入數據,OpenGL 吐出 FrameBuffer 輸出數據的過程。

圖 2 屏幕顯像過程軟體組件圖

上圖是 《Getting Pixels onto the Screen》 文章中提供的顯像過程圖。應用程序利用 Core Graphics、Core Animation、Core Image 等框架設定渲染圖形所需要的數據,並交由 OpenGL(ES) ,然後,OpenGL 與 GPU 驅動進行通訊,最終 GPU 控制顯示器進行渲染顯示。

了解基本的屏幕顯像原理之後,接下來就具體學習 OpenGL 是什麼,OpenGL 的架構是如何設計,以及 OpenGL 是如何進行圖形處理的。


OpenGL 是一個開放的、跨平台的擁有廣泛工業支持的圖形處理標準。OpenGL 通過提供一個成熟、文檔完善的,支持現在和未來硬體加速的抽象概念的圖形處理管道,令寫實時 2D 或者 3D 的圖形應用非常容易。


圖 3 OpenGL CPU and GPU

macOS 中的 OpenGL 使用通用的 OpenGL 框架和插件驅動實現了 OpenGL Client-Server 模型。如圖 3 所示,框架和驅動聯合一起,用來實現客戶端 OpenGL 埠。專用的圖形處理硬體支持了服務端。儘管這是通用場景,Apple 也在 CPU 上提供了全部的軟體渲染實現。

圖 4 OpenGL client-server model

如圖 4 所示,最上層是應用層,中間是 OpenGL Client,下層是 OpenGL Server。當我們的應用調用 OpenGL API 的時候,它會先跟 OpenGL Client 進行通訊,然後,OpenGL Client 向 OpenGL Server 傳遞繪畫指令。OpenGL Client-Server 模型設計的好處是,Client 能夠在命令完成執行之前將控制權返回給應用程序,即 OpenGL 的命令能夠非同步執行。

圖 5 Macintosh OpenGL Hardware Architecture

Client-Server 模型允許圖形工作在客戶端和在服務端之間被分開。例如,所有 Macintosh 計算機搭載了專屬於圖形處理的硬體,使運行圖形處理的並行計算最優化。圖 5 展示了一組 CPU 和 GPU 的通用布局。通過這樣的硬體配置,OpenGL 客戶端在 CPU 上執行,而 OpenGL 服務端在 GPU 上執行


OpenGL 提供了一套豐富的跨平台繪圖命令,但並不定義它和操作系統的圖形子系統交互的函數,而是 OpenGL 要求每個平台實現去定義一個介面用來創建渲染上下文(Context),並且將它們和圖形子系統相關聯。渲染上下文保存所有在OpenGL 狀態機(OpenGL State Machine)中存儲的數據。程序可以維護多個上下文,允許一個上下文被應用改變機器中的狀態,而不影響其他上下文。

那什麼是渲染上下文呢?渲染上下文(Context),或者說簡單上下文,包含 OpenGL 狀態信息和應用的對象。狀態變數包括諸如繪圖顏色、視圖和投影轉換、照明特性以及材質屬性等內容。狀態變數在每個上下文被設置。當你的應用創建 OpenGL 對象(如,紋理),這些都與渲染上下文相關。儘管應用程序可以維護多個上下文,但只有一個上下文能夠在一個線程中成為當前上下文。當前上下文是接收應用發出 OpenGL 命令的渲染上下文

當我們的應用程序調用 OpenGL 的函數時,OpenGL Client 在將控制權返回給應用程序之前,會拷貝任何一個在參數中提供的數據。因此,應用是自由改變他自己擁有的內存的,無需顧忌他對 OpenGL 的調用。 Client(CPU) 拷貝的數據,在傳遞給 Server(GPU) 之前經常會被重新格式化,故而向 Server(GPU) 拷貝、修改以及傳遞參數增加了調用 OpenGL 的開支。


來看 CPU 和英偉達顯卡之間進行數據通信的一張圖片,以加深以上這段文字的理解。

圖 6 現代計算機的硬體結構

上圖中,CPU 中二級緩存(L2 Cache)和 2GB 主存(Main Memory)的數據交換速度為 12.8GB/sec,NVIDIA GPU 和顯存(Video Memory)數據交換速度是 84 GB/sec,而主存和顯存通過 PCI 匯流排之間的數據交換速度為 4 GB/sec。顯然,顯存的帶寬是內存的 5 倍以上,而 PCI 匯流排的帶寬大約是內存速度的三分之一。

在上文的學習中,我們已經知道,渲染上下文(Context)包含 OpenGL 狀態信息和用於應用對象,在調用任何 OpenGL 的介面前我們必須要先創建渲染上下文(Context),並且一個線程中只有一個上下文能夠成為當前上下文。當 OpenGL Client(CPU) 拷貝數據時,由於顯存和主存以及匯流排帶寬之間速度的不匹配,如果頻繁地由 CPU 向 GPU 傳遞數據,那麼必然會增加調用 OpenGL 的開支。

在當前上下文不是屏幕所顯示的上下文時,比如屏幕顯示了一個列表視圖,但是程序員需要調用 OpenGL 命令去繪製一張圖片,創建新的渲染上下文,並把 OpenGL 的當前上下文切換到這張圖片的上下文,此時就發生了離屏渲染。為了調用 OpenGL 命令來進行新圖片的渲染運算,程序必須創建新的渲染上下文,所以,離屏渲染時,CPU 會對新創建的渲染上下文存儲的數據進行拷貝,這些渲染數據再經由主存、匯流排、顯存,最終傳遞到 GPU 進行大量並行計算,輸出 FrameBuffer。而在這個數據傳遞和處理過程中,由於如圖 6 所顯示的 CPU 和主存、主存和顯存,以及顯存和 GPU 帶寬速度不一致,導致 CPU 數據拷貝和傳遞的過程中,增加了調用 OpenGL 的開支。

以上便是什麼是離屏渲染,以及在離屏渲染時會造成性能損耗的原因。另外,我節選了 Apple 文檔中對離屏渲染的相關描述來加深理解。

OpenGL applications may want to use OpenGL to render images without actually displaying them to the user. For example, an image processing application might render the image, then copy that image back to the application and save it to disk. Another useful strategy is to create intermediate images that are used later to render additional content. For example, your application might want to render an image and use it as a texture in a future rendering pass. For best performance, offscreen targets should be managed by OpenGL. Having OpenGL manage offscreen targets allows you to avoid copying pixel data back to your application, except when this is absolutely necessary.

上文是 Apple Document 對離屏渲染的介紹,譯文如下。

OpenGL 應用程序可能想要使用 OpenGL 來渲染圖像,而無需將其實際顯示給用戶(顯示到屏幕上)。 例如,圖像處理應用程序可能會渲染圖像,然後將該圖像複製回應用程序並將其保存到磁碟。 另一個有用的策略是創建稍後用於呈現額外內容的中間圖像。 例如,您的應用程序可能想要渲染圖像,並將其用作未來渲染過程中的紋理。 為了獲得最佳性能,屏幕外目標應由 OpenGL 管理。 使用OpenGL管理屏幕外目標可以避免將像素數據複製回應用程序,除非這是絕對必要的。


在本文的第一部分 OpenGL 模型內容中,我們主要學習了 OpenGL 基於 Client-Server 模型的架構,以及這樣的架構會對渲染有怎樣的影響。接下來,我們開始學習 OpenGL 在這個架構上具體如何處理輸入的渲染數據並輸出 FrameBuffer 的,即 OpenGL 的管線(Pipeline)。

圖 7 OpenGL Graphics Pipeline

文章開頭,我用比較淺白的語言描述 Pipeline 是什麼,「OpenGL 圖形處理過程就是,丟給 OpenGL 輸入數據,OpenGL 吐出 FrameBuffer 輸出數據的過程」。如果用規範的語言來描述應該是,「OpenGL 的圖形渲染管線(Graphics Pipeline)指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程。」

而我們程序員所要參與的過程,實際就是 Pipeline 這中間的幾個可編程著色器(Shader)的階段(Stage)。圖 7 為我們簡略展示了 Pipeline 的主要幾個階段。輸入數據 Vertex Data 是一個一維數組,分別經過頂點著色器(Vertex Shader)、圖元裝配(Shape Assembly)、幾何著色器(Geometry Shader)、光柵化(Rasterization)、片段著色器(Fragment Shader)、測試與混合(Tests and Blending)等階段,最終輸出幀緩衝(FrameBuffer)。

而在圖 7 中藍色底色的頂點著色器(Vertex Shader)、幾何著色器(Geometry Shader)以及片段著色器(Fragment Shader)可以注入自己的編寫著色器程序來達到程序所需的渲染效果。現代 OpenGL 要求我們,必須定義至少一個頂點著色器和一個片段著色器(因為GPU中沒有默認的頂點/片段著色器)。所以,我們能夠自己編寫 Shader 對 Shader 源碼進行編譯等處理,然後對 Vertex Buffer Object、Element Buffer Object 以及 Vertex Array Object 進行綁定,最終輸出 FrameBuffer 將圖形繪製到屏幕上。這種用於編寫 Shader 程序的類 C 的編程語言,稱之為 GLSL(OpenGL Shading Language)。而這也是為什麼稱 Shader 為可編程管線的原因,在本文的第三部分 Practice 中,我們會編寫兩個比較簡單的 Shader 程序,來繪製三角形和矩形,並且學習如何使用 OpenGL 進行圖形處理的編程。

圖 8 Shader Inputs and Outputs

接下來,先分別看一下圖中幾個重要的 Shader 具體都做了些什麼。圖 8 中顯示的是 OpenGL 4.5 版本的內置的輸入和輸出,從這個圖表中,我們就可以比較清楚地了解具體著色器所需的輸入數據,以及經過著色器處理後的輸出數據。內置輸入輸出數據更加清楚地啟示給我們,我們編寫 Shader 源碼需要向 OpenGL 輸入什麼數據,以及 OpenGL 最終輸出怎樣的數據。


Vertex Shaders

圖形渲染管線的第一個階段是頂點著色器(Vertex Shader),頂點著色器進行有關頂點值和頂點數據的操作,主要是頂點齊次坐標變換和光照。

為了進行頂點處理,在編程過程中,我們需要先向 Vertex Shader 輸入一組頂點數組,讓 Vertex Shader 可以將頂點數據(Vertex Data)設置成頂點屬性(Vertex Attribute)。

Vertex Input

上段代碼就聲明(declare)了一個浮點型的頂點數組,這個數組將 9 個浮點數元素分三行編碼,按照這樣的格式編碼的原因是,OpenGL 是在三維空間中進行渲染工作的,所以每三個浮點數就表示一個點在三維空間中的坐標(x, y, z)。由於我們渲染的圖形是顯示在平面的屏幕上,所以,z 軸的坐標值始終為 0。

以上段代碼為例,第一行的頂點坐標為(0.5, 0.5, 0),第二行的頂點坐標為(0.5, -0.5, 0),第三行的頂點坐標為(-0.5, -0.5, 0)。當我們完成綁定 Vertex Buffer Object、Vertex Array Object 等操作,調用繪製圖形命令後就會出現如下所顯示的三角形。

圖 9 Triangle

注意,在 OpenGL 的坐標系(標準化設備坐標,Normalized Device Coordinates, NDC)中,坐標的原點位於視口(View Port)的中心,這與 Cocoa(Cocoa Touch)框架中的窗體(Window)的坐標系原點不同。NDC 坐標系如下圖所示。

圖 10 Normalized Device Coordinates

在編程學習的部分中,我們會利用 GLSL 編寫一個簡單的 Vertex Shader 程序。Vertex Shader 頂點著色器允許我們指定任何以頂點屬性為形式的輸入。頂點屬性所鏈接的頂點數據,是一組 4 位元組 float 型的數組。

上段代碼就是一個 Vertex Shader 程序的代碼。

代碼的第一行指定了 GLSL 的版本為 3.3,這就意味著程序在運行時,Shader 的編譯器必須能夠支持 3.3 的 GLSL 的版本。(直接使用 Cocoa 框架提供的 NSOpenGLView 來編譯這段代碼會報錯,原因稍後具體解釋。)

代碼第二行聲明了一個指定為頂點屬性的 float 類型三維向量 aPos,這個向量表示了這個輸入頂點在 Shader 中的位置(Position)。這一步就將頂點數組 vertices 第一行所代表的頂點坐標 (0.5, 0.5, 0) 設置成了頂點屬性。「頂點屬性」意味著 GPU 中的 Shader 程序每調用一次,頂點緩衝區(Vertex Buffer)都會為其提供一個新的頂點數據,頂點屬性的數據結構如下圖所示。

圖 11 Vertex Attribute

在上圖中,0 ~ 12 BYTE 這一整個格子就表示了一個頂點屬性,這個頂點屬性表示頂點的坐標,包括了頂點的 x、 y、 z 的坐標值。頂點屬性有時還可能包括其他屬性,如顏色(RGB)、法向量(Normal)、 紋理坐標(Texture Coordinates)等屬性。如圖 12,展示了包含顏色屬性的頂點屬性內存布局。

圖 12 Vertex Attribute with Color

添加額外的頂點屬性,會增加頂點屬性所佔用的內存空間,但本篇文章並不對如何添加除坐標以外屬性的方法做更多討論。

進入 GLSL 主函數之後,在代碼第五行,利用gl_Position函數,將以頂點的坐標屬性創建的四維向量 vec4 賦值給 Shader 的 Position。可以觀察到,除了 aPos 向量的 x、y、z 三個坐標值,還有一個值 1.0,這個值代表的是深度(Depth),是用在所謂透視劃分(Perspective Division)上,本文不做更多研究。

這樣,一段簡單的 Vertex Shader 代碼就編寫完成了。

Vertex Attribute Values

在之前的學習中,我么已經了解到頂點處理過程,頂點著色器會通過輸入頂點數據的數組獲取 4 維向量的頂點屬性。而頂點屬性可以包括坐標、顏色、法向量(Normal)、 紋理坐標(Texture Coordinates)等屬性。具體詳細介紹可以回顧上面的內容。

註:本節公示可以在 《OpenGL 4.6 Core Profile》 454 頁找到。

圖 13 Vertex Processing and Primitive Assembly

頂點處理完成後就進行圖元裝配(Primitive Assembly),圖元裝配場景包括圖元裝配、裁剪、透視除法、視口變換等。頂點處理或者頂點著色器的輸出一個四維向量,輸出的向量如下所示。

?????

x

c

y

c

z

c

w

c

?????

(xcyczcwc)

圖元裝配之後是裁剪(Clipping),裁剪公式如下。

?

w

c

x

c

w

c

?wc≤xc≤wc

?

w

c

y

c

w

c

?wc≤yc≤wc

?

w

c

z

c

w

c

?wc≤zc≤wc

裁剪之後是透視除法(Perspective Division),公示如下。其中,

???

x

d

y

d

z

d

???

(xdydzd)

是設備(Device)顯示的坐標向量。

???

x

d

y

d

z

d

???

=

?????

x

c

w

c

y

c

w

c

z

c

w

c

?????

(xdydzd)=(xcwcf×ycwczcwc)

最後進行的是視口變換(Viewport Transformation)。視口轉換由所選視口的寬度和高度(以像素為單位)確定,後面的編程學習中,我們將會利用glViewport對視口的 x、y 坐標,以及視口的寬度和高度進行設定。視口變換的公示如下。

???

x

w

y

w

z

w

???

=

????

p

x

2

x

d

+

o

x

p

y

2

y

d

+

o

y

z

d

+b

????

(xwywzw)=(px2xd+oxpy2yd+oyf×zd+b)

其中,

p

x

px

是程序員輸入的寬度值,

p

y

py

是程序員輸入的高度值。s 和 b 是裁剪控制深度模式的係數。


圖元裝配之後,裁剪、透視除法、視口變換等操作之前,也可以由幾何著色器對圖元進行操作,即在圖元裝配之後插入幾何著色器。

幾何著色器是一個額外的流水線階段,定義了進一步處理這些基元的操作。 幾何著色器一次對單個圖元進行操作,並發出一個或多個輸出圖元,它們都是相同的類型,然後像應用程序指定的等效 OpenGL 圖元一樣進行處理。幾何著色器通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀,在圖 7 的圖形管線中,它生成了另一個三角形。


圖 14 Vertex Processing and Primitive Assembly

圖元裝配後輸出的圖元將交由光柵化處理。光柵化是將圖元轉換為二維圖像的過程,此圖像的每個點都包含顏色和深度等信息。

光柵化有兩個任務:

確定圖元包含哪些由整數坐標確定的「小方塊」(和屏幕像素對應,現在還不能叫片斷,光柵化完成後才能叫片斷)。

確定這些小方塊的 Depth 值和 Color 值(從圖片頂點的 Depth 和 Color 插值得到),這些顏色後來可能被其他如紋理操作修改。

可以這樣簡單地理解光柵化過程,由於數學世界中線的函數是連續的,然而現實世界的物理設備屏幕是離散的。所以,光柵化的任務其實就是數學中連續的點轉化成顯示屏幕上不連續的小方塊。


圖 15 Fragment Shader

光柵化的輸出是一些列片斷(Fragments,這些片斷可能經過片斷著色器處理),片斷被稱為「准像素」,要能想像出屏幕坐標系的一個整數坐標上只有一個像素,但可以前後「堆疊」多個片斷。這些片斷進入逐片斷處理(Per-Fragment Operations),首先進行各種測試。每步測試,不通過的片斷將被丟棄從而不能進入後續操作,然後進行一些操作(如混合),最終通過所有處理的片斷將被寫入 FrameBuffer 用於最終屏幕顯示。

上文中我們已經了解到, Fragment Shader 這個階段中,我們也要編寫一個 Shader 程序來實現著色器的具體操作,本文的 Fragment Shader 示例代碼如下。

代碼第一行也是指定 GLSL 的版本為 3.3。

代碼第二行聲明輸出四維向量 FragColor。

進入 Fragment Shader 主函數之後,設置 Fragment 的色值 RGB 為 (1, 0.5, 0.2),並且 alpha 值為 1,即不透明。

這是一段比較簡單的 Fragment Shader 示例代碼。至此,OpenGL Graphics Pipeline 中比較重要的幾個階段就介紹完了,關於 OpenGL 的理論學習部分就暫時告一段落,接下來開始本文最後一部分編程實踐。


在本文前兩部分中,我們已經學習了 OpenGL 的 Client-Server 模型架構及其影響。此外,還較為詳細地學習了 OpenGL Graphics Pipeline 及幾個重要的 Pipeline Shader。其中,Vertex Shader 和 Fragment Shader 需要我們編程實現,編寫 Shader 程序的語言是 GLSL。

接下來,開始學習最簡單的 OpenGL 繪圖編程——如何利用 OpenGL API 繪製三角形和正方形(矩形)。

為了避免佔用文章篇幅,我將項目放置在 GitHub 的倉庫中。另外,完整源碼可直接在 OpenGL Programming: Draw Rectangle 中閱讀學習。這裡我將按照完整代碼中的實現文件(*.m 文件)進行逐代碼塊詳解。


創建 MyOpenGLView 類並繼承自 NSOpenGLView,覆寫(override) MyOpenGLView 的初始化方法,實現initGL方法,方法函數體具體如下。並在 MyOpenGLView 初始化方法中調用(Call)initGL方法。

在使用 Cocoa 框架時,進行 OpenGL 的編程前必須先指定 OpenGL Profile,否則,就會出現如下報錯。

由於網上很多 OpenGL 開發資料是在 macOS 上使用跨平台的 GLFW 框架實現的,但我按照 stackoverflow, 知乎提供的方法修改了很久都沒有解決這個問題。這是因為 Cocoa 框架的窗體系統(Window System)是 Apple 自己封裝的系統,而非 GLFW 框架內部實現的窗體系統,所以,要解決報錯需要先給 NSOpenGLView 的 pixelFormat 賦值,類型為 NSOpenGLPixelFormatAttribute。而在賦值過程中,Cocoa 框架就為我們創建了新的渲染上下文(Context),這也是調用任何 OpenGL API 的前提。


初始化 MyOpenGLView 之後,覆寫- (void)drawRect:(NSRect)dirtyRect方法,實現 OpenGL 的調用。

Current Context

首先要切換 OpenGL 當前上下文到 MyOpenGLView 的上下文。

Vertex Shader, Fragment Shader, Shader Program

在 Graphics Pipeline 部分中,我們已經學習了如何利用 GLSL 語言編寫 Vertex Shader 和 Fragment Shader 程序。這裡,我們只把 Shader 用常量字元串來編寫,而非創建單獨文件(Shader 程序文件本質也是嚮應用程序提供常量字元串)。

有了 Shader 的源碼字元串之後,我們就要先創建 Shader,載入 Shader 源碼,編譯 GLSL 程序。

當編譯完 Shader 後,可以檢查編譯是否有報錯,這裡不做贅述。當 Shader 的源碼(Source)編譯成功後,就要創建 Shader Program,並把 Shader 附加(Attach)在 Shader Program 上,最後鏈接 Shader Program。

Shader Program 鏈接成功之後,就需要將一開始創建的 Vertex Shader 和 Fragment Shader 刪除以釋放不用的內存空間。

通過觀察以上代碼,我們可以總結出有關 Shader 程序處理流程如下:

創建 Shader,glCreateShader

載入 Shader 源碼,glShaderSource

編譯 Shader,glCompileShader

創建 Shader Program,glCreateProgram

附加 Shader 到 Shader Program 上,glAttachShader

鏈接 Shader Program,glLinkProgram

刪除 Shader,glDeleteShader

完成 Shader 相關的編碼之後,就要開始進行 Vertex Data 相關處理的編程了。

VBO, EBO, VAOVBO

在第二部分 Graphics Pipeline 中我們已經學習過,Pipeline 的 Vertex Shader 根據頂點數據的數組,設置頂點屬性,然後後續的 Shader 繼續對頂點屬性進行處理最終輸出 FrameBuffer。而對於 OpenGL 而言,承載頂點屬性的載體是 Vertex Buffer Object(VBO),VBO 是頂點屬性的內存緩衝區,用來存儲頂點屬性的相關數據。

在前文的圖 11 Vertex Attribute 中,我們可以看到頂點屬性在緩衝區中的內存布局。上文已經提到,頂點屬性的數據為 4Byte 的 float 型,在本文的示例代碼中,只存在 Position 屬性,x、y、z 的位元組數總共 12Byte,即 32bit。這裡,12Byte 又稱之為頂點屬性的步長(Stride)。頂點之間沒有空隙,在數組中緊密排列。

由於本文示例中,只存在 Position 屬性,所以 offset 為 0。如圖 12 當存在 Color 屬性時,Color 屬性的 offset 就為 12Byte。

對於圖 11 的頂點屬性緩衝圖,我們可以這樣形象的理解,灰色長條(VERTEX 1、VERTEX 2,VERTEX 3)存儲的是頂點屬性(Vertex Attribute),頂點屬性包含了位置(Position)數據,而這整個存儲頂點屬性的區域就是頂點緩衝對象(Vertex Buffer Object, VBO)

VAO

了解了 VBO 之後,我們再來了解什麼是 VAO。

圖 16 Vertex Array Objects

圖 16 為我們展示了頂點數組對象(Vertex Array Object, VAO) 鏈接存儲頂點屬性的頂點緩衝對象(VBO)的操作。在圖中我們可以觀察到,VAO 中存儲著 Attribute Pointer,即頂點屬性的指針。這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執行一次,之後再繪製物體的時候只需要綁定相應的 VAO 就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的 VAO 就行了。剛剛設置的所有狀態都將存儲在 VAO 中。所以,VAO 實際上是存儲頂點屬性指針的數組

OpenGL 的核心模式要求我們使用 VAO,所以它知道該如何處理我們的頂點輸入。如果我們綁定 VAO 失敗,OpenGL 會拒絕繪製任何東西。

EBO

在本文使用的示例代碼中,繪製了一個正方形,並且使用了元素緩衝對象(Element Buffer Object, EBO)。之所以使用 EBO,是因為在頂點數組中,有重複的頂點坐標。在 OpenGL 的圖形繪製中,實際上都是在繪製三角形。如果不使用 EBO 對象,讓 OpenGL 繪製一個正方形,那麼就需要輸入 6 個頂點,這樣就增加了頂點緩衝對象(VBO)佔用內存空間,造成了內存浪費,所以並不推薦。根據這樣的需求,我們大致修改下示例代碼。

由此可見,使用 EBO 對減少從代碼量和內存使用都有幫助,推薦使用 EBO 來優化 OpenGL 應用程序。

Bind Buffer and Draw

上段代碼就是生成(Generate)、綁定頂點數據有關 Buffer 的過程。通過對代碼的觀察,可以總結出以下規律:

必須先生成 VAO,glGenVertexArrays

綁定 VAO,glBindVertexArray

生成 VBO,glGenBuffers

綁定 VBO,glBindBuffer

把頂點緩衝對象複製到緩衝中供 OpenGL 使用,glBufferData

生成 EBO,glGenBuffers

綁定 EBO,glBindBuffer

把元素緩衝對象複製到緩衝中供 OpenGL 使用,glBufferData

設置頂點屬性,glVertexAttribPointer

當我們的頂點緩衝對象(VBO)、頂點數組對象(VAO)、元素緩衝對象(EBO)都生成並綁定,且 VBO、EBO 對象複製到緩衝中都完成後,我們就可以進行繪製了,代碼如下。

視口變換在 Graphics Pipeline 中已經有介紹,這裡,利用glViewport函數,對視口在窗體中的 x 坐標、y 坐標、寬度、高度進行設置,最終得到在窗體中的坐標(具體公示可以回顧 Primitive Assembly)。

glClearColor設置背景顏色,glClear清空屏幕。調用glUseProgram使用之前創建好並附加上我們編寫過 Shader 的 Shader Program,最後調用glDrawElements(使用了 EBO)繪製圖形。


到這裡,我們完成了 OpenGL 的一些基本概念的學習,並進行了簡單的 OpenGL 編程練習。最後回顧總結一下本文的重要概念,並對應文章開頭的提問來檢驗自己是否學有所獲。

OpenGL 客戶端在 CPU 上執行,而 OpenGL 服務端在 GPU 上執行。

OpenGL 本質上是一個狀態機(OpenGL State Machine)。

渲染上下文(Context),或者說簡單上下文,包含 OpenGL 狀態信息和應用的對象。

儘管應用程序可以維護多個上下文,但只有一個上下文能夠在一個線程中成為當前上下文。當前上下文是接收應用發出 OpenGL 命令的渲染上下文。

調用 OpenGL 命令去繪製一張圖片,創建新的渲染上下文,並把 OpenGL 的當前上下文切換到這張圖片的上下文,此時就發生了離屏渲染。

離屏渲染時,CPU 會對新創建的渲染上下文存儲的數據進行拷貝,這些渲染數據再經由主存、匯流排、顯存,最終傳遞到 GPU 進行大量並行計算,輸出 FrameBuffer。而在這個數據傳遞和處理過程中,由於 CPU 和主存、主存和顯存,以及顯存和 GPU 帶寬速度不一致,導致 CPU 數據拷貝和傳遞的過程中增加了調用 OpenGL 的開支。

OpenGL 的圖形渲染管線(Graphics Pipeline)指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程。

OpenGL Graphics Pipeline 包括頂點著色器(Vertex Shader)、圖元裝配(Shape Assembly)、幾何著色器(Geometry Shader)、光柵化(Rasterization)、片段著色器(Fragment Shader)、測試與混合(Tests and Blending)等階段。

頂點著色器(Vertex Shader)、幾何著色器(Geometry Shader)以及片段著色器(Fragment Shader)可以注入自己的編寫著色器程序來達到程序所需的渲染效果。現代 OpenGL 要求我們,必須定義至少一個頂點著色器和一個片段著色器(因為GPU中沒有默認的頂點/片段著色器)。所以,我們能夠自己編寫 Shader 對 Shader 源碼進行編譯等處理,然後對 Vertex Buffer Object、Element Buffer Object 以及 Vertex Array Object 進行綁定,最終輸出 FrameBuffer 將圖形繪製到屏幕上。這種用於編寫 Shader 程序的類 C 的編程語言,稱之為 GLSL(OpenGL Shading Language)。

存儲頂點屬性的區域就是頂點緩衝對象(Vertex Buffer Object, VBO)。

頂點數組對象(Vertex Array Object, VAO)實際上是存儲頂點屬性指針的數組。

元素緩衝對象(Element Buffer Object, EBO)是存儲重複使用的頂點的緩衝對象,用來減少 VBO 的內存損耗。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 NYcn 的精彩文章:

TAG:NYcn |