天天看點

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

本節書摘來自華章出版社《opengl程式設計指南》一書中的第1章,第1.5節,作者 bill licea-kane ,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視

為了了解示例程式從一開始是如何運作的,首先了解一下main()函數當中都發生了什麼。前面的6行使用opengl utility toolkit初始化和打開了一個渲染用的視窗。這方面的詳細介紹可以參見附錄a,這裡隻介紹每一行的執行結果。

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

第一個函數glutinit()負責初始化glut庫。它會處理向程式輸入的指令行參數,并且移除其中與控制glut如何操作相關的部分(例如設定視窗的大小)。glutinit()必須是應用程式調用的第一個glut函數,它會負責設定其他glut例程所必需的資料結構。

glutinitdisplaymode()設定了程式所使用的視窗的類型。在這個例子中隻需要設定視窗使用rgba顔色空間(這會在第4章深入進行讨論)。除此之外,還可以給視窗設定更多的opengl特性,例如使用深度緩存或者動畫效果。

glutinitwindowsize()設定所需的視窗大小。如果不想在這裡設定一個固定值,也可以先查詢顯示裝置的尺寸,然後根據計算機的螢幕動态設定視窗的大小。

後面的兩個調用glutinitcontextversion()和glutinitcontextprofile()設定了我們所需的opengl環境(context)的類型—這是opengl内部用于記錄狀态設定和操作的資料結構。這個例子中使用opengl 4.3版本的核心模式(core profile)來建立環境。這個模式可以確定使用的隻是opengl的最新特性,否則也可以選擇另外一種相容模式,這樣自opengl 1.0版本以來的所有特性都可以在程式中使用。

随後的一個調用是glutcreatewindow(),它的功能和它的名字一緻。如果目前的系統環境可以滿足glutinitdisplaymode()的顯示模式要求,這裡就會建立一個視窗(此時會調用計算機視窗系統的接口)。隻有glut建立了一個視窗之後(其中也包含建立opengl環境的過程),我們才可以使用opengl相關的函數。

繼續這個例子的内容,接下來會調用glewinit()函數,它屬于我們用到的另一個輔助庫glew(opengl extension wrangler)。glew可以簡化擷取函數位址的過程,并且包含了可以跨平台使用的其他一些opengl程式設計方法。如果沒有glew,我們可能還需要執行相當多的工作才能夠運作程式。

到這裡,我們已經完成了使用opengl之前的全部設定工作。在馬上要介紹的init()例程中,我們将初始化opengl相關的所有資料,以便完成之後的渲染工作。

下一個例程是glutdisplayfunc(),它設定了顯示回調(display callback),即glut在每次更新視窗内容的時候會自動調用的例程。這裡給glut庫傳入display()這個函數的位址,後文會讨論其中的内容。glut可以使用一系列回調函數來處理諸如使用者輸入、重設視窗尺寸等不同的操作。附錄a會詳細地介紹glut庫的内容。

main()函數中調用的最後一個函數是glutmainloop(),這是一個無限執行的循環,它會負責一直處理視窗和作業系統的使用者輸入等操作。舉例來說,glutmainloop()會判斷視窗是否需要進行重繪,然後它就會自動調用glutdisplayfunc()中注冊的函數。特别要注意的是,glutmainloop()是一個無限循環,是以不會執行在它之後的所有指令。

下面将要讨論例1.1中的init()函數。首先再次列出與之相關的代碼。

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

初始化頂點數組對象

在init()中使用了不少函數和資料。在函數的起始部分,我們調用glgenvertexarrays()配置設定了頂點數組對象(vertex-array object)。opengl會是以配置設定一部分頂點數組對象的名稱供我們使用,在這裡共有numvaos個對象,即這個全局變量所指代的數值。glgenvertexarrays()的第二個參數傳回的是對象名的數組,也就是這裡的vaos。

我們對glgenvertexarrays()函數的完整解釋如下:

void glgenvertexarrays(glsizei n, gluint *arrays);

傳回n個未使用的對象名到數組arrays中,用作頂點數組對象。傳回的名字可以用來配置設定更多的緩存對象,并且它們已經使用未初始化的頂點數組集合的預設狀态進 行了數值的初始化。

