一看就懂的 OpenGL 基礎概念丨音視訊基礎

語言: CN / TW / HK

這個公眾號會 路線圖 式的遍歷分享音視訊技術 音視訊基礎  →  音視訊工具  →  音視訊工程示例  →  音視訊工業實戰 關注一下成本不高,錯過乾貨損失不小 ↓↓↓

這篇文章是音視訊基礎專欄系列關於渲染的第一篇文章,我們來聊一聊 OpenGL,希望能做到讓沒接觸過 OpenGL 的同學能比較容易的建立起一個初步的印象。

這篇文章的內容包括:

  • 常見的移動端圖形渲染技術

  • OpenGL 在圖形應用程式中的角色

  • OpenGL 的渲染架構

  • OpenGL 狀態機思想

  • OpenGL 的圖形渲染管線

1、圖形渲染方案

提到移動裝置的圖形渲染,我們經常會聽到 OpenGL、OpenGL ES、Metal、Vulkan 等方案,它們有什麼差別呢?

  • OpenGL是一套跨語言、跨平臺,支援 2D、3D 圖形渲染 介面 。這套介面由一系列的函式組成,定義瞭如何對簡單及複雜的圖形進行繪製。這套介面涉及到對裝置的影象硬體進行呼叫,因此在不同的平臺基於這套統一介面做了對應的實現。

  • OpenGL ES是 OpenGL 的 子集 ,是針對手機和遊戲主機等嵌入式裝置而設計,去除了許多不必要和效能較低的 API 介面。

  • Metal是蘋果為了解決 3D 渲染效能問題而推出的框架,該技術將 3D 圖形渲染效能提高了 10 倍。

  • Vulkan是一套新的跨平臺支援 2D、3D 圖形渲染的介面。Vulkan 針對全平臺即時 3D 程式(如電子遊戲和互動媒體)設計,並提供高效能與更均衡的 CPU/GPU 使用。

這些渲染方案之間還有著一定的 歷史淵源

OpenGL 已經發展了 25 年以上,不斷滿足著行業需求。但是,隨著 CPU、GPU 等硬體技術的發展和 3D 等更復雜場景對效能的需要,OpenGL 已經逐漸滿足不了行業的需要了。後來在 2013 年,AMD 主導開發了 Mantle 。Mantle 是面向 3D 遊戲的新一代圖形渲染介面,可以讓開發人員直接操作 GPU 硬體底層,從而提高硬體利用率和遊戲效能,效果顯著。Mantle 很好的帶動了圖形行業發展,微軟參考 AMD Mantle 的思路開發了 DirectX 12 ,蘋果則提出了 Metal 。但是因為 AMD 行業影響力和領導力不足,Mantle 沒有發展成為全行業的標準。2015 年,AMD 宣佈不在維護 Mantle,Mantle 功成身退。Khronos 接過 AMD 手中的接力棒,在 Mantle 的基礎上推出了 Vulkan ,Khronos 最先把 Vulkan API 稱為『下一代 OpenGL 行動(glNext)』,但在正式宣佈 Vulkan 之後這些名字就沒有再使用了。

2014 年之前蘋果一直是使用 OpenGL ES 來處理底層渲染,之後慢慢的把渲染框架遷移到了 Metal。到 iOS 12 蘋果已經開始棄用 OpenGL,完全使用 Metal 實現底層渲染。不過 OpenGL 是跨平臺的且相當穩定,目前 Metal 還只是用於蘋果體系。

谷歌則是從 2016 年的 Android N(安卓 7.0)開始支援 Vulkan API。當然 OpenGL ES 也仍是持續支援的。

可以看到移動裝置的渲染方案基本上都是從 OpenGL 的思想上繼承和發展而來的,所以瞭解 OpenGL 就變得很有必要,我們接著往下講。

2、OpenGL 的角色

要了解 OpenGL,首先可以看看它在一個應用程式中的 位置和角色

OpenGL 不能開發程式、構建後臺,它只是一套處理圖形影象的統一規則。它在一個圖形應用程式中的角色大致如下圖所示:

OpenGL 在圖形應用中的角色(iOS)

上圖是基於 iOS 平臺的,圖中的 Core GraphicsCore AnimationCore Image 是 iOS 平臺封裝的繪製相關的上層 API,在 Android 平臺則是其他的 API,這裡不必深究。

在日常開發中,開發者一般通過使用上層 API 來構建和繪製介面,而呼叫 API 時系統最終還是通過 OpenGL/Metal/Vulkan 來實現檢視的渲染。開發者也可以直接使用 OpenGL/Metal/Vulkan 來驅動 GPU 芯⽚⾼效渲染圖形影象以滿足一些特殊的需求。

