初识OpenGL
文章目錄
- 前置知識:
- 核心模式和渲染模式:
- 狀態機:
- 對象
- OpenGL上下文(Context)
- 幀緩沖區(FrameBuffer)
- 附著(Attachment)
- 紋理(Texture)和渲染緩沖區(RenderBuffer)
- 頂點數組(VertexArray)和頂點緩沖區(VertexBuffer)
- 索引數組(ElementArray)和索引緩沖區(ElementBuffer)
- OpenGL渲染管線
- OpenGL語法簡介
- 附加資源:
前置知識:
由于OpenGL是一個圖形API,并不是一個獨立的平臺,它需要一個編程語言來工作,在這里我們使用的是C++。所以,對C++的熟練掌握在學習這個教程中是必不可少的。但是,也不要求你的C++多熟練。至少會寫一些復雜的程序,基本的知識都了解。
除此之外,我們也將用到一些數學知識(線性代數、幾何、三角學)。不要被必須的數學知識嚇到了,幾乎所有的概念只要有基礎的數學背景都可以理解。我也會將數學的內容壓縮至極限。大部分的功能甚至都不需要你理解所有的數學知識,只要你會使用就行。
核心模式和渲染模式:
早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管線),這個模式下繪制圖形很方便。OpenGL的大多數功能都被庫隱藏起來,開發者很少有控制OpenGL如何進行計算的自由。而開發者迫切希望能有更多的靈活性。隨著時間推移,規范越來越靈活,開發者對繪圖細節有了更多的掌控。立即渲染模式確實容易使用和理解,但是效率太低。因此從OpenGL3.2開始,規范文檔開始廢棄立即渲染模式,并鼓勵開發者在OpenGL的核心模式(Core-profile)下進行開發,這個分支的規范完全移除了舊的特性。
當使用OpenGL的核心模式時,OpenGL迫使我們使用現代的函數。當我們試圖使用一個已廢棄的函數時,OpenGL會拋出一個錯誤并終止繪圖。現代函數的優勢是更高的靈活性和效率,然而也更難于學習。立即渲染模式從OpenGL實際運作中抽象掉了很多細節,因此它在易于學習的同時,也很難讓人去把握OpenGL具體是如何運作的。現代函數要求使用者真正理解OpenGL和圖形編程,它有一些難度,然而提供了更多的靈活性,更高的效率,更重要的是可以更深入的理解圖形編程。
這也是為什么我們的教程面向OpenGL3.3的核心模式。雖然上手更困難,但這份努力是值得的。
現今,更高版本的OpenGL已經發布(寫作時最新版本為4.5),你可能會問:既然OpenGL 4.5 都出來了,為什么我們還要學習OpenGL 3.3?答案很簡單,所有OpenGL的更高的版本都是在3.3的基礎上,引入了額外的功能,并沒有改動核心架構。新版本只是引入了一些更有效率或更有用的方式去完成同樣的功能。因此,所有的概念和技術在現代OpenGL版本里都保持一致。當你的經驗足夠,你可以輕松使用來自更高版本OpenGL的新特性。
狀態機:
OpenGL自身是一個巨大的狀態機(State Machine):一系列的變量描述OpenGL此刻應當如何運行。OpenGL的狀態通常被稱為OpenGL上下文(Context)。我們通常使用如下途徑去更改OpenGL狀態:設置選項,操作緩沖。最后,我們使用當前OpenGL上下文來渲染。
假設當我們想告訴OpenGL去畫線段而不是三角形的時候,我們通過改變一些上下文變量來改變OpenGL狀態,從而告訴OpenGL如何去繪圖。一旦我們改變了OpenGL的狀態為繪制線段,下一個繪制命令就會畫出線段而不是三角形。
當使用OpenGL的時候,我們會遇到一些狀態設置函數(State-changing Function),這類函數將會改變上下文。以及狀態使用函數(State-using Function),這類函數會根據當前OpenGL的狀態執行一些操作。只要你記住OpenGL本質上是個大狀態機,就能更容易理解它的大部分特性。
注意:amqp的連接庫對應vs版本下載
對象
OpenGL庫是用C語言寫的,同時也支持多種語言的派生,但其內核仍是一個C庫。由于C的一些語言結構不易被翻譯到其它的高級語言,因此OpenGL開發的時候引入了一些抽象層。“對象(Object)”就是其中一個。
在OpenGL中一個對象是指一些選項的集合,它代表OpenGL狀態的一個子集。比如,我們可以用一個對象來代表繪圖窗口的設置,之后我們就可以設置它的大小、支持的顏色位數等等。可以把對象看做一個C風格的結構體(Struct):
當我們使用一個對象時,通常看起來像如下一樣(把OpenGL上下文看作一個大的結構體):
// OpenGL的狀態struct OpenGL_Context {...object* object_Window_Target;... }; // 創建對象unsigned int objectId = 0; glGenObject(1, &objectId);// 綁定對象至上下文 glBindObject(GL_WINDOW_TARGET, objectId);// 設置當前綁定到 GL_WINDOW_TARGET 的對象的一些選項 glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800); glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);// 將上下文對象設回默認 glBindObject(GL_WINDOW_TARGET, 0);這一小段代碼展現了你以后使用OpenGL時常見的工作流。我們首先創建一個對象,然后用一個id保存它的引用(實際數據被儲存在后臺)。然后我們將對象綁定至上下文的目標位置(例子中窗口對象目標的位置被定義成GL_WINDOW_TARGET)。接下來我們設置窗口的選項。最后我們將目標位置的對象id設回0,解綁這個對象。設置的選項將被保存在objectId所引用的對象中,一旦我們重新綁定這個對象到GL_WINDOW_TARGET位置,這些選項就會重新生效。
目前提供的示例代碼只是OpenGL如何操作的一個大致描述,通過閱讀以后的教程你會遇到很多實際的例子。
使用對象的一個好處是在程序中,我們不止可以定義一個對象,并設置它們的選項,每個對象都可以是不同的設置。在我們執行一個使用OpenGL狀態的操作的時候,只需要綁定含有需要的設置的對象即可。比如說我們有一些作為3D模型數據(一棟房子或一個人物)的容器對象,在我們想繪制其中任何一個模型的時候,只需綁定一個包含對應模型數據的對象就可以了(當然,我們需要先創建并設置對象的選項)。擁有數個這樣的對象允許我們指定多個模型,在想畫其中任何一個的時候,直接將對應的對象綁定上去,便不需要再重復設置選項了。
OpenGL上下文(Context)
在應用程序調用任何OpenGL的指令之前,需要安排首先創建一個OpenGL的上下文。這個上下文是一個非常龐大的狀態機,保存了OpenGL中的各種狀態,這也是OpenGL指令執行的基礎。
OpenGL的函數不管在哪個語言中,都是類似C語言一樣的面向過程的函數,本質上都是對OpenGL上下文這個龐大的狀態機中的某個狀態或者對象進行操作,當然你得首先把這個對象設置為當前對象。因此,通過對OpenGL指令的封裝,是可以將OpenGL的相關調用封裝成為一個面向對象的圖形API的。
由于OpenGL上下文是一個巨大的狀態機,切換上下文往往會產生較大的開銷,但是不同的繪制模塊,可能需要使用完全獨立的狀態管理。因此,可以在應用程序中分別創建多個不同的上下文,在不同線程中使用不同的上下文,上下文之間共享紋理、緩沖區等資源。這樣的方案,會比反復切換上下文,或者大量修改渲染狀態,更加合理高效的。
幀緩沖區(FrameBuffer)
OpenGL是圖形API,因此可以說所有的運算和結果最終都是需要通過圖像進行輸出的。那么繪圖必然就需要有一塊畫板,而幀緩沖區就是OpenGL中的畫板。但是特別需要注意的是,幀緩沖區不是常規意義緩沖區(就像鯨魚不是魚一樣),它并不是實際存儲數據的對象,類似畫畫的時候,需要在畫板上放一塊畫布,才能實際在畫布上進行繪畫,這些畫布可以是紋理(Texture)或者是渲染緩沖區(RenderBuffer),而放置這些畫布的位置被稱為幀緩沖區的附著(Attachment)。
附著(Attachment)
附著可以理解為畫板上的夾子,夾住了哪個畫布,就往對應畫布上輸出數據。
在幀緩沖區中可以附著3種類型的附著,顏色附著(ColorAttachment),深度附著(DepthAttachment),模板附著(StencilAttachment)。這三種附著對應的存儲區域也被稱為顏色緩沖區(ColorBuffer),深度緩沖區(DepthBuffer),模板緩沖區(StencilBuffer)。
顏色附著輸出繪制圖像的顏色數據,也就是平時常見的圖像的RGBA數據。如果使用了多渲染目標(Multiple Render Targets)技術,那么顏色附著的數量可能會大于一。
深度附著輸出繪制圖像的深度數據,深度數據主要在3D渲染中使用,一般用于判斷物體的遠近來實現遮擋的效果。
模板附著輸出模板數據,模板數據是渲染中較為高級的用法,一般用于渲染時進行像素級別的剔除和遮擋效果,常見的應用場景比如三維物體的描邊。
紋理(Texture)和渲染緩沖區(RenderBuffer)
前面已經說過,幀緩沖區并不是實際存儲數據的地方,實際存儲圖像數據數據的對象就是紋理和渲染緩沖區。
他們三者的關系是這樣的,紋理或渲染緩沖區作為幀緩沖區的附著。
那么,紋理和渲染緩沖區又有什么關系和區別呢?
紋理和渲染緩沖區同樣是存儲圖像的對象。一般來說,渲染緩沖區對應操作系統提供的窗口,而紋理代表列離屏的圖像存儲區域。因此,渲染緩沖區都是2D的圖像類型,而紋理一般有立方體紋理,1D、2D、3D紋理等類型,同時紋理還額外支持了mipmap等其他特性。
值得注意的是,一般來說渲染緩沖區和紋理不能同時掛載在同一個幀緩沖區上。
頂點數組(VertexArray)和頂點緩沖區(VertexBuffer)
準備好了畫布之后,就要開始畫圖了。畫圖一般是先畫好圖像的骨架,然后再往骨架里面填充顏色,這對于OpenGL也是一樣的。頂點數據就是要畫的圖像的骨架,和現實中不同的是,OpenGL中的圖像都是由圖元組成。在OpenGL ES中,有3種類型的圖元:點、線、三角形。那這些頂點數據最終是存儲在哪里的呢?開發者可以選擇設定函數指針,在調用繪制方法的時候,直接由內存傳入頂點數據,也就是說這部分數據之前是存儲在內存當中的,被稱為頂點數組。而性能更高的做法是,提前分配一塊顯存,將頂點數據預先傳入到顯存當中。這部分的顯存,就被稱為頂點緩沖區。
索引數組(ElementArray)和索引緩沖區(ElementBuffer)
其實我覺得索引在OpenGL叫Element確實有點不夠貼切,而在DirectX中叫做IndexBuffer更加合適一些。
索引數據的目的主要是為了實現頂點的復用,在繪制圖像時,總是會有一些頂點被多個圖元共享,而反復對這個頂點進行運算常常是沒有必要的(也有某些特殊場景需要)。因此對通過索引數據,指示OpenGL繪制頂點的順序,不但能防止頂點的重復運算,也能在不修改頂點數據的情況下,一定程度的重新組合圖像。
和頂點數據一樣,索引數據也可以以索引數組的形式存儲在內存當中,調用繪制函數時傳入;或者提前分配一塊顯存,將索引數據存儲在這塊顯存當中,這塊顯存就被稱為索引緩沖區。同樣的,使用緩沖區的方式,性能一般會比直接使用索引數組的方式更加高效。
OpenGL ES提供了2種主要的繪制方法:glDrawArrays和glDrawElements。前者對應的就是沒有索引數據的情況,后者對應的是有索引數據的情況。
OpenGL渲染管線
OpenGL實現了我們通常所說的渲染管線(redering pipeline),它是一系列數據處理過程,并將應用程序的數據轉換到最終渲染的圖像。
OpenGL首先接收用戶提供的幾何數據(頂點和幾何圖元),并且將它輸入到一系列著色器階段中進行處理,這些階段包括頂點著色、細分著色(它本身包含兩個著色器)以及最后的幾何著色,然后再經過圖元裝配和剪切后,將它們送到光柵化單元(rasterizer)。光柵化單元負責對所有剪切區域(clipping region)內的圖元生成片元數據,然后對每個生成的片元都執行一個片元著色器.
- 頂點著色器:對于繪制命令傳輸的每個頂點,OpenGL都會調用一個頂點著色器來處理頂點相關的數據。通常來說,一個復雜的應用程序可能包 含許多個頂點著色器,但是在同一時刻只能有一個頂點著色器起作用
- 細分著色器:頂點著色器處理每個頂點的關聯數據之后,如果同時激活了細分著色器(tessellation shader),那么它講進一步處理這些數據。細分 著色器會使用面片(patch)來描述一個物體的形狀,并且使用相對簡單的面片幾何體連接來完成細分的工作,其結果是幾何圖元 的數量增加,并且模型的外觀變得更加平順。細分著色階段會用到兩個著色器來分別管理面片數據并生產最終的形狀
- 幾何著色器:允許在光柵化之前對每個幾何圖元做更進一步的處理
- 圖元裝配 : 圖元裝配將頂點與幾何圖元之間組織起來,準備下一步的剪切和光柵化工作
- 剪切 : 頂點可能落在視口(viewport)之外,此時與頂點相關的圖元會做出改動,以保障相關的像素不會再視口外繪制
- 光柵化 : 剪切之后馬上要執行的工作,就是講更新后的圖元傳遞到光柵化(rasterizer)單元,生成對應的片元。光柵化的工作是判斷某一部分幾何體(點、線、三角形)所覆蓋的屏幕空間。得到了屏幕空間信息以及輸入的頂點數據之后,光柵化單元就可以直接對片元著色器中的每個可變變量進行線性插值,然后將結果傳遞給用戶的片元著色器。光柵化意味著一個片元的聲明伊始,而片元著色器 中的計算過程本質上意味著計算這個片元的最終顏色,它決不等價于OpenGL對這個片元所執行的全部操作
- 片元著色器: 這個最后一個可以通過編程控制顯示顏色的階段。片元著色器計算片元的最終顏色(盡管在逐片元操作中可能還會最終改變一次顏色)和它的深度值。片元著色器會使用紋理映射的方式,對頂點處理階段所計算的顏色紙進行補充。如果我們覺得不應該繼續繪制某個片元,在片元著色器中還可以終止這個片元的處理,這一步叫做片元的丟棄(discard)。總之,頂點著色(包括細分著色和幾何著色)決定了一個圖元應該位于屏幕的什么位置,而片元著色使用這些信息來決定某個片元的顏色應該是什么
- 逐片元的操作:除了在片元著色器中做的工作之外,片元操作的下一步就是最后的獨立片元處理過程。這個階段里會使用深度測試(depth test)和模板測試(stencil test)的方式來決定一個片元是否是可見的。如果一個片元通過了所有的測試,那么它就可以被直接繪制到幀緩存中了,它對應的像素的顏色值(也可能包括深度z值)會被更新,如果開啟了混合(blending)模式,那么片元的顏色會與該像素當前的顏色相疊加,形成一個新的顏色值并寫入幀緩存中。
OpenGL語法簡介
- OpenGL中所有函數都以字符gl作為前綴,還有些以glfw、gl3w或glew為前綴的函數,來自于第三方庫GLFW、GL3W、GLEW
- OpenGL中的常量也采用GL_為前綴,并且使用下劃線來分割單詞,如GL_COLOR。這些常量的定義是通過#defines來定義的,它們基本上都可以在OpenGL的頭文件glcorearb.h和glext.h中找到
- OpenGL中為函數定義了不同的數據類型,如GLfloat表示浮點型。另外,由于OpenGL是C語言的庫,沒有重載,所以OpenGL中區分同名函數使用后綴的變化來標記,例如glUniform2f()和glUniform3fv(),2f中的2表示兩個參數,f表示參數類型為GLfloat類型,即glUniform2f()需要傳入兩個GLfloat類型的參數,而glUniform3fv()中v表示vector(向量)類型,即這個函數我們需要使用一個3維向量作為參數傳入,這個三維向量的每個分量都是GLfloat類型,注意,在OpenGL中,向量vector類型是使用一維數組來表示,所以glUniform3fv()的參數為一個含有3個GLfloat值的一維數組
下面是命令后綴與參數數據類型的對應關系
注意: 盡量使用OpenGL的類型,因為有可能因為OpenGL自身的實現不同,可能會造成類型不匹配,同時,在不同的OpenGL實現之間移植代碼時,使用OpenGL類型就不會出現不匹配的問題
附加資源:
? opengl.org:OpenGL官方網站。
? OpenGL registry:包含OpenGL各版本的規范和擴展。
總結
- 上一篇: unity基础(1)
- 下一篇: 智慧社区下的智慧物业