我們會發現很多opengl指令都是glgen的形式,它們負責配置設定不同類型的opengl對象的名稱。這裡的名稱類似c語言中的一個指針變量,我們必須配置設定記憶體并且用名稱引用它之後,名稱才有意義。在opengl中,這個配置設定的機制叫做綁定對象(bind an object),它是通過一系列glbind形式的opengl函數集合去實作的。在這個例子中,我們通過glbindvertexarray()函數建立并且綁定了一個頂點數組對象。

void glbindvertexarray(gluint array);

glbindvertexarray()完成了三項工作。如果輸入的變量array非0,并且是glgenvertexarrays()所傳回的,那麼它将建立一個新的頂點數組對象并且與其名稱關聯起來。如果綁定到一個已經建立的頂點數組對象中,那麼會激活這個頂點數組對象,并且直接影響對象中所儲存的頂點數組狀态。如果輸入的變量array為0,那麼opengl将不再使用程式所配置設定的任何頂點數組對象,并且将渲染狀态重設為頂點數組的預設狀态。

如果array不是glgenvertexarrays()所傳回的數值,或者它已經被gldelete-vertexarrays()函數釋放了,那麼這裡将産生一個gl_invalid_operation錯誤。

這個例子中,在生成一個頂點數組對象的名字之後,就會使用glbindvertexarray()将它綁定起來。在opengl中這樣的對象綁定操作非常常見,但是我們可能無法立即了解它做了什麼。當我們第一次綁定對象時(例如,第一次用指定的對象名作為參數調用glbind()),opengl内部會配置設定這個對象所需的記憶體并且将它作為目前對象,即所有後繼的操作都會作用于這個被綁定的對象,例如,這裡的頂點數組對象的狀态就會被後面執行的代碼所改變。在第一次調用glbind()函數之後,新建立的對象都會初始化為其預設狀态,而我們通常需要一些額外的初始化工作來確定這個對象可用。

綁定對象的過程有點類似設定鐵路的道岔開關。一旦設定了開關,從這條線路通過的所有列車都會駛向對應的軌道。如果我們将開關設定到另一個狀态,那麼所有之後經過的列車都會駛向另一條軌道。opengl的對象也是如此。總體上來說,在兩種情況下我們需要綁定一個對象:建立對象并初始化它所對應的資料時;以及每次我們準備使用這個對象,而它并不是目前綁定的對象時。我們會在display()例程中看到後一種情況,即在程式運作過程中第二次調用glbindvertexarray()函數。

由于示例程式需要盡量短小,是以我們不打算做任何多餘的操作。舉例來說,在較大的程式裡當我們完成對頂點數組對象的操作之後,是可以調用gldeletevertexarrays()将它釋放的。

void gldeletevertexarrays(glsizei n, gluint *arrays);

删除n個在arrays中定義的頂點數組對象,這樣所有的名稱可以再次用作頂點數組。如果綁定的頂點數組已經被删除,那麼目前綁定的頂點數組對象被重設為0(類似執行了glbindbuffer()函數,并且輸入參數為0),而預設的頂點數組會變成目前對象。在arrays當中未使用的名稱都會被釋放,但是目前頂點數組的狀态不會發生任何變化。

最後,為了確定程式的完整性,我們可以調用glisvertexarray()檢查某個名稱是否已經被保留為一個頂點數組對象了。

glboolean glisvertexarray(gluint array);

如果array是一個已經用glgenvertexarrays()建立且沒有被删除的頂點數組對象的名稱,那麼傳回gl_true。如果array為0或者不是任何頂點數組對象的名稱,那麼傳回gl_false。

對于opengl中其他類型的對象,我們都可以看到類似的名為gldelete和glis的例程。

配置設定頂點緩存對象

頂點數組對象負責儲存一系列頂點的資料。這些資料儲存到緩存對象當中,并且由目前綁定的頂點數組對象管理。我們隻有一種頂點數組對象類型,但是卻有很多種類型的對象,并且其中一部分對象并不負責處理頂點資料。正如前文中所提到的,緩存對象就是opengl服務端配置設定和管理的一塊記憶體區域,并且幾乎所有傳入opengl的資料都是存儲在緩存對象當中的。

頂點緩存對象的初始化過程與頂點數組對象的建立過程類似,不過需要有向緩存中添加資料的一個過程。

首先,我們需要建立頂點緩存對象的名稱。我們調用的還是glgen*形式的函數,即glgenbuffers()。在這個例子中,我們配置設定numvbos個對象(vbo即vertex buffer objects)到數組buffers當中。以下是glgenbuffers()的詳細介紹。

