1. 程式人生 > IOS開發 >OpenGL學習(三)-- OpenGL 基礎渲染

OpenGL學習(三)-- OpenGL 基礎渲染

我的 OpenGL 專題學習目錄,希望和大家一起學習交流進步!


一、基礎圖形管線

渲染管線(rendering pipeline),它是一系列資料處理過程,並且將應用程式的資料轉換到最終渲染的影象。下圖是 OpenGL 4.3 版本的管線。

管線.png
OpenGl渲染流程.png

OpenGL 中的 圖元 只不過是頂點的集合以預定義的方式結合在一起罷了。

通過最近學習 OpenGL 的藍寶書(《OpenGL超級寶典》),學到了基礎渲染這塊,為了加深理解,按照書中優化了一下渲染管線的流程圖,並在圖中新增上了翻譯和自己的理解,加深自己的印象並幫助更多學習 OpenGL

的朋友們更好的學習。

OpenGL渲染管線簡化流程圖:

OpenGL渲染管線簡化流程.png

1、客戶端-伺服器

管線上半部分是客戶端,下半部分是伺服器。就 OpenGL 而言,客戶端是儲存在 CPU 儲存器中的,驅動程式將渲染命令與資料組合起來發給伺服器執行。 伺服器和客戶端在功能上是非同步的。客戶端不斷的將資料和命令組合在一起送入緩衝區,緩衝區再發送到伺服器執行。

2、著色器

上圖中最大的框代表的是 頂點著色器片元著色器。著色器是使用GLSL編寫的程式。

頂點著色器 頂點著色器處理從客戶端輸入的資料,用數學運算來計算光照效果、位移、顏色值等。有幾個頂點,頂點著色器就要執行幾次。 上圖中的 圖元組合(Primitive Assembly)

框圖意在說明3個頂點已經組合在了一起。

片元著色器 片元著色器來計算片元的最終顏色(儘管在下一個階段(逐片元的操作)時可能還會改變顏色一次)和它的深度值。在這裡我們會使用紋理對映的方式,對頂點處理階段所計算的顏色值進行補充。如果我們覺得不應該繼續繪製某個片元,在片元著色器中還可以終止這個片元的處理,這一步叫做片元的 丟棄(discard)

頂點的著色器和片元著色器之間的區別: 頂點著色(包括細分和幾何著色) 決定了一個圖元應該位於螢幕的什麼位置,而 片元著色 使用這些資訊來決定某個片元的顏色應該是什麼。

著色器的渲染:

  • 頂點著色器(必要)
  • 細分著色器(可選)
  • 幾何著色器(可選)
  • 片元著色器(必要)

3、三種向OpenGL 著色器傳遞渲染資料的⽅法

屬性: 就是對⼀個頂點都要作出改變的資料元素。實際上,頂點位置本身就是一個屬性.。屬性可以是浮點型別,整型,布林型別等。

Uniform 值: 通過設定 Uniform 變數就緊接著傳送一個圖元批次處理命令。Uniform 變數實際上可以無限次的使⽤。 設定一個應用於整個表⾯面的單個顏色值,還可也是一個時間值。

使用下面的函式:

// Use a stock shader,and pass in the parameters needed
GLint UseStockShader(GLT_STOCK_SHADER nShaderID,...);
複製程式碼

傳遞不用的 Uniform 引數可以使用不同的儲存著色器: 首先定義一個顏色黑色 vBlack

GLfloat vBlack[] = { 0.0f,0.0f,1.0f };
複製程式碼

宣告一個全域性的儲存著色器變數 shaderManager

GLShaderManager		shaderManager;
複製程式碼
shaderManager.UseStockShader(GLT_SHADER_FLAT,transformPipeline.GetModelViewProjectionMatrix(),vBlack);
複製程式碼
//使用 單位著色器
//引數1:簡單的使用預設笛卡爾座標系(-1,1),所有片段都應用一種顏色。GLT_SHADER_IDENTITY
//引數2:著色器顏色
shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vGreen);
複製程式碼
GLShaderManger::UseStockShader(GLT_SHADER_IDENTITY,GLfloat mvp[16],GLfloat vColor[4])
複製程式碼

