OpenGL(英語:Open Graphics Library,譯名:開放圖形庫或者“開放式圖形庫”)是用於渲染2D、3D矢量圖形的跨語言、跨平台的應用程序編程接口(API)。這個接口由近350個不同的函數調用組成,用來繪制從簡單的圖形比特到復雜的三維景象。而另一種程序接口系統是僅用於Microsoft Windows上的Direct3D。OpenGL常用於CAD、虛擬現實、科學可視化程序和電子游戲開發。
OpenGL被設計為只有輸出的,所以它只提供渲染功能。核心API沒有窗口系統、音頻、打印、鍵盤/鼠標或其他輸入設備的概念。雖然這一開始看起來像是一種限制,但它允許進行渲染的代碼完全獨立於他運行的操作系統,允許跨平台開發。
OpenGL沒有提供着色器編譯器,而是由顯卡驅動來完成着色器的編譯工作,也就是說,只要顯卡驅動支持對GLSL的編譯它就能運行,所以能夠跨平台。而DirectX是由微軟控制着色器的編譯,就算用了不同的硬件,同一個着色器的編譯也是一樣,所以支持的平台只有微軟自己的產品。
OpenGL:一般它被認為是一個API(Application Programming Interface, 應用程序編程接口),包含了一系列可以操作圖形、圖像的函數。實際僅僅是由Khronos組織制定並維護的規范(Specification)。
OpenGL規范嚴格規定了每個函數該如何執行,以及它們的輸出值。至於內部具體每個函數是如何實現(Implement)的,將由OpenGL庫的開發者自行決定。實際的OpenGL庫的開發者通常是顯卡的生產商。
核心模式與立即渲染模式:
早期OpenGL使用立即渲染(Immediate mode,也就是固定渲染管線)容易使用和理解,但是效率太低。從OpenGL3.2開始廢棄立即渲染模式,鼓勵使用核心模式(Core-profile)。
核心模式:要求使用者真正理解OpenGL和圖形編程,有一些難度,然而提供了更多的靈活性,更高的效率,可以更深入理解圖形編程。
- 擴展:
OpenGL的一大特性就是對擴展(Extension)的支持,當一個顯卡公司提出一個新特性或者渲染上的大優化,通常會以擴展的方式在驅動中實現。
- 狀態機:
OpenGL自身是一個巨大的狀態機(State Machine):一系列的變量描述OpenGL此刻應當如何運行。OpenGL的狀態通常被稱為OpenGL上下文(Context)。我們通常使用如下途徑去更改OpenGL狀態:設置選項,操作緩沖。最后,我們使用當前OpenGL上下文來渲染。
- 對象:
OpenGL庫是用C語言寫的,同時也支持多種語言的派生,但其內核仍是一個C庫。由於C的一些語言結構不易被翻譯到其它的高級語言,因此OpenGL開發的時候引入了一些抽象層。“對象(Object)”就是其中一個。
使用對象的一個好處是在程序中,我們不止可以定義一個對象,並設置它們的選項,每個對象都可以是不同的設置。在我們執行一個使用OpenGL狀態的操作的時候,只需要綁定含有需要的設置的對象即可。
- GLFW:
GLFW是一個專門針對OpenGL的C語言庫,它提供了一些渲染物體所需的最低限度的接口。它允許用戶創建OpenGL上下文,定義窗口參數以及處理用戶輸入。
- GLAD:
GLAD是用來管理OpenGL的函數指針的,所以在調用任何OpenGL的函數之前我們需要初始化GLAD。
- 視口(Viewport):
OpenGL幕后使用glViewport中定義的位置和寬高進行2D坐標的轉換,將OpenGL中的位置坐標轉換為你的屏幕坐標。例如,OpenGL中的坐標(-0.5, 0.5)有可能(最終)被映射為屏幕中的坐標(200,450)。注意,處理過的OpenGL坐標范圍只為-1到1,因此我們事實上將(-1到1)范圍內的坐標映射到(0, 800)和(0, 600)。
- 雙緩沖(Double Buffer)
應用程序使用單緩沖繪圖時可能會存在圖像閃爍的問題。 這是因為生成的圖像不是一下子被繪制出來的,而是按照從左到右,由上而下逐像素地繪制而成的。最終圖像不是在瞬間顯示給用戶,而是通過一步一步生成的,這會導致渲染的結果很不真實。為了規避這些問題,我們應用雙緩沖渲染窗口應用程序。前緩沖保存着最終輸出的圖像,它會在屏幕上顯示;而所有的的渲染指令都會在后緩沖上繪制。當所有的渲染指令執行完畢后,我們交換(Swap)前緩沖和后緩沖,這樣圖像就立即呈顯出來,之前提到的不真實感就消除了。
頂點數組對象:Vertex Array Object,VAO
頂點緩沖對象:Vertex Buffer Object,VBO
索引緩沖對象:Element Buffer Object,EBO或Index Buffer Object,IBO
- 圖形渲染管線:
在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關於把3D坐標轉變為適應你屏幕的2D像素。3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline,大多譯為管線,實際上指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程)管理的。圖形渲染管線可以被划分為兩個主要部分:第一部分把你的3D坐標轉換為2D坐標,第二部分是把2D坐標轉變為實際的有顏色的像素。這個教程里,我們會簡單地討論一下圖形渲染管線,以及如何利用它創建一些漂亮的像素。
圖形渲染管線接受一組3D坐標,然后把它們轉變為你屏幕上的有色2D像素輸出。圖形渲染管線可以被划分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數),並且很容易並行執行。正是由於它們具有並行執行的特性,當今大多數顯卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做着色器(Shader)。
OpenGL着色器是用OpenGL着色器語言(OpenGL Shading Language, GLSL)寫成的。
下面,你會看到一個圖形渲染管線的每個階段的抽象展示。要注意藍色部分代表的是我們可以注入自定義的着色器的部分。
首先,我們以數組的形式傳遞3個3D坐標作為圖形渲染管線的輸入,用來表示一個三角形,這個數組叫做頂點數據(Vertex Data);頂點數據是一系列頂點的集合。一個頂點(Vertex)是一個3D坐標的數據的集合。而頂點數據是用頂點屬性(Vertex Attribute)表示的,它可以包含任何我們想用的數據,我們還是假定每個頂點只由一個3D位置和一些顏色值組成的吧。
為了讓OpenGL知道我們的坐標和顏色值構成的到底是什么,OpenGL需要你去指定這些數據所表示的渲染類型。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitive),任何一個繪制指令的調用都將把圖元傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
頂點着色器--圖元裝配--幾何着色器--光柵化--片段着色器--混合階段
圖形渲染管線的第一個部分是頂點着色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點着色器主要的目的是把3D坐標轉為另一種3D坐標,同時頂點着色器允許我們對頂點屬性進行一些基本處理。
圖元裝配(Primitive Assembly)階段將頂點着色器輸出的所有頂點作為輸入(如果是GL_POINTS,那么就是一個頂點),並將所有的點裝配成指定圖元的形狀
圖元裝配階段的輸出會傳遞給幾何着色器(Geometry Shader)。幾何着色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。
渲染一個三角形的過程
幾何着色器的輸出會被傳入光柵化階段(Rasterization Stage),這里它會把圖元映射為最終屏幕上相應的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器運行之前會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。(OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據。)
片段着色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段着色器包含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色。
在所有對應顏色值確定以后,最終的對象將會被傳到最后一個階段,我們叫做Alpha測試和混合(Blending)階段。這個階段檢測片段的對應的深度(和模板(Stencil))值,用它們來判斷這個像素是其它物體的前面還是后面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend)。所以,即使在片段着色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
- 頂點輸入
開始繪制圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x、y和z)。OpenGL不是簡單地把所有的3D坐標變換為屏幕上的2D像素;OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它。所有在所謂的標准化設備坐標(Normalized Device Coordinates)范圍內的坐標才會最終呈現在屏幕上(在這個范圍以外的坐標都不會顯示)
通常深度可以理解為z坐標,它代表一個像素在空間中和你的距離,如果離你遠就可能被別的像素遮擋,你就看不到它了,它會被丟棄,以節省資源。
- 標准化設備坐標(Normalized Device Coordinates, NDC)
一旦你的頂點坐標已經在頂點着色器中處理過,它們就應該是標准化設備坐標了,標准化設備坐標是一個x、y和z值在-1.0到1.0的一小段空間。
你的標准化設備坐標接着會變換為屏幕空間坐標(Screen-space Coordinates),這是使用你通過glViewport函數提供的數據,進行視口變換(Viewport Transform)完成的。所得的屏幕空間坐標又會被變換為片段輸入到片段着色器中。
- 頂點着色器:
它會在GPU上創建內存用於儲存我們的頂點數據,還要配置OpenGL如何解釋這些內存,並且指定其如何發送給顯卡。頂點着色器接着會處理我們在內存中指定數量的頂點。
我們通過頂點緩沖對象(Vertex Buffer Objects, VBO)管理這個內存,它會在GPU內存(通常被稱為顯存)中儲存大量頂點。
頂點着色器(Vertex Shader)是幾個可編程着色器中的一個。如果我們打算做渲染的話,現代OpenGL需要我們至少設置一個頂點和一個片段着色器。
- 編譯着色器
我們已經寫了一個頂點着色器源碼(儲存在一個C的字符串中),但是為了能夠讓OpenGL使用它,我們必須在運行時動態編譯它的源碼
- 片段着色器
片段着色器(Fragment Shader)是第二個也是最后一個我們打算創建的用於渲染三角形的着色器。片段着色器所做的是計算像素最后的顏色輸出。
兩個着色器現在都編譯了,剩下的事情是把兩個着色器對象鏈接到一個用來渲染的着色器程序(Shader Program)中
- 着色器程序
着色器程序對象(Shader Program Object)是多個着色器合並之后並最終鏈接完成的版本。如果要使用剛才編譯的着色器我們必須把它們鏈接(Link)為一個着色器程序對象,然后在渲染對象的時候激活這個着色器程序。已激活着色器程序的着色器將在我們發送渲染調用的時候被使用。
在把着色器對象鏈接到程序對象以后,記得刪除着色器對象,
我們已經把輸入頂點數據發送給了GPU,並指示了GPU如何在頂點和片段着色器中處理它。但還沒結束,OpenGL還不知道它該如何解釋內存中的頂點數據,以及它該如何將頂點數據鏈接到頂點着色器的屬性上。我們需要告訴OpenGL怎么做。
- 鏈接頂點屬性
頂點着色器允許我們指定任何以頂點屬性為形式的輸入。這使其具有很強的靈活性的同時,它還的確意味着我們必須手動指定輸入數據的哪一個部分對應頂點着色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數據。
- 頂點數組對象
頂點數組對象(Vertex Array Object, VAO)可以像頂點緩沖對象那樣被綁定,任何隨后的頂點屬性調用都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執行一次,之后再繪制物體的時候只需要綁定相應的VAO就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。剛剛設置的所有狀態都將存儲在VAO中
- 流程:
一個儲存了我們頂點屬性配置和應使用的VBO的頂點數組對象。一般當你打算繪制多個物體時,你首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然后儲存它們供后面使用。當我們打算繪制物體的時候就拿出相應的VAO,綁定它,繪制完物體后,再解綁VAO。
- 索引緩沖對象:
索引緩沖對象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO),只儲存不同的頂點,並設定繪制這些頂點的順序。和頂點緩沖對象一樣,EBO也是一個緩沖,它專門儲存索引,OpenGL調用這些頂點的索引來決定該繪制哪個頂點。
- 着色器:
着色器(Shader)是運行在GPU上的小程序。這些小程序為圖形渲染管線的某個特定部分而運行。從基本意義上來說,着色器只是一種把輸入轉化為輸出的程序。着色器也是一種非常獨立的程序,因為它們之間不能相互通信;它們之間唯一的溝通只有通過輸入和輸出。
- GLSL:
着色器是使用一種叫GLSL的類C語言寫成的。GLSL是為圖形計算量身定制的,它包含一些針對向量和矩陣操作的有用特性。
着色器的開頭總是要聲明版本,接着是輸入和輸出變量、uniform和main函數。每個着色器的入口點都是main函數,在這個函數中我們處理所有的輸入變量,並將結果輸出到輸出變量中。如果你不知道什么是uniform也不用擔心,我們后面會進行講解。
- 數據類型:
GLSL中包含C等其它語言大部分的默認基礎數據類型:int、float、double、uint和bool。GLSL也有兩種容器類型,它們會在這個教程中使用很多,分別是向量(Vector)和矩陣(Matrix)
- vecn 包含n個float分量的默認向量
- bvecn 包含n個bool分量的向量
- ivecn 包含n個int分量的向量
- uvecn 包含n個unsigned int分量的向量
- dvecn 包含n個double分量的向量
可以分別使用.x、.y、.z和.w來獲取它們的第1、2、3、4個分量。GLSL也允許你對顏色使用rgba,或是對紋理坐標使用stpq訪問相同的分量。
- 輸入與輸出:
雖然着色器是各自獨立的小程序,但是它們都是一個整體的一部分,出於這樣的原因,我們希望每個着色器都有輸入和輸出,這樣才能進行數據交流和傳遞。GLSL定義了in和out關鍵字專門來實現這個目的。
- Uniform:
Uniform是一種從CPU中的應用向GPU中的着色器發送數據的方式,但uniform和頂點屬性有些不同。首先,uniform是全局的(Global)。全局意味着uniform變量必須在每個着色器程序對象中都是獨一無二的,而且它可以被着色器程序的任意着色器在任意階段訪問。第二,無論你把uniform值設置成什么,uniform會一直保存它們的數據,直到它們被重置或更新。(可以設置顏色隨時間變化)
- 更多屬性:
我們了解了如何填充VBO、配置頂點屬性指針以及如何把它們都儲存到一個VAO里。這次,我們同樣打算把顏色數據加進頂點數據中。我們將把顏色數據添加為3個float值至vertices數組。我們將把三角形的三個角分別指定為紅色、綠色和藍色:
float vertices[] = {
// 位置 // 顏色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 頂部
};
- 紋理:
為了能夠把紋理映射(Map)到三角形上,我們需要指定三角形的每個頂點各自對應紋理的哪個部分。這樣每個頂點就會關聯着一個紋理坐標(Texture Coordinate),用來標明該從紋理圖像的哪個部分采樣(譯注:采集片段顏色)。之后在圖形的其它片段上進行片段插值(Fragment Interpolation)。
紋理坐標在x和y軸上,范圍為0到1之間(注意我們使用的是2D紋理圖像)。使用紋理坐標獲取紋理顏色叫做采樣(Sampling)。紋理坐標起始於(0, 0),也就是紋理圖片的左下角,終始於(1, 1),即紋理圖片的右上角。
- 紋理過濾:
紋理坐標不依賴於分辨率(Resolution),它可以是任意浮點值,所以OpenGL需要知道怎樣將紋理像素(Texture Pixel,也叫Texel,譯注1)映射到紋理坐標。當你有一個很大的物體但是紋理的分辨率很低的時候這就變得很重要了。你可能已經猜到了,OpenGL也有對於紋理過濾(Texture Filtering)的選項。紋理過濾有很多個選項,但是現在我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。
- GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認的紋理過濾方式。當設置為GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理坐標的那個像素。
- GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理坐標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。
GL_NEAREST產生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素,而GL_LINEAR能夠產生更平滑的圖案,很難看出單個的紋理像素。GL_LINEAR可以產生更真實的輸出,但有些開發者更喜歡8-bit風格,所以他們會用GL_NEAREST選項。
- 多級漸遠紋理:
想象一下,假設我們有一個包含着上千物體的大房間,每個物體上都有紋理。有些物體會很遠,但其紋理會擁有與近處物體同樣高的分辨率。由於遠處的物體可能只產生很少的片段,OpenGL從高分辨率紋理中為這些片段獲取正確的顏色值就很困難,因為它需要對一個跨過紋理很大部分的片段只拾取一個紋理顏色。在小物體上這會產生不真實的感覺,更不用說對它們使用高分辨率紋理浪費內存的問題了。
OpenGL使用一種叫做多級漸遠紋理(Mipmap)的概念來解決這個問題,它簡單來說就是一系列的紋理圖像,后一個紋理圖像是前一個的二分之一。多級漸遠紋理背后的理念很簡單:距觀察者的距離超過一定的閾值,OpenGL會使用不同的多級漸遠紋理,即最適合物體的距離的那個。由於距離遠,解析度不高也不會被用戶注意到。同時,多級漸遠紋理另一加分之處是它的性能非常好。