void glgenbuffers(glsizei n, gluint *buffers);

傳回n個目前未使用的緩存對象名稱,并儲存到buffers數組中。傳回到buffers中的名稱不一定是連續的整型資料。

這裡傳回的名稱隻用于配置設定其他緩存對象,它們在綁定之後隻會記錄一個可用的狀态。

0是一個保留的緩存對象名稱,glgenbuffers()永遠都不會傳回這個值的緩存對象。

當配置設定緩存的名稱之後,就可以調用glbindbuffer()來綁定它們了。由于opengl中有很多種不同類型的緩存對象,是以綁定一個緩存時,需要指定它所對應的類型。在這個例子中,由于是将頂點資料儲存到緩存當中,是以使用gl_array_buffer類型。緩存對象的類型現在共有8種,分别用于不同的opengl功能實作。本書後面的章節會分别讨論各種類型的對應操作。

glbindbuffer()函數的詳細介紹如下。

void glbindbuffer(glenum target, gluint buffer);

指定目前激活的緩存對象。target必須設定為以下類型中的一個:gl_array_buffer、gl_element_array_buffer、gl_pixel_pack_buffer、gl_pixel_unpack_buffer、gl_copy_read_buffer、gl_copy_write_buffer、gl_transform_feedback_buffer和gl_uniform_buffer。buffer設定的是要綁定的緩存對象名稱。

glbindbuffer()完成了三項工作:1)如果是第一次綁定buffer,且它是一個非零的無符号整型,那麼将建立一個與該名稱相對應的新緩存對象。2) 如果綁定到一個已經建立的緩存對象,那麼它将成為目前被激活的緩存對象。3)如果綁定的buffer值為0,那麼opengl将不再對目前target應用任何緩存對象。

所有的緩存對象都可以使用gldeletebuffers()直接釋放。

void gldeletebuffers(glsizei n, const gluint *buffers);

删除n個儲存在buffers數組中的緩存對象。被釋放的緩存對象可以重用(例如,使用glgenbuffers())。

如果删除的緩存對象已經被綁定,那麼該對象的所有綁定将會重置為預設的緩存對象,即相當于用0作為參數執行glbindbuffer()的結果。如果試圖删除不存在的緩存對象,或者緩存對象為0,那麼将忽略該操作(不會産生錯誤)。

我們也可以用glisbuffer()來判斷一個整數值是否是一個緩存對象的名稱。

glboolean glisbuffer(gluint buffer);

如果buffer是一個已經配置設定并且沒有釋放的緩存對象的名稱,則傳回gl_true。如果buffer為0或者不是緩存對象的名稱,則傳回gl_false。

将資料載入緩存對象

初始化頂點緩存對象之後,我們需要把頂點資料從對象傳輸到緩存對象當中。這一步是通過glbufferdata()例程完成的,它主要有兩個任務:配置設定頂點資料所需的存儲空間,然後将資料從應用程式的數組中拷貝到opengl服務端的記憶體中。

有可能在很多不同的場景中多次應用glbufferdata(),是以我們有必要在這裡深入了解它的過程,盡管我們在這本書中還會多次遇到這個函數。首先,glbufferdata()的詳細定義介紹如下。

void glbufferdata(glenum target, glsizeiptr size, const glvoid *data, glenum usage);

在opengl服務端記憶體中配置設定size個存儲單元(通常為byte),用于存儲資料或者索引。如果目前綁定的對象已經存在了關聯的資料,那麼會首先删除這些資料。

對于頂點屬性資料,target設定為gl_array_buffer;索引資料為gl_element_array_buffer;opengl的像素資料為gl_pixel_unpack_buffer;對于從opengl中擷取的像素資料為gl_pixel_pack_buffer;對于緩存之間的複制資料為gl_copy_read_buffer和gl_copy_write_buffer;對于紋理緩存中存儲的紋理資料為gl_texture_buffer;對于通過transform feedback着色器獲得的結果設定為gl_transform_feedback_buffer;而一緻變量要設定為gl_uniform_buffer。

size表示存儲資料的總數量。這個數值等于data中存儲的元素的總數乘以機關元素存儲空間的結果。

data要麼是一個用戶端記憶體的指針,以便初始化緩存對象,要麼是null。如果傳入的指針合法,那麼将會有size大小的資料從用戶端拷貝到服務端。如果傳入null,那麼将保留size大小的未初始化的資料,以備後用。