紋理: 對紋理進行取樣和篩選。紋理資料的作用不僅僅是表現圖形。很多圖形檔案格式都是以無符號位元組形式對顏色分量進行儲存的,但我們仍然可以設定浮點紋理。這就是說,任何大型浮點資料塊(例如消耗資源很大的函式的大型查詢表)都可以通過這種方式傳遞給著色器。

4、使用儲存著色器

在OpenGL 核心框架中,並沒提供任何內建渲染管線,在提交一個幾何圖形進行渲染之前,必須實現一個著色器。著色器由GLToolsC++GLShaderManager 管理。他們能夠滿足進行基本渲染的基本要求,要求不高的程式設計師,這些儲存著色器已經足以滿足他們的需求。但隨著時間和經驗的提升,大部分開發者可能不滿足於此,會著手去寫著色器,手寫的我會在以後的文章裡再寫出來。

4.1著色器的使用

1)GLShaderManager 的初始化:

GLShaderManager		shaderManager;
shaderManager.InitializeStockShaders();
複製程式碼

2)GLShaderManager 屬性:

儲存著色器為每個變數都使用一致的內部變數命名規則和相同的屬性槽(Attribute Slot)。下表列出了這些屬性: 表-GLShaderManager 預定義的識別符號

識別符號 描述
GLT_ATTRIBUTE_VERTEX 3分量(x,y,z)頂點位置
GLT_ATTRIBUTE_COLOR 4分量(r,g,b,a)顏色值
GLT_ATTRIBUTE_NORMAL 3分量(x,z)表面法線
GLT_ATTRIBUTE_TEXTURE0 第一對 2 分量(s,t)紋理座標
GLT_ATTRIBUTE_TEXTURE1 第二對 2 分量(s,t)紋理座標

4.2 GLShanderManager 的 uniform 值

一般情況,要對幾何圖形進行渲染,我們需要為讀寫遞交屬性矩陣,首先要繫結到我們想要使用的著色程式上,並提供程式的 Uniform 值。GLShanderManager 類可以(暫時)為我們完成這項工作。 useStockShader 函式會選擇一個儲存著色器並提供這個著色器的 Uniform 值,這些工作通過一次函式呼叫就能完成:

GLShaderManager::UseStockShader(GLenum shader,... ...);
複製程式碼

C 語言(或 C++ 語言)中,......表示函式接受一個可變的引數數量。就這個函式本身而言,它根據我們選擇的著色器,從堆疊中提取正確的引數,這些引數就是特定著色器要求的 Uniform 值。

(1) 單位(Identity)著色器 GLT_SHADER_IDENTITY

引數1:單位著色器
引數2:顏色值
GLShaderManager::UseStockShader(GLT_SHADER_IDENTITY,GLfoat vColor[4]);
複製程式碼

單位(Identity)著色器: 只是簡單地使用預設的笛卡爾座標系(座標範圍 -1.0~1.0)。所有片段都應用同一種顏色,結合圖形為實心和未渲染的。這種著色器只使用一個屬性 GLT_ATTRIBUTE_VERTEXvColor 引數包含了要求的顏色。

(2) 平面(Flat)著色器 GLT_SHADER_FLAT

引數1:平面著色器
引數2:允許變化的4*4矩陣
引數3:顏色值
GLShaderManager::UseStockShader(GLT_SHADER_FLAT,GLfoat mvp[16],GLfloat vColor[4]);
複製程式碼

**平面(Flat)著色器:**將單位著色器進行了擴充套件,允許為集合圖形變換指定一個 4 x 4 的變換矩陣。經常被稱作“模型師徒投影矩陣”。這種著色器只使用一個屬性 GLT_ATTRIBUTE_VERTEX

(3) 上色(Shaded)著色器 GLT_SHADER_SHADED

GLShaderManager::UseStockShader(GLT_SHADER_SHADED,GLfoat mvp[16]);
複製程式碼

