1. 程式人生 > >Cocos2d-x 3.x 圖形學渲染系列三

Cocos2d-x 3.x 圖形學渲染系列三

筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人,已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社 和《Unity3D實戰核心技術詳解》電子工業出版社 等。

市面上,跨平臺引擎使用的底層圖形庫都是用OpenGL,很多人都認為OpenGL是一個API(Applicatoin Programming Interface,應用程式程式設計介面),因為它裡面包含了一系列操作圖形、影象的函式。實際上,OpenGL本身並不是一個API,它是一個由Khronos組織制定並維護的規範。OpenGL規範規定了庫中的每個函式如何執行,以及它們的輸出值。本書介紹的OpenGL是面向OpenGL3.3以上的版本。

OpenGL庫是用C語言寫的,同時也支援多種語言的派生,但其核心仍是一個C庫。由於C的一些語言結構不易被翻譯到其它的高階語言,因此OpenGL開發的時候引入了一些抽象層。“物件(Object)”就是其中一個。

在OpenGL中一個物件是指一些選項的集合,它代表OpenGL狀態的一個子集。比如,可以用一個物件來代表繪圖視窗的設定,之後我們就可以設定它的大小、支援的顏色位數等等。可以把物件看做一個C風格的結構體(Struct):

struct object_name {
    GLfloat  option1;
    GLuint   option2;
    GLchar[] name;
};

使用OpenGL時,建議使用OpenGL定義的基元型別。比如使用float時會加上字首GL(因此寫作GLfloat),int 寫成GLInt等等。下面通過一段程式碼給讀者介紹,如何理解OpenGL中的物件概念?程式碼段如下所示:

// 建立物件
GLuint 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位置,這些選項就會重新生效。如下語句:

glBindObject(GL_WINDOW_TARGET,objectId);

函式是繫結物件至上下文,OpenGL自身是一個巨大的狀態機(State Machine):一系列的變數描述OpenGL此刻應當如何執行,OpenGL的狀態通常被稱為OpenGL上下文(Context)。換句話說就是,當更改OpenGL狀態,比如設定某些選項後,用OpenGL上下文來渲染。下面再介紹一下在OpenGL的Shader程式設計中經常使用的著色器。

首先介紹一下著色器,著色器是使用一種叫GLSL的類C語言寫成的。GLSL是為圖形計算量身定製的,它包含一些針對向量和矩陣操作的所有特性。

     著色器中定義了輸入和輸出變數:uniform、varing、attribute以及main函式編寫。每個著色器的入口點都是main函式,在這個函式中處理所有的輸入變數,並將結果輸出到已定義的輸出變數中。

     著色器是各自獨立的小程式,它們都是一個整體的一部分,每個著色器都有輸入和輸出,這樣才能進行資料交流和傳遞。GLSL定義了in和out關鍵字專門來實現這個目的,每個著色器使用這兩個關鍵字設定輸入和輸出,只要一個輸出變數與下一個著色器階段的輸入匹配,它就會傳遞下去。但在頂點和片段著色器中會有點不同。頂點著色器應該接收的是一種特殊形式的輸入,否則就會效率低下。頂點著色器的輸入特殊在,它從頂點資料中直接接收輸入。而片段著色器,它需要一個vec4顏色輸出變數,因為片段著色器需要生成一個最終輸出的顏色。如果你在片段著色器沒有定義輸出顏色,OpenGL會把你的物體渲染為預設的黑色(或白色)。

   下面介紹一下頂點著色器和片段著色器二者是如何結合在一起的,如果開發者打算從一個著色器向另一個著色器傳送資料,那必須在傳送方著色器中宣告一個輸出,在接收方著色器中宣告一個相同定義的輸入。當型別和名字都一樣時,OpenGL就會把兩個變數連結到一起,它們之間就能傳送資料了(這是在連結程式物件時完成的)。以程式碼為例說明一下,首先給大家展示的是頂點著色器程式碼:

// position變數的屬性位置值為0
layout (location = 0) in vec3 position; 
// 為片段著色器指定一個顏色輸出
out vec4 vertexColor; 

voidmain()
{
	// 注意我們如何把一個vec3作為vec4的構造器的引數
	gl_Position = vec4(position, 1.0);
	// 把輸出變數設定為暗紅色
        vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); 
}