3、OpenGL 的渲染架構

知道了 OpenGL 在整個應用程式中的定位和角色後,那它在內部是怎麼實現串聯上下游的呢?這就涉及到其 渲染架構 的設計了。

OpenGL 的渲染架構是 Client/Server 模式 :Client(客戶端)指的是我們在 CPU 上執行的一些程式碼,比如我們會編寫 OC/C++/Java 程式碼呼叫 OpenGL 的一些 API;而 Server(服務端)則對應的是圖形渲染管線,會呼叫 GPU 晶片。我們開發的過程就是不斷用 Client 通過 OpenGL 提供的通道去向 Server 端傳輸渲染指令,來間接的操作 GPU 晶片。

OpenGL 渲染架構

渲染架構的 Client 和 Server 是怎麼通訊和互動的呢?這又涉及到 C/S 通道 的設計,下面我們來接著介紹,不過這裡會提到一些你可能不太熟悉的名詞,可以先不用深究,有個印象就可以了。

OpenGL 提供了 3 個通道來讓我們從 Client 向 Server 中的頂點著色器(Vertex Shader)和片元著色器(Fragment Shader)傳遞引數和渲染資訊,如下圖所示:

OpenGL 渲染架構及資料互動通道

這 3 個通道分別是:

  • Attribute(屬性通道):通常用來傳遞經常可變引數。比如顏色資料、頂點資料、紋理座標、光照法線這些變數。

  • Uniform(統一變數通道):通常用來傳遞不變的引數。比如變化矩陣。一個圖形做旋轉的時候,實質上是這個圖形的所有頂點都做相應的變化,而這個變化的矩陣就是一個常量,可以用 Uniform 通道傳遞引數到頂點著色器的一個例項。再比如視訊的顏色空間通常用 YUV,但是 YUV 顏色空間想要正常渲染到螢幕上面,需要轉化成 RGBA 顏色空間,這個轉換就需要把 YUV 的顏色值乘以一個轉換矩陣轉換為 RGBA 顏色值,這個轉換矩陣也是一個常量,可以用 Uniform 通道傳遞引數到片元著色器的一個例項。

  • Texture Data(紋理通道):專門用來傳遞紋理資料的通道。

需要注意的是,這 3 個通道中 Uniform 通道和 Texture Data 通道都可以直接向頂點著色器和片元著色器傳遞引數,但是 Attribute 只能向頂點著色器傳遞引數,因為 OpenGL 架構在最初設計的時候,Attribute 屬性通道就是頂點著色器的專用通道。片元著色器中是不可能有 Attribute 的,但是我們可以使用 GLSL 程式碼,通過頂點著色器把 Attribute 資訊間接傳遞到片元著色器中。

另外,雖然 Texture Data 通道能直接向頂點著色器傳遞紋理資料,但是向頂點著色器傳遞紋理資料本身是沒有實質作用的,因為頂點著色器並不處理太多關於紋理的計算,紋理更多是在片元著色器中進行計算。

參考: 瞭解 OpenGL 渲染架構 [1]

4、OpenGL 狀態機

在 Client/Server 的渲染架構下,OpenGl 的渲染流程其實是基於一個 狀態機 來工作的。

我們先舉個例子說明什麼是狀態機。我們都坐過電梯,一般來說電梯有這樣幾個狀態: 開門關門執行(上升/下降)靜止

它們有什麼特點呢?

電梯只有靜止的時候才能開門,只有開門之後才能關門,只有關門之後才可以運動,只有運動之後才可以靜止,所以,可以說電梯的各個狀態是有依賴關係的,換種更專業的說法,就是各種狀態可以通過有向圖來表示。

電梯狀態圖

電梯不能隨意從一個狀態跳轉到另一個狀態,比如:不能在運動過程中開門。

關於 OpenGL 狀態機, Learn OpenGL [2] 中有概述:

OpenGL 自身是一個巨大的狀態機(State Machine):一系列的變數描述 OpenGL 此刻應當如何執行。OpenGL 的狀態通常被稱為 OpenGL 上下文(Context)。我們通常使用如下途徑去更改 OpenGL 狀態:設定選項,操作緩衝。最後,我們使用當前 OpenGL 上下文來渲染。

假設當我們想告訴 OpenGL 去畫線段而不是三角形的時候,我們通過改變一些上下文變數來改變 OpenGL 狀態,從而告訴 OpenGL 如何去繪圖。一旦我們改變了 OpenGL 的狀態為線段繪製模式,下一個繪製命令就會畫出線段而不是三角形。