**上色(Shaded)著色器:**唯一的 Uniform 值就是在幾何圖形中應用的變換矩陣。GLT_ATTRIBUTE_VERTEXGLT_ATTRIBUTE_COLOR 在這種著色器中都會使用。顏色值將被平滑地插入頂點之間(稱為平滑著色)。

(4) 預設光源著色器 GLT_SHADER_DEFAULT_LIGHT

引數1:預設光源著色器
引數2:模型檢視矩陣
引數3:投影矩陣
引數4:顏色值
GLShaderManager::UseStockShader(GLT_SHADER_DEFAULT_LIGHT,GLfoat mvMatrix[16],GLfloat pMatrix[16],GLfloat vColor[4]);
複製程式碼

**預設光源著色器:**這種著色器使物件產生陰影和光照的效果。需要模型檢視矩陣、投影矩陣和作為基本色的顏色值等 Uniform 值。所需的屬性有 GLT_ATTRIBUTE_VERTEX(頂點分量)和 GLT_ATTRIBUTE_NORMAL(表面法線)。

(5) 點光源著色器 GLT_SHADER_POINT_LIGHT_DIFF

引數1:點光源著色器
引數2:模型檢視矩陣
引數3:投影矩陣
引數4:視點座標系中的光源位置
引數5:顏色值
GLShaderManager::UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,GLfloat vLightPos[3],GLfloat vColor[4]);
複製程式碼

點光源著色器: 和預設光源著色器很相似,但光源位置可能是待定的。接受 4Uniform,即模型檢視矩陣、投影矩陣、視點座標系中的光源位置和物件的基本漫反射顏色。同樣所需的屬性有 **GLT_ATTRIBUTE_VERTEX(頂點分量)**和 GLT_ATTRIBUTE_NORMAL(表面法線)

(6) 紋理替換矩陣 GLT_SHADER_TEXTURE_REPLACE

GLShaderManager::UseStockShader(GLT_SHADER_TEXTURE_REPLACE,GLint nTextureUnit);
複製程式碼

**紋理替換矩陣:**著色器通過給定的模型檢視投影矩陣,使用繫結到 nTextureUnit(紋理單元) 指定的紋理單元的紋理對幾何圖形進行變換。片段顏色是從紋理樣本中直接獲取的。所需的屬性有 **GLT_ATTRIBUTE_VERTEX(頂點分量)**和 GLT_ATTRIBUTE_NORMAL(表面法線)

(7) 紋理調整著色器 GLT_SHADER_TEXTURE_MODULATE

GLShaderManager::UseStockShader(GLT_SHADER_TEXTURE_MODULATE,GLfloat vColor,GLint nTextureUnit);
複製程式碼

紋理調整著色器: 這種著色器將一個基本色乘以一個取自紋理單元 nTextureUnit 的紋理。所需的屬性有 GLT_ATTRIBUTE_VERTEX(頂點分量)GLT_ATTRIBUTE_TEXTURE0(紋理座標)

(8) 紋理光源著色器 GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF

引數1:紋理光源著色器
引數2:模型檢視矩陣
引數3:投影矩陣
引數4:視點座標系中的光源位置
引數5:幾何圖形的基本色
引數6:將要使用的紋理單元
GLShaderManager::UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF,GLfloat mvMatrix,GLfloat vBaseColor[4],GLint nTextureUnit);
複製程式碼

紋理光源著色器: 這種著色器將一個紋理通過漫反射照明計算進行調整(相乘),光線在視覺空間中的位置是給定的。這種著色器接受 5Uniform 值,即模型檢視矩陣、投影矩陣、視覺空間中的光源位置、幾何圖形的基本色和將要使用的紋理單元。所需的屬性有 GLT_ATTRIBUTE_VERTEX(頂點分量)GLT_ATTRIBUTE_NORMAL(表面法線)GLT_ATTRIBUTE_TEXTURE0(紋理座標)

二、OpenGL 基礎圖元

OpenGL 圖元的模式標識:

