1. 程式人生 > 其它 >【OpenGL基礎】|| OpenGL渲染過程介紹_右弦GISer的部落格

【OpenGL基礎】|| OpenGL渲染過程介紹_右弦GISer的部落格

文章目錄

1. 介紹

在OpenGL中,所有要素都是三維的,但螢幕卻是二維的,因此在渲染過程中,需要將3D座標轉換為適應螢幕的2D座標,其處理過程由圖形渲染管線Graphics Pipeline)管理。包括將3D座標轉換為2D座標;將2D座標轉換為實際的有顏色的畫素兩個部分。(2D座標是指點在二維空間的位置,而2D畫素是這個點的近似值,受螢幕解析度的限制)。

其中圖形渲染管線可以被劃分為幾個階段,每個階段會把前一階段的輸出作為輸入。在每個階段快速處理資料的小程式稱之為著色器

(shader)。著色器在GPU中執行,使用OpenGL著色器語言(OpenGL Shading Language,GLSL)編寫。

下圖是一個圖形渲染管線每個階段的抽象展示,藍色部分可以自定義著色器內容。

首先以陣列的形式傳遞3個3D座標作為圖形渲染管線的輸入,用來表示一個三角形。這個陣列叫做頂點資料(Vertex Data);頂點資料是一系列頂點的集合。一個頂點(Vertex)是一個3D座標,而頂點資料用頂點屬性(Vertex Attribute)表示,可以包含位置和顏色等資訊。在OpenGL渲染過程中,需要指定資料表示的渲染型別,即是點,還是三角形等,即圖元(Primitive),任何一個繪製指令的呼叫都需要將圖元傳遞給OpenGL。

(1)頂點著色器(Vertex Shader)作為第一個階段,將單獨的頂點作為輸入。頂點著色器執行對頂點屬性進行一些基本處理。
(2)圖元裝配(Primitive Assembly)將頂點著色器輸出的所有頂點作為輸入,並將所有的點裝配為指定圖元的形狀。
(3)幾何著色器(Geometry Shader)將圖元的頂點集合作為輸入,可以通過產生新頂點構造出新的圖元來生成其他形狀。
(4)光柵格化(Rasterization Stage)將圖元對映為最終螢幕上相應的畫素,生成供片元著色器使用的片元(Fragment)。在片元著色器執行之前會進行裁剪(Clipping),將超出檢視範圍外的畫素丟棄,以提升效率。
(5)片元著色器(Fragment Shader)

用於計算每個畫素的最終顏色。
(6)測試和混合(Test and blending)階段,用來檢測片元的深度,判斷物體的前後位置關係。

在OpenGL中,必須至少定義一個頂點著色器和一個片元著色器(GPU中沒有預設的),幾何著色器可選,通常直接使用預設的。

2. 頂點輸入

在開始繪製圖形之前,需要先給OpenGL輸入一些頂點資料,這些頂點都是3D座標格式(x,y,z),座標值在-1.0至1.0範圍內才會被處理。即在標準化裝置座標(Normalized Device Coordinate)範圍內的座標才會最終呈現在螢幕上(在範圍外的都不會顯示)。

如一個三角形的頂點陣列(三個頂點的z座標均為0,即深度一致):

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

標準化裝置座標:
當頂點座標在頂點著色器中處理之後,就會形成標準裝置座標(三個座標值都在-1.0到1.0之間)。在之後的處理過程中,標準化裝置座標會變換為螢幕空間座標(Screen-sapce coordinates),即glViewport函式通過視口變換(Viewport Transform)完成的。得到的螢幕座標將會被輸入到片元著色器中。

頂點資料在輸入頂點著色器之前,需要在GPU上儲存,通過頂點緩衝物件(Vertex Buffer Objects, VBO)管理這個記憶體,它會在GPU記憶體(視訊記憶體)中儲存大量的頂點,可以一次性將大批量的頂點資料傳送到顯示卡,減少時間消耗,其中CPU和顯示卡之間的通訊較慢,頂點著色器訪問視訊記憶體中的頂點資料非常快。

頂點緩衝物件有唯一的ID,可以使用glGenBuffers函式和一個ID生成一個VBO物件。

unsigned int VBO;
glGenBuffers(1, &VBO);

OpenGL有多種緩衝物件型別,頂點緩衝物件的型別是GL_ARRAY_BUFFER。OpenGL允許同時繫結多個緩衝(不同型別),可以使用glBindBuffer函式將新建立的緩衝繫結到GL_ARRAY_BUFFER目標上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);  