當使用 OpenGL 的時候,我們會遇到一些狀態設定函式(State-changing Function),這類函式將會改變上下文。以及狀態使用函式(State-using Function),這類函式會根據當前 OpenGL 的狀態執行一些操作。只要你記住 OpenGL 本質上是個大狀態機,就能更容易理解它的大部分特性。

基於上面的理解,我們來看一段 OpenGL 的程式碼:

unsigned int VBO, VAO; 
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

初看這段程式碼,我們最深的印象可能是各種 glBind... ,字面上是 繫結 的意思,如果從狀態機的角度理解,其實 glBind... 就意味著 進入了某個狀態

所以我們可以用狀態圖來表示上面的程式碼如下:

示例程式碼狀態圖

不過 OpenGL 的狀態是可以 巢狀 的,所以細看上面的程式碼,我們還能看到這裡狀態存在包含關係,因為一個 VBO 會被綁定於一個 VAO 中,所以用下圖來看會更加直觀:

狀態巢狀示例

通俗來說就是,執行了繫結 X 到解綁 X 之間的任何操作,都會影響到 X。

明白了上面的狀態機機制後,相信後面學習 OpenGL 的程式碼就能降低不少難度了。

參考: OpenGL 工作機制 [3]

5、圖形渲染管線

一個一個狀態的切換以及在不同狀態中的渲染邏輯和資料處理構成了 OpenGL 的 渲染管線

什麼是管線?其實也可理解為一個 流程 。理解影象渲染管線前,我們可以想象一下如果讓你在螢幕上繪製一個三角形,你要怎麼做呢?

第一步,可能是先確定三角形三個頂點的位置:

三角形繪製流程 1

第二步,自然是將三個點用線段連起來:

三角形繪製流程 2

第三步,你可能覺得這樣的三角形太過於單調,於是準備給三角形上色,因為是在螢幕上的,而螢幕本質用是一個個畫素來顯示顏色的,所以上色之前要先確定好哪些畫素是屬於三角形的,於是你叫計算機把屬於三角形內部的畫素一個個圈出來:

三角形繪製流程 3

第四步,你想畫一個帶漸變色的炫酷三角形,所以需要給每個畫素都上不同的顏色,於是你給一個個畫素精心上色:

三角形繪製流程 4

這樣下來,一個漂亮的三角形就畫出來了。回想這個過程,其實就像工廠的流水線一樣,將整個工作拆解成一步一步實現即可。

OpenGL 的渲染管線其實也是類似的一個過程,它的工序包括: 頂點著色器 → 圖元裝配 → 幾何著色器 → 光柵化 → 片段著色器 → 測試與混合

OpenGL 渲染管線

這些工序是將輸入的 3D 的座標,轉化為顯示在螢幕上的 2D 的畫素的一個處理流程。

早期的 OpenGL 使用 立即渲染模式(Immediate Mode,也就是固定渲染管線) 。這種模式下繪製圖形很方便,OpenGL 的大多數功能都被庫隱藏起來,是一種 配置化(Configurable) 的管線,開發者很少有控制 OpenGL 如何進行計算的自由。而隨著需求場景變的多樣和複雜,開發者迫切希望能有更多的靈活性。隨著時間推移,規範越來越靈活,開發者對繪圖細節有了更多的掌控,現代 OpenGL 轉變為 可程式設計(Programmable) 渲染管線,而這裡的程式語言就是 GLSL 語言 ,它是一種類 C 的語言,專為圖形計算量身定製,包含了一些針對向量和矩陣操作的有用特性,我們用它編寫我們自己的頂點著色器和片段著色器。

上面的介紹中我們多次提到了一個詞: 著色器(Shader) ,它是什麼呢?

著色器就是一段執行在 GPU 中的程式,這段程式由開發者編寫,所以說為開發者提供了很大的靈活度和可掌控度。現在 OpenGL 主要有三種著色器: 頂點著色器幾何著色器片段著色器 ,其中頂點著色器和片段著色器為開發者必須提供,幾何著色器為可選提供。

下面我們介紹一下 OpenGL 渲染管線的幾個重要工序:

1)頂點著色器(Vertex Shader)

頂點著色器主要用於 確定繪製圖形的形狀,以及接收開發者傳入的資料並傳給後面階段 。接收外部傳入的頂點資料,根據需要對頂點資料進行變換處理之後,再將頂點資料傳入下一個階段圖元裝配。另外頂點著色器也接收外部傳進來的顏色值以及紋理取樣器,然後再傳遞給下一個階段進行圖元裝配處理。

每個頂點著色器只接收處理一個頂點座標,有多少個頂點就會執行多少次。

2)圖元裝配