圖元型別 OpenGL 列舉量
GL_POINTS
GL_LINES
條帶線 GL_LINE_STRIP
迴圈線 GL_LINE_LOOP
獨立三角形 GL_TRIANGLES
三角形條帶 GL_TRIANGLE_STRIP
三角形扇面 GL_TRIANGLE_FAN

1、點和線

(1)點

是最簡單的影象。每個特定的頂點在螢幕上都僅僅是一個單獨的點。預設的情況下,點的大小是一個畫素的大小。我們可通過呼叫 glPointSize 改變預設點的大小:

void glPointSize(GLfloat size);
複製程式碼
//  1.最簡單也是最常用的 4.0f,表示點的大小
   glPointSize(4.0f);
    
// 2.設定點的大小範圍和點與點之間的間隔
GLfloat sizes[2] = {2.0f,4.0f};
GLfloat step = 1.0f;

// 獲取點大小範圍和最小步長(增量)
glGetFloatv(GL_POINT_SIZE_RANGE,sizes);
glGetFloatv(GL_POINT_GRAULARITY,&step); 
複製程式碼

還可以通過使用程式點大小模式來設定點大小。

// 3.通過使用程式點大小模式來設定點大小
glEnable(GL_PROGRAM_POINT_SIZE);
// 這種模式下允許我們通過程式設計在頂點著色器或幾何著色器中設定點大小。著色器內建變數:gl_PointSize,並且可以在著色器原始碼直接寫
 gl_PointSize = 5.0;
複製程式碼

(2)線

比點更進一步的是獨立線段。一個線段就是 2 個頂點之間繪製的。 預設情況下,線段的寬度是一個畫素。改變線段唯一的方式是使用函式 glLineWidth:

void glLineWidth(GLfloat width);
複製程式碼
// 設定獨立線段寬度為1.5f;
glLineWidth(1.5f);
複製程式碼

(3)線帶

**線帶(line strip)**連續地從一個頂點到下一個頂點繪製的線段,以形成一個真正連線點的線條。 (為了把圖形連線起來,每個連線的頂點會被選定 2 次。一次作為線段的終點、一次作為下一條線段的起點),這次是作為 GL_LINE_STRIP 繪製的。

(4)線環

線環(line loop) 是線帶的一種簡單拓展,線上帶的基礎上額外增加了一條連線著一批次中最後一個點和第一個點的線段。

2、繪製三角形

可能存在的最簡單的實體多邊形就是三角形,它只有 3 個邊。光柵化硬體最歡迎三角形。並且現在 OpenGL 已經是 OpenGL 中支援的唯一一種多邊形。每 3 個頂點定義一個新的三角形。

(1)單獨的三角形

如下圖是使用 GL_TRIANGLES 繪製的兩個三角形:

繪製三角形.png
繪製金字塔 下面繪製 4 個三角形組成金字塔形的三角形。我們可以使用方向鍵來旋轉金字塔,從不同角度進行觀察。但是這個金字塔木有底面,所以我們可以看到它的內部。
金字塔無底.png
程式碼如下:

    //通過三角形建立金字塔
    GLfloat vPyramid[12][3] = {
        -2.0f,-2.0f,2.0f,4.0f,0.0f};
    
    //GL_TRIANGLES 每3個頂點定義一個新的三角形
    triangleBatch.Begin(GL_TRIANGLES,12);
    triangleBatch.CopyVertexData3f(vPyramid);
    triangleBatch.End();
複製程式碼

(2)環繞

將順時針方向繪製的三角形用逆時針的方式繪製。 如下圖,在繪製第一個三角形時,線條是按照從 V0-V1,再到 V2。最後再回到 V0 的一個閉合三角形。 這個是沿著頂點順時針方向。 這種順序與方向結合來指定頂點的方式稱為 環繞。 下圖的 2 個三角形的環繞方向完全相反。

三角形環繞.png

正面與背面:

在預設的情況下,OpenGL 認為具有 逆時針方向 環繞的多邊形是 正面 的。而右側的 順時針方向 三角形是三角形的 背面

為什麼區分正背面很重要?