GL_ARRAY_BUFFER上的任何緩衝呼叫都會用來配置當前繫結的緩衝(VBO),然後呼叫glBufferData函式,將之前定義的頂點資料複製到緩衝的記憶體中。

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData函式專門用來將頂點資料繫結到緩衝中。第一個採納數是目標緩衝的型別;第二個引數是輸出資料的大小(以位元組為單位);第三個引數是待發送的實際資料;第四個引數是指定顯示卡如何管理給定的資料,有三種形式:

  • GL_STATIC_DRAW:資料不會或者幾乎不會改變
  • GL_DYNAMIC_DRAW:資料會被改變很多
  • GL_STAREAM_DRAW:資料每次繪製時都會改變

3. 頂點著色器

頂點著色器(Vertex Shader)作為可程式設計的著色器之一,使用著色器語言GLSL(OpenGL Shading Language)編寫,如下:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

#version 330 core表示OpenGL的版本號及使用核心模式。(對輸入資料,只傳輸未處理)。

in關鍵字,在頂點著色器中宣告所有的輸入頂點屬性(Input Vertex Attribute)。頂點都為3D座標,因此建立vec3型別的輸入變數aPos

頂點著色器需要將資料賦值給預定義的gl_Position變數(頂點著色器的輸出,vec4型別,w分量設定為1.0f)。

4. 編譯著色器

首先將頂點著色器編碼在C風格的字串中:

const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";

為了讓OpenGL使用,必須在執行時動態編譯。

首先需要建立一個著色器物件,使用ID進行引用。將頂點著色器儲存為unsigned int型別,然後使用glCreateShader建立,引數設定為頂點著色器(GL_VERTEX_SHADER

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

然後將著色器原始碼附加到著色器物件上,進行編譯。

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource函式第一個引數為著色器物件;第二個引數為傳遞的原始碼字串數量;第三個引數是著色器的原始碼。

在呼叫glCompileShader編譯著色器後,檢查是否編譯成功,並輸出錯誤資訊。定義一個整型變數用來表示是否編譯成功,使用glGetShaderiv檢查是否編譯成功,若編譯失敗,則使用glGetShaderInfoLog獲取錯誤資訊。

int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

5. 片元著色器

片元著色器(Fragment Shader)用於計算畫素最後的顏色輸出,OpenGL中顏色使用vec4型別表示,即RGBA,每個分量的值在0.0至1.0之間。

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

該片元著色器只有一個輸出變數,便是最終的輸出顏色。宣告輸出變數使用out關鍵字。編譯片元著色器的過程與頂點著色器類似,只是需要使用GL_FRAGMENT_SHADER常量作為著色器型別:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

當兩個著色器都編譯完成後,需要將兩個著色器物件連結到一個用來渲染的著色器程式(Shader Program)中。

6. 著色器程式

著色器程式物件(Shader Program Object)是多個著色器合併之後並最終連結的版本。若需要使用剛才編譯的著色器,則需要將其連結(Link)為一個著色器程式物件,然後在渲染物件時啟用這個著色器程式。

當連結著色器至一個程式時,它會將每個著色器的輸出連結到下個著色器的輸入,當輸出和輸入不匹配時,將會發生連線錯誤。

建立程式物件:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glCreateProgram函式建立一個程式,並返回新建立程式物件的ID引用。並將之前編譯的著色器附加到程式物件上,然後使用glLinkProgram連結。

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

使用glGetProGramivglGetProgramInfoLog檢查連結著色器程式是否成功:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    ...
}

使用glUseProgram函式啟用程式物件:

glUseProgram(shaderProgram);

啟用後每個每個著色器的呼叫和渲染呼叫都會使用這個程式物件,即將著色器連結到程式物件後,就可以刪除原來的著色器物件了。

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

7. 連結頂點屬性

在渲染前,需要指定輸入資料中的哪一個部分對應頂點著色器的哪一個頂點屬性。

頂點緩衝資料會被解析為以下示例:

即:

  • 位置資料被儲存為32位(4位元組)浮點值
  • 每個位置包含x,y,z共3個值,且在陣列中緊密排列(Tightly Packed)
  • 資料中第一個值在緩衝開始的位置

然後可以使用glVertexAttribPointer函式,用於在OpenGL中解析頂點資料。

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer函式引數介紹:

  • 第一個引數:指定要配置的頂點屬性,在上文使用了layout(location = 0)定義了Position頂點屬性的位置值,因此引數值設定為0。
  • 第二個引數指定頂點屬性的大小。頂點屬性型別是vec3,因此大小是3。
  • 第三個引數指定資料的型別,這裡是GL_FLOAT(GLSL中vec*都是浮點型別)
  • 第四個引數用於指定是否希望資料被標準化(Normalize),若設定為GL_TRUE,則所有資料會被對映到0-1之間(對於有符號的signed資料是-1至1之間),這裡設定為GL_FALSE。
  • 第五個引數是步長(Stride),即連續的頂點屬性組之間的間隔。下組頂點陣列在3個float之後,因此步長設定為3*sizeof(float)。也可以設定為0,讓OpenGL決定具體步長(只有當數值緊密排列時才能使用)。
  • 最後一個引數的型別是void*,它表示未知資料在緩衝中起始位置的偏移量。由於位置在陣列的開頭,因此設定為0。