圖元裝配階段是接收頂點著色器的輸出資料, 將頂點著色器傳來的頂點資料組裝為圖元 。就如上面畫三角形中所說的將三角形三個頂點連線起來,具體連線方式需要開發者指定。所謂圖元,指的就是點、線、三角形等最基本的幾何圖形,再複雜的圖形也離不開這些基本圖形的組成。另外,圖元裝配階段還會將超出螢幕的頂點座標進行裁剪,裁剪之後,頂點座標被轉化為螢幕座標,之後將圖元資料傳遞給管線的下一個階段進行光柵化(幾何著色器為非必須階段,這裡就暫時不講了)。

下圖是 OpenGL 支援的圖元型別:

OpenGL 圖元型別

3)光柵化

拿到圖元裝配傳遞過來的圖元資料,光柵化要做的就是 將一個圖元轉化為一張二維的圖片 。而這張圖片由若干個 片段(fragment) 組成(可以當做將這張圖拆解為一個個類似螢幕上畫素的小片段),片段可以近似看成畫素,但是又略有不同,一個片段包含渲染該片段所需要的位置、顏色和深度的全部資訊。光柵化完成之後,就把每個片段傳給片段著色器。

4)片段著色器(Fragment Shader)

接下來的階段是片段著色器,這是另外一個必須有的重要著色器,也是最後一個可以通過程式設計來控制螢幕是上顯示顏色的階段(後面的混合測試階段還可以改變片段的顏色),在這個階段主要是 計算片段的顏色 。這裡每個片段著色器接收一個片段資料的輸入,所以有幾個片段就會執行所少次,根據具體需要靈活設定該片段的顏色。然後片段資料就被傳遞到下一個階段:測試與混合。

5)測試和混合

這個階段的測試是專門用來 丟棄一些不需要顯示的片段 ,其中測試主要包含 深度測試模板測試

深度測試是在顯示 3D 圖形的時候,根據片段的深度來防止被阻擋的面渲染到其它面的前面。這裡是 OpenGL 內部維護一個 深度緩衝 ,儲存這一幀中深度最小的片段的深度,然後對螢幕同一個位置的其他片段的深度再進行比較,深度比緩衝中大的片段則丟棄,直到找到深度最小的片段,就將其顯示出來。

深度測試

上圖中每個方格表示一個片段,片段上的數值表示當前片段的深度,R 則表示深度無限,加號表示 2 個圖形疊加一起,則由下面部分的圖可知,當 2 個圖形疊加在一起的時候,同一個位置的片段總是顯示深度較小的那一個。

模板緩衝區是用於控制螢幕需要顯示的內容,螢幕大小決定了模板緩衝區大小;模板測試基於 模板緩衝區 ,從而讓我們完成想要的效果。模板測試類似於 與運算

模板測試

上圖可以看出,模板就是每個片段位置有 0 也有 1,然後和緩衝中的影象資料對應片段進行類似與運算,也類似與拿一個遮罩罩住,只留下 1 的對應片段顯示出來。

混合則是 計算帶有透明度的片段的最終顏色 ,在這個階段會與顯示在它背後的片段的顏色按照透明度進行疊加行成新的顏色,通俗講就是形成透明物體的效果。

混合

由圖可以看出,通過混合,右邊的窗戶既有部分自己的顏色,又有窗戶裡面物體的部分顏色,就是兩者透明度按照比例疊加的結果。

於是走完整個渲染管線流程,我們的渲染工作就算是告一段落了。

我們再來回顧一下這條 渲染管線 做了哪些事情:

首先我們傳入了圖形的 頂點資料 ,然後 OpenGL 內部會按照指定的 圖元型別 自動將頂點連成圖形,然後再將圖形內的區域切成一個個 小片段 ,然後給每個小片段自由 上色 ,最後把被擋住的或者我們不想顯示的區域的下片段 丟棄 ,並且對有透明度的片段進行前後片段顏色的 混合

參考: 圖形渲染管線的那些事 [4]

到此,我們基本上就對 OpenGL 有個初步的認識了,至於更細節的知識則需要在實踐中去學習和領悟了。

參考資料

[1]

瞭解 OpenGL 渲染架構: http://www.jianshu.com/p/51be4551d36f

[2]

Learn OpenGL: http://learnopengl-cn.github.io/01%20Getting%20started/01%20OpenGL/

[3]

OpenGL 工作機制: http://juejin.cn/post/7121525553491869703

[4]

圖形渲染管線的那些事: http://juejin.cn/post/7119135465302654984

- 完 -

加我微信,拉你入群

謝謝看完全文,也點一下『贊』和 『在看』吧 ↓