因為我們常常希望為一個多邊形的正面和背面分別設定不同的物理特徵。我們可以完全隱藏一個多邊形的背面,或者給它設定一種不同的顏色和反射屬性。紋理影象在背面三角形中也是相反的。在一個場景中,使所有的多邊形保持環繞方向的一致,並使用正面多邊形來繪製所有實心物體的表面是非常重要的。

如果想改變 OpenGL 這個預設行為,可以呼叫下面的函式: glFrontFace(GL_CW);

引數:GL_CW | GL_CCW

  • GL_CCW:表示傳入的mode會選擇逆時針為前向(預設:GL_CCW)
  • GL_CW:表示順時針為前向。

(3) 三角地帶

對於很多表面和形狀來說,我們可能需要繪製幾個相連的三角形。我們可以使用 GL_TRIANGLE_STRIP 圖元繪製一串相連的三角形。從而節省大量的時間。

三角形帶.png

使用三角帶而不是分別指定每個三角形,這樣做有兩個優點:

  • 用前 3 個頂點指定第 1 個三角形之後,對於接下來的每一個三角形,只需要再指定 1 個頂點。需要繪製大量的三角形時,採用這種方法可以節省大量的程式程式碼和資料儲存空間。
  • 提供運算效能和節省頻寬。更少的頂點意味著資料從記憶體傳輸到圖形卡的速度更快,並且頂點著色器需要處理的次數也更少了。

(4) 三角形扇

除了三角形帶之外,還可以使用 GL_TRIANGLE_FAN 建立一組圍繞一箇中心點的相連三角形。通過 4 個頂點所產生的包括 3 個三角形的三角形扇。 第一個頂點 V0 構建了扇形的原點,用前 3 個頂點指定了最初的三角形之後,後續的每個頂點都和原點 (V0) 以及之前緊挨著它的那個頂點 (Vn-1) 形成接下來的三角形。

三角形扇.png

3、一個簡單批次容器

GLTools 庫中包含額一個簡單的容器類,叫做 GLBatch。這個類可以作為7種圖元的簡單批次容器使用。而且它知道在使用 GL_ShaderManager 支援的任意儲存著色器時如何對圖元進行渲染。 使用 GLBatch 類非常簡單。首先對批次進行初始化,告訴這個類它代表哪種圖元,其中包括的頂點數,以及(可選)一組或兩組紋理座標。

引數1:圖元
引數2:頂點數
引數3:一組或者2組紋理座標(可選)
void GLBatch::Begain(GLeunm primitive,GLuint nVerts,GLuint nTexttureUnints = 0);
複製程式碼

然後,至少要複製一個由3分量(x,z)頂點組成的陣列。

//複製表面法線
void GLBatch::CopyVertexData3f(GLfloat *vVerts);
複製程式碼

還可以選擇複製表面發現、顏色和紋理座標。

//複製表面法線
void GLBatch::CopyNormalDataf(GLfloat *vNorms);
//複製顏色
void GLBatch::CopyColorData4f(GLfloat *vColors);
//複製紋理座標
void GLBatch::CopyTexCoordData2f(GLFloat *vTextCoords,GLuint uiTextureLayer);
複製程式碼

完成上述工作以後,可呼叫End來表明已經完成了資料複製工作,並且將設定內部標記,以通知這個類包含哪些屬性。

//結束繪製
void GLBatch::End(void);
複製程式碼

實際上,可以在任何我們想要的時候進行復制,只要不改變累的大小即可。 而一旦呼叫 End 函式,就不能再增加新的屬性了(也就是說我們現在也不能確定是否要有表面法線了)。

關於提交屬性的 OpengGL 實際內部執行機制實際上比這要複雜的多。GLBatch 類只是一個便利類(convenience class),就像使用GLUT一樣方便。

以上的總結參考了並部分摘抄了以下文章,非常感謝以下作者的分享!:

1、《OpenGL超級寶典 第5版》

2、《OpenGL程式設計指南(英文第八版)》

3、作者CC老師_HelloCoder的《004--OpenGL 圖元》

轉載請備註原文出處,不得用於商業傳播——凡幾多