每個頂點屬性從一個VBO管理的記憶體中獲得其資料,在呼叫glVertexAttribPointer之前繫結預先定義的VBO物件,頂點屬性0會連結到它的頂點資料。

定義了OpenGL如何解釋頂點資料後,然後使用glEnableVertexAttribArray啟用頂點屬性(預設禁用),即在OpenGL中繪製一個物體,程式碼示例如下:

// 0. 複製頂點陣列到緩衝中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 設定頂點屬性指標
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 當我們渲染一個物體時要使用著色器程式
glUseProgram(shaderProgram);
// 3. 繪製物體
someOpenGLFunctionThatDrawsOurTriangle();

所有物體繪製都必須重複上述過程,必然會造成繁瑣的步驟,因此可以使用頂點資料物件進行優化。

8. 頂點陣列物件

頂點陣列物件(Vertex Array Object,VAO)會將頂點屬性的呼叫儲存。當配置頂點屬性指標時,只需要將那些呼叫執行一次,之後再繪製物體的時候只需要繫結相應的VAO就可以了。這使得不同頂點資料和屬性配置之間的切換非常簡單,值需要繫結不同的VAO,VAO中儲存了上文設定的所有狀態。
一個VAO會儲存:

  • glEnableVertexAttribArrayglDisplayVertexAttribArray的呼叫
  • 通過glVertexAttribPointer設定的頂點屬性配置
  • 通過glVertexAttribPointer呼叫與頂點屬性關聯的頂點緩衝物件


建立VAO與VBO類似:

unsigned int VAO;
glGenVertexArrays(1, &VAO);

使用glBindVertexArray繫結VAO之後才能使用。

// ..:: 初始化程式碼(只執行一次 (除非你的物體頻繁改變)) :: ..
// 1. 繫結VAO
glBindVertexArray(VAO);
// 2. 把頂點陣列複製到緩衝中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 設定頂點屬性指標
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 繪製程式碼(渲染迴圈中) :: ..
// 4. 繪製物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

當繪製多個物體時,首先需要生成和配置所有VAO,在繪製物體過程中繫結相應的VAO,繪製完成後再解綁。

9. 索引緩衝物件

當需要繪製一個矩形時,可以通過繪製兩個三角形來組成一個矩形(OpenGL主要處理三角形),則頂點集合如下:

float vertices[] = {
    // 第一個三角形
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角
    // 第二個三角形
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

雖然矩陣只需要4個頂點,但上文頂點集合儲存了6個頂點,造成了資源的浪費。更好的解決方案是隻儲存不同的頂點,並設定這些頂點的繪製順序,即索引緩衝物件(Element Buffer Object,EBO)

EBO專門儲存索引,OpenGL呼叫這些頂點的索引來決定該繪製哪個點,即索引繪製(Index Drawing)。即首先定義不重複的頂點和繪製索引:

float vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = { // 注意索引從0開始! 
    0, 1, 3, // 第一個三角形
    1, 2, 3  // 第二個三角形
};

上文只定義了4個頂點,然後需要建立索引緩衝物件:

unsigned int EBO;
glGenBuffers(1, &EBO);

然後繫結EBO,並把索引複製到緩衝裡,型別設定為GL_ELEMENT_ARRAY_BUFFER

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

最後使用glDrawElements替換glDrawArrays函式,即從索引緩衝渲染。第一個引數指定了繪製的模式;第二個引數指定了繪製頂點的數量;第三個引數是索引的型別,這裡是GL_UNSIGNED_INT;最後一個引數指定EBO中的偏移量。

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawElements函式從當前繫結到GL_ELEMENT_ARRAY_BUFFER目標的EBO中獲取索引。即在每次使用索引渲染時需要繫結相應的EBO,不過頂點陣列物件同樣可以儲存索引緩衝物件的繫結狀態。VAO繫結時正在繫結的索引緩衝物件會被儲存為VAO的元素緩衝物件,繫結VAO時會自定繫結EBO。


當目標是GL_ELEMENT_ARRAY_BUFFER時,VAO會儲存glBindBuffer的函式呼叫,即也會儲存解綁呼叫,所以確保不能在解綁VAO之前解綁EBO,否則會沒有EBO的配置了。

最後的繪製程式碼如下:

// ..:: 初始化程式碼 :: ..
// 1. 繫結頂點陣列物件
glBindVertexArray(VAO);
// 2. 把我們的頂點陣列複製到一個頂點緩衝中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 複製我們的索引陣列到一個索引緩衝中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 設定頂點屬性指標
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 繪製程式碼(渲染迴圈中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

本文轉自 https://blog.csdn.net/weixin_45782925/article/details/124822990,如有侵權,請聯絡刪除。