usage用于設定配置設定資料之後的讀取和寫入方式。可用的方式包括gl_stream_draw、gl_stream_read、gl_stream_copy、gl_static_draw、gl_static_read、gl_static_copy、gl_dynamic_draw、gl_dynamic_read和gl_dynamic_copy。

如果所需的size大小超過了服務端能夠配置設定的額度,那麼glbufferdata()将産生一個gl_out_of_memory錯誤。如果usage設定的不是可用的模式值,那麼将産生gl_invalid_value錯誤。

一下子了解這麼多的内容可能有點困難,但是這個函數在後面的學習中會多次出現,是以有必要在本書的開始部分就詳細地對它做出講解。

在上面的例子中,直接調用了glbufferdata()。因為頂點資料就儲存在一個vertices數組當中。如果需要靜态地從程式中加載頂點資料,那麼我們可能需要從模型檔案中讀取這些數值,或者通過某些算法來生成。由于我們的資料是頂點屬性資料,是以設定這個緩存為gl_array_buffer,即指定它的第一個參數。我們還需要指定記憶體配置設定的大小(機關為byte),是以直接使用sizeof(vertices)來完成計算。最後,我們需要指定資料在opengl中使用的方式。因為我們隻是用它來繪制幾何體,不會在運作時對它做出修改,是以設定glbufferdata()的usage參數為gl_static_draw。

除此之外,usage還有一系列的選項可用,第3章會詳細地介紹它們。

如果我們仔細觀察vertices數組中的數值,就會發現它們在x和y方向都被限定在[–1, 1]的範圍内。實際上,opengl隻能夠繪制坐标空間内的幾何體圖元。而具有該範圍限制的坐标系統也稱為規格化裝置坐标系統(normalized device coordinate,ndc)。這聽起來好像是一個巨大的限制,但實際上并不是問題。第5章會介紹将3維空間中的複雜物體映射到規格化裝置坐标系中的數學方法。在這個例子中直接使用ndc坐标,不過實際上我們通常會使用一些更為複雜的坐标空間。

現在,我們已經成功地建立了一個頂點數組對象,并且将它傳遞到緩存對象中。下一步,我們要設定程式中用到的着色器了。

初始化頂點與片元着色器

對于每一個opengl程式,當它所使用的opengl版本高于或等于3.1時,都需要指定至少兩個着色器:頂點着色器和片元着色器。在這個例子中,我們通過一個輔助函數loadshaders()來實作這個要求,它需要輸入一個shaderinfo結構體數組(這個結構體的實作過程可以參見示例源代碼的頭檔案loadshaders.h)。

對于opengl程式員而言,着色器就是使用opengl着色語言(opengl shading language,glsl)編寫的一個小型函數。glsl是構成所有opengl着色器的語言,它與c++語言非常類似,盡管glsl中的所有特性并不能用于opengl的每個着色階段。我們可以以字元串的形式傳輸glsl着色器到opengl。不過為了簡化這個例子,并且讓讀者更容易地使用着色器去進行開發,我們選擇将着色器字元串的内容儲存到檔案中,并且使用loadshaders()讀取檔案和建立opengl着色器程式。使用opengl着色器進行程式設計的具體過程可以參見第2章的内容。

為了幫助讀者盡快開始了解着色器的内容,我們并沒有将所有相關的細節内容都立即呈現出來。事實上,本書後面的内容都會與glsl的具體實作相關,而現在,我們隻需要在例1.2中對頂點着色器的代碼做一個深入了解。

例1.2 triangles.cpp對應的頂點着色器:triangles.vert

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

沒錯,它的内容隻有這麼多。事實上這就是我們之前所說的傳遞着色器(pass-through shader)的例子。它隻負責将輸入資料拷貝到輸出資料中。不過即便如此,我們也還是要展開深入讨論。

第一行“#version 430 core”指定了我們所用的opengl着色語言的版本。這裡的“430”表示我們準備使用opengl 4.3對應的glsl語言。這裡的命名規範是基于opengl 3.3版本的。在那之前的opengl版本中,版本号所用的數字是完全不一樣的(詳細介紹參見第2章)。這裡的“core”表示我們将使用opengl核心模式(core profile),這與之前glut的函數glutinitcontextprofile()設定的内容應當一緻。每個着色器的第一行都應該設定“#version”,否則系統會假設使用“110”版本,但是這與opengl核心模式并不相容。我們在本書中隻針對330版本及以上的着色器以及它的特性進行講解;如果這個版本号不是最新的版本,那麼程式的可移植性應該會更好,但是你将無法使用最新的系統特性。