頂點著色器中實現了位置的輸出以及顏色的輸出,下面是片段著色器程式碼如下所示:

// 從頂點著色器傳來的輸入變數(名稱相同、型別相同)
in vec4 vertexColor; 
// 片段著色器輸出的變數名可以任意命名,型別必須是vec4
out vec4 color;

voidmain()
{
    color = vertexColor;
}

通過頂點著色器和片段著色器程式碼可以看到,在頂點著色器中聲明瞭一個vertexColor變數作為vec4輸出,並在片段著色器中聲明瞭一個相同的vertexColor。由於它們名字相同且型別相同,片段著色器中的vertexColor就和頂點著色器中的vertexColor連結了。它們的連結當然是GPU內部實現的,在這裡不需要再繼續深入理解,只要知道你原理就可以了。由於在頂點著色器中將顏色設定為深紅色,最終的片段也是深紅色的。實現的效果如下圖2-1所示:



下面介紹一下使用案例:

遊戲美工製作的模型由於匯出格式加密成二進位制的原因,對於大部分開發者模型內容都是不可見的,在這裡根據以前的開發經驗給讀者介紹一下模型內部組成。模型的最基本組成:頂點、法線、UV座標等數值,模型中的頂點是必須的,因為每個3D模型都是由點組成的三角面或者是四邊形,在引擎載入中都是以三角面為基本單元載入的。為了更好的給大家展示,下面自定義一個三角形,也是為了後面模型檔案內部的介紹以及模型檔案載入鋪路,下面是自定義的三角形結構體:

GLfloat 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// 頂部
};

三角形的定義用了陣列的形式,包括頂點的三維座標和顏色值,將定義的顏色和位置資訊通過GPU渲染展示出來,這就需要編寫頂點著色器和片段著色器程式碼,下面是對應的完整頂點著色器程式碼:

// 位置變數的屬性位置值為 0
attribute vec3 position; 
// 顏色變數的屬性位置值為 1
attribute vec3 color;    
// 向片段著色器輸出一個顏色
varying vec3 ourColor; 
void main()
{
gl_Position = vec4(position, 1.0);
// 將ourColor設定為我們從頂點資料那裡得到的輸入顏色
    ourColor = color; 
}

與頂點著色器對應的是片段著色器程式碼如下所示:

varying vec3 ourColor;
void main()
{
gl_FragColor = vec4(ourColor, 1.0f);
}

通過這兩個著色器就可以把定義的三角形顯示在螢幕上,下面把定義的頂點陣列在記憶體內部的結構體解釋一下:定義好的頂點也稱為VBO(全稱:Vertex Buffer Object)頂點快取物件在記憶體的分佈圖給讀者展示效果如下圖2-2所示:



學習Shader程式設計對於頂點的一些儲存,至少要明白其在記憶體是如何存放的。瞭解了其在記憶體的佈局後,呼叫OpenGL庫的介面函式glVertexAttribPointer更新頂點格式,程式碼如下所示:

// 位置屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 顏色屬性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
glEnableVertexAttribArray(1);

在設定函式引數時,第五個引數的含義是必須向右移動6float其中3個是位置值,另外3個是顏色值。這使我們的步長值為6乘以float的位元組數從而指定一個偏移量。對於每個頂點來說,位置頂點屬性在前,所以它的偏移量是0顏色屬性緊隨位置資料之後,所以偏移量就是3 * sizeof(GLfloat)用位元組來計算就是12位元組。其渲染的效果圖如下圖2-3所示:


通過給讀者展現的程式碼可以看到Shader的編寫還是比較容易掌握的,在以上程式碼中沒有使用任何演算法運算,只是為了通過Shader編寫渲染一個簡單的三角形。

小結:

學習OpenGL程式設計首先要了解Shader程式語言的基本語法,這與傳統的學習語言類似。如果開發者有C語言背景更好,這樣學習起來更順手。掌握語法後,網上有很多這方面的教程,試著拿別人已編寫Shader指令碼執行,看一下效果,然後再在已有的基礎上修改別人的指令碼,逐步的去掌握。另外Shader指令碼在程式中是不可以除錯的,如果遇到問題,只能通過賦值操作或者註釋行操作以及使用特定值操作,最終通過排除法找到問題所在。