下一步,我們配置設定了一個着色器變量。着色器變量是着色器與外部世界的聯系所在。換句話說,着色器并不知道自己的資料從哪裡來,它隻是在每次運作時直接擷取資料對應的輸入變量。而我們必須自己完成着色管線的裝配(在後面内容中你将了解它所表示的意思),然後才可以将應用程式中的資料與不同的opengl着色階段互相關聯。

在這個簡單的例子中,隻有一個名為vposition的輸入變量,它被聲明為“in”。事實上,就算是這一行也包含了很多的内容。

我們最好從右往左來解讀這一行的資訊。

顯而易見vposition就是變量的名稱。我們使用一個字元“v”作為這個頂點屬性名稱的字首。這個變量所儲存的是頂點的位置資訊。

下一個字段是vec4,也就是vposition類型。在這裡它是一個glsl的四維浮點數向量。glsl中有非常多的資料類型,這會在第2章裡詳細介紹。

你也許已經注意到,我們在例1.1的程式中對每個頂點隻設定了兩個坐标值,但是在頂點着色器中卻使用vec4來表達它。那麼另外兩個坐标值來自哪裡?事實上opengl會用預設數值自動填充這些缺失的坐标值。而vec4的預設值為(0, 0, 0, 1),是以當僅指定了x和y坐标的時候,其他兩個坐标值(z和w)将被自動指定為0和1。

在類型之前就是我們剛才提到的in字段,它指定了資料進入着色器的流向。正如你所見,這裡還可以聲明變量為out。不過我們在這裡暫時還不會用到它。

最後的字段是layout(location = 0),它也叫做布局限定符(layout qualifier),目的是為變量提供中繼資料(meta data)。我們可以使用布局限定符來設定很多不同的屬性,其中有些是與不同的着色階段相關的。

在這裡,設定vposition的位置屬性location為0。這個設定與init()函數的最後兩行會共同起作用。

最後,在着色器的main()函數中實作它的主體部分。opengl的所有着色器,無論是處于哪個着色階段,都會有一個main()函數。對于這個着色器而言,它所實作的就是将輸入的頂點位置複制到頂點着色器的指定輸出位置gl_position中。後文中我們将會了解到opengl所提供的一些着色器變量,它們全部都是以gl_作為字首的。

與之類似,我們也需要一個片元着色器來配合頂點着色器的工作。例1.3所示就是片元着色器的内容。

例1.3 triangles.cpp對應的片元着色器:triangles.frag

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

令人高興的是,這裡大部分的代碼看起來很類似,雖然它們分别屬于兩個完全不同的着色器類型。我們還是需要聲明版本号、變量以及main()函數。這裡存在着一些差異,但是你依然可以看出,幾乎所有着色器的基本結構都是這樣的。

片元着色器的重點内容如下:

聲明的變量名為fcolor。沒錯,它使用了out限定符!在這裡,着色器将會把fcolor對應的數值輸出,而這也就是片元所對應的顔色值(是以這裡用到了字首字元“f”)。

設定片元的顔色。在這裡,每個片元都會設定一個四維的向量。opengl中的顔色是通過rgb顔色空間來表示的,其中每個顔色分量(r表示紅色,g表示綠色,b表示藍色)的範圍都是[0, 1]。留心的讀者在這裡可能會問,“但是這是一個四維的向量”。沒錯,opengl實際上使用了rgba顔色空間,其中第四個值并不是顔色值。它叫做alpha值,專用于度量透明度。第4章将深入讨論這個話題,但是在現在,我們将它直接設定為1.0,這表示片元的顔色是完全不透明的。

片元着色器具有非常強大的功能,我們可以用它來實作非常多的算法和技巧。

我們已經基本完成了初始化的過程。init()中最後的兩個函數指定了頂點着色器的變量與我們存儲在緩存對象中資料的關系。這也就是我們所說的着色管線裝配的過程,即将應用程式與着色器之間,以及不同着色階段之間的資料通道連接配接起來。

為了輸入頂點着色器的資料,也就是opengl将要處理的所有頂點資料,需要在着色器中聲明一個in變量,然後使用glvertexattribpointer()将它關聯到一個頂點屬性數組。

void glvertexattribpointer(gluint index, glint size, glenum type, glboolean normalized, glsizei stride, const glvoid *pointer);

設定index(着色器中的屬性位置)位置對應的資料值。pointer表示緩存對象中,從起始位置開始計算的數組資料的偏移值(假設起始位址為0),使用基本的系統機關(byte)。size表示每個頂點需要更新的分量數目,可以是1、2、3、4或者gl_bgra。type指定了數組中每個元素的資料類型(gl_byte、gl_unsigned_byte、gl_short、gl_unsigned_short、gl_int、gl_unsigned_int、gl_fixed、gl_half_float、gl_float或gl_double)。normalized設定頂點資料在存儲前是否需要進行歸一化(或者使用glvertexattribfourn*()函數)。stride是數組中每兩個元素之間的大小偏移值(byte)。如果stride為0,那麼資料應該緊密地封裝在一起。

看起來我們有一大堆事情需要考慮,因為glvertexattribpointer()其實是一個非常靈活的指令。隻要在記憶體中資料是規範組織的(儲存在一個連續的數組中,不使用其他基于節點的容器,比如連結清單),我們就可以使用glvertexattribpointer()告訴opengl直接從記憶體中擷取資料。在例子中,vertices裡已經包含了我們所需的全部資訊。表1-2所示為在這個例子裡glvertexattribpointer()中各個參數的設定及其意義。

表1-2 判斷glvertexattribpointer()中參數的例子

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

希望上面的參數解釋能夠幫助你判斷自己的資料結構所對應的數值。在後文中我們還會多次用到glvertexattribpointer()來實作示例程式。

這裡我們還用到了一個技巧,就是用glvertexattribpointer()中的buffer_offset宏來指定偏移量。這個宏的定義沒有什麼特别的,如下所示。

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

在以往版本的opengl當中并不需要用到這個宏,不過現在我們希望使用它來設定資料在緩存對象中的偏移量,而不是像glvertexattribpointer()的原型那樣直接設定一個指向記憶體塊的指針。

在init()中,我們還有一項任務沒有完成,那就是啟用頂點屬性數組。我們通過調用glenablevertexattribarray()來完成這項工作,同時将glvertexattribpointer()初始化的屬性數組指針索引傳入這個函數。有關glenablevertexattribarray()的詳細解釋如下所示。

void glenablevertexattribarray(gluint index);

void gldisablevertexattribarray(gluint index);

設定是否啟用與index索引相關聯的頂點數組。index必須是一個介于0到gl_max_vertex_attribs-1之間的值。

現在,我們隻需要完成繪制的工作即可。

在設定和初始化所有資料之後,渲染的工作(在這個例子中)就非常簡單了。display()函數隻有4行代碼,不過它所包含的内容在所有opengl程式中都會用到。下面我們先閱讀其中的代碼。

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

首先,我們要清除幀緩存的資料再進行渲染。清除的工作由glclear()完成。

void glclear(glbitfield mask);

清除指定的緩存資料并重設為目前的清除值。mask是一個可以通過邏輯“或”操作來指定多個數值的參數,可用的數值如表1-3所示。

我們會在第4章中學習深度緩存(depth buffer)與模闆緩存(stencil buffer)的内容,當然還有對顔色緩存(color buffer)的深入探讨。

現在你可能想知道,glclear()會使用一個什麼樣的清除數值。在這個例子中,我們直接使用opengl預設的清除顔色,即黑色。如果要改變清除顔色的數值,可以使用glclearcolor()。

《OpenGL程式設計指南》一1.5 第一個程式:深入分析

設定目前使用的清除顔色值,用于rgba模式下對顔色緩存的清除工作。(參見第4章有關rgba模式的内容)這裡的red、green、blue、alpha都會被截斷到[0, 1]的範圍内。預設的清除顔色是(0, 0, 0, 0),在rgba模式下它表示黑色。

清除顔色本身也是opengl狀态機制的一個例子,它的數值會一直保留在目前opengl環境當中。opengl有一個龐大的狀态量清單(詳細的介紹參見附錄d),當建立一個新的opengl環境時,所有的狀态量都會被初始化為預設數值。因為opengl會保留所有更改的狀态值,是以我們可以減少設定狀态數值的次數。

舉例說明清除顔色的用法,比如我們希望将目前視口的背景顔色設定為白色,那麼需要調用glclearcolor(1, 1, 1, 1)。但是我們應該在什麼時候調用這個函數呢?當然,我們可以直接在display()函數中調用glclear()之前調用它。但是這樣的話,除了第一次進入循環之時,其他所有對glclearcolor()的調用都是多餘的—因為opengl在每次渲染時都會重複設定清除顔色的狀态值為白色。另一個效率更高的方法是在init()函數中設定清除顔色。事實上,這樣我們就可以避免備援的狀态切換;所有在程式運作時不會發生變化的數值都應該在init()中設定。當然,備援的函數調用本身并沒有危害,但是它會造成程式運作速度稍微變慢。

試一試 在triangles.cpp中添加對于glclearcolor()的調用。

使用opengl進行繪制

例子中後面兩行的工作是選擇我們準備繪制的頂點資料,然後請求進行繪制。首先調用glbindvertexarray()來選擇作為頂點資料使用的頂點數組。正如前文中提到的,我們可以用這個函數來切換程式中儲存的多個頂點資料對象集合。

其次調用gldrawarrays()來實作頂點資料向opengl管線的傳輸。

使用目前綁定的頂點數組元素來建立一系列的幾何圖元,起始位置為first,而結束位置為first + count-1。mode設定了建構圖元的類型,它可以是gl_points、gl_lines、gl_line_strip、gl_line_loop、gl_triangles、gl_triangle_strip、gl_triangle_fan和gl_patches中的任意一種。

在這個例子中,我們使用glvertexattribpointer()設定渲染模式為gl_triangles,起始位置位于緩存的0偏移位置,共渲染numvertices個元素(這個例子中為6個),這樣就可以渲染出獨立的三角形圖元了。我們會在第3章詳細介紹所有的圖元形狀。

試一試 修改triangles.cpp讓它渲染一個不同類型的幾何圖元,例如gl_points或者gl_lines。你可以使用上文中列出的任何一種圖元,但是有些的結果可能會比較奇怪,此外gl_patches類型是不會輸出任何結果的,因為它是用于細分着色器的,參見第9章的内容。

最後在display()函數中調用glflush(),即強制所有進行中的opengl指令立即完成并傳輸到opengl服務端處理。在後文中我們很快就會把glflush()替換為另一個更為平滑的指令,但是這樣的話還需要對目前這個例子進行更多的設定。

強制之前的opengl指令立即執行,這樣就可以保證它們在一定時間内全部完成。

深入了解

在你的opengl程式設計生涯的某個時刻,你可能會被問及(或者自問)“這需要多少時間”?“這”可能是渲染一個物體、繪制一整個場景,或者opengl能夠實作的其他操作。為了能夠精确地度量和執行自己的任務,我們有必要了解opengl是在什麼時候完成這些操作的。

上文所述的glflush()指令看起來像是一個正确的答案,但是它不是。事實上glflush()隻是強制所有運作中的指令送入opengl服務端而已,并且它會立即傳回—它并不會等待所有的指令完成,而等待卻是我們所需要的。為此,我們需要使用glfinish()指令,它會一直等待所有目前的opengl操作完成後,再傳回。

強制所有目前的opengl指令立即執行,并且等待它們全部完成。

 你最好隻是在開發階段使用glfinish()—如果你已經完成了開發的工作,那麼最好去掉對這個指令的調用。雖然它對于判斷opengl指令運作效率很有幫助,但是對于程式的整體性能卻有着相當的拖累。

啟用和禁用opengl的操作

在第一個例子當中有一個重要的特性并沒有用到,但是在後文中我們會反複用到它,那就是對于opengl操作模式的啟用和禁用。絕大多數的操作模式都可以通過glenable()和gldisable()指令開啟或者關閉。

glenable()會開啟一個模式,gldisable()會關閉它。有很多枚舉量可以作為模式參數傳入glenable()和gldisable()。例如gl_depth_test可以用來開啟或者關閉深度測試;gl_blend可以用來控制融合的操作,而gl_rasterizer_discard用于transform feedback過程中的進階渲染控制。

很多時候,尤其是我們用opengl編寫的庫需要提供給其他程式員使用的時候,可以根據自己的需要來判斷是否開啟某個特性,這時候可以使用glisenabled()來傳回是否啟用指定模式的資訊。

glboolean glisenabled(glenum capability);

根據是否啟用目前指定的模式,傳回gl_true或者gl_false。

繼續閱讀