1. 程式人生 > >opengl---2.圖形渲染的過程

opengl---2.圖形渲染的過程

一、過程介紹

在OpenGL中,任何事物都在3D空間中,而螢幕和視窗卻是2D畫素陣列,這導致OpenGL的大部分工作都是關於把3D座標轉變為適應你螢幕的2D畫素。3D座標轉為2D座標的處理過程是由OpenGL的圖形渲染管線
圖形渲染管線可以被劃分為兩個主要部分:
第一部分把你的3D座標轉換為2D座標,
第二部分是把2D座標轉變為實際的有顏色的畫素。

圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函式),並且很容易並行執行。正是由於它們具有並行執行的特性,當今大多數顯示卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段執行各自的小程式,從而在圖形渲染管線中快速處理你的資料。這些小程式叫做著色器(Shader)

這裡寫圖片描述

如圖,圖形渲染過程如下:
1.我們以陣列的形式傳遞3個3D座標作為圖形渲染管線的輸入,用來表示一個三角形,這個陣列叫做頂點資料(Vertex Data);頂點資料是一系列頂點的集合。

2.頂點著色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D座標轉為另一種3D座標,同時頂點著色器允許我們對頂點屬性進行一些基本處理。

3.圖元裝配(Primitive Assembly)階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS,那麼就是一個頂點),並所有的點裝配成指定圖元的形狀。

4.幾何著色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。

5.光柵化階段(Rasterization Stage),這裡它會把圖元對映為最終螢幕上相應的畫素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器執行之前會執行裁切(Clipping)。裁切會丟棄超出你的檢視以外的所有畫素,用來提升執行效率 。

6.片段著色器的主要目的是計算一個畫素的最終顏色,這也是所有OpenGL高階效果產生的地方。通常,片段著色器包含3D場景的資料(比如光照、陰影、光的顏色等等),這些資料可以被用來計算最終畫素的顏色。

7.Alpha測試和混合(Blending)階段,檢測片段的對應的深度(和模板(Stencil))值,用它們來判斷這個畫素是其它物體的前面還是後面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend)。所以,即使在片段著色器中計算出來了一個畫素輸出的顏色,在渲染多個三角形的時候最後的畫素顏色也可能完全不同。

二、具體展開:你好,三角形

1.頂點輸入

OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有座標都是3D座標(x、y和z)。OpenGL不是簡單地把所有的3D座標變換為螢幕上的2D畫素;OpenGL僅當3D座標在3個軸(x、y和z)上都為-1.0到1.0的範圍內時才處理它。所有在所謂的標準化裝置座標(Normalized Device Coordinates)範圍內的座標才會最終呈現在螢幕上(在這個範圍以外的座標都不會顯示)。
渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置,將它們以標準化裝置座標的形式(OpenGL的可見區域)定義為一個float陣列。

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

定義這樣的頂點資料以後,我們會把它作為輸入傳送給圖形渲染管線的第一個處理階段:頂點著色器。

2.頂點緩衝

頂點陣列物件:Vertex Array Object,VAO
頂點緩衝物件:Vertex Buffer Object,VBO
索引緩衝物件:Element Buffer Object,EBO或Index Buffer Object,IBO

在定義好頂點資料以後,需要在記憶體中儲存這些頂點,我們通過頂點緩衝物件(Vertex Buffer Objects, VBO)管理這個記憶體,它會在GPU記憶體(通常被稱為視訊記憶體)中儲存大量頂點。使用這些緩衝物件的好處是我們可以一次性的傳送一大批資料到顯示卡上,而不是每個頂點發送一次。從CPU把資料傳送到顯示卡相對較慢,所以只要可能我們都要嘗試儘量一次性發送儘可能多的資料。當資料傳送至顯示卡的記憶體中後,頂點著色器幾乎能立即訪問頂點,這是個非常快的過程。

①生成一個VBO物件:

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

②OpenGL有很多緩衝物件型別,頂點緩衝物件的緩衝型別是GL_ARRAY_BUFFER,通過glBindBuffer函式把新建立的緩衝繫結到GL_ARRAY_BUFFER目標上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);  

③呼叫glBufferData函式,它會把之前定義的頂點資料複製到緩衝的記憶體中:

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

glBufferData是一個專門用來把使用者定義的資料複製到當前繫結緩衝的函式。
它的第一個引數是目標緩衝的型別:頂點緩衝物件當前繫結到GL_ARRAY_BUFFER目標上。
第二個引數指定傳輸資料的大小(以位元組為單位);用一個簡單的sizeof計算出頂點資料大小就行。
第三個引數是我們希望傳送的實際資料。

第四個引數指定了我們希望顯示卡如何管理給定的資料。它有三種形式:

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

3.解析頂點資料

我們必須手動指定輸入資料的哪一個部分對應頂點著色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點資料。
使用glVertexAttribPointer函式告訴OpenGL該如何解析頂點資料(應用到逐個頂點屬性上):

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

glVertexAttribPointer是一個專門用來解析頂點資料的函式。
第一個引數指定我們要配置的頂點屬性。在頂點著色器中使用layout(location = 0)定義了position頂點屬性的位置值(Location),它可以把頂點屬性的位置值設定為0。因為我們希望把資料傳遞到這一個頂點屬性中,所以這裡我們傳入0。
第二個引數指定頂點屬性的大小。頂點屬性是一個vec3,它由3個值組成,所以大小是3。
第三個引數指定資料的型別,這裡是GL_FLOAT。
第四個個引數定義我們是否希望資料被標準化(Normalize)。如果我們設定為GL_TRUE,所有資料都會被對映到0(對於有符號型signed資料是-1)到1之間。我們把它設定為GL_FALSE
第五個引數叫做步長(Stride),它告訴我們在連續的頂點屬性組之間的間隔。由於下個組位置資料在3個float之後,我們把步長設定為3 * sizeof(float)。
最後一個引數的型別是void*,所以需要我們進行這個奇怪的強制型別轉換。它表示位置資料在緩衝中起始位置的偏移量(Offset)。由於位置資料在陣列的開頭,所以這裡是0。

4.頂點陣列物件

頂點陣列物件(Vertex Array Object, VAO)可以像頂點緩衝物件那樣被繫結,任何隨後的頂點屬性呼叫都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指標時,你只需要將那些呼叫執行一次,之後再繪製物體的時候只需要繫結相應的VAO就行了。這使在不同頂點資料和屬性配置之間切換變得非常簡單,只需要繫結不同的VAO就行了。剛剛設定的所有狀態都將儲存在VAO中。

建立VAO:

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

要想使用VAO,要做的只是使用glBindVertexArray繫結VAO。從繫結之後起,我們應該繫結和配置對應的VBO和屬性指標,之後解綁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.解綁VAO,供之後使用
glBindVertexArray(0); 

5.頂點著色器

一個非常基礎的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);
}

為了設定頂點著色器的輸出,我們必須把位置資料賦值給預定義的gl_Position變數,它在幕後是vec4型別的。在main函式的最後,我們將gl_Position設定的值會成為該頂點著色器的輸出。由於我們的輸入是一個3分量的向量,我們必須把它轉換為4分量的。我們可以把vec3的資料作為vec4構造器的引數,同時把w分量設定為1.0f(我們會在後面解釋為什麼)來完成這一任務。

6.片段著色器

片段著色器只需要一個輸出變數,這個變數是一個4分量向量,它表示的是最終的輸出顏色

#version 330 core
out vec4 FragColor;

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

7.編譯著色器

建立著色器物件

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

下一步我們把這個著色器原始碼附加到著色器物件上,然後編譯它

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

glShaderSource函式把要編譯的著色器物件作為第一個引數。第二引數指定了傳遞的原始碼字串數量,這裡只有一個。第三個引數是頂點著色器真正的原始碼,第四個引數我們先設定為NULL。

最後,我們需要檢測編譯是否成功:

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;
}

8.著色器程式

著色器程式物件(Shader Program Object)是多個著色器合併之後並最終連結完成的版本。如果要使用剛才編譯的著色器我們必須把它們連結(Link)為一個著色器程式物件,然後在渲染物件的時候啟用這個著色器程式。已啟用著色器程式的著色器將在我們傳送渲染呼叫的時候被使用。

建立一個程式物件很簡單:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

把之前編譯的著色器附加到程式物件上,然後用glLinkProgram連結它們:

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

檢測連結著色器程式是否失敗,並獲取相應的日誌。

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

得到的結果就是一個程式物件,我們可以呼叫glUseProgram函式,用剛建立的程式物件作為它的引數,以啟用這個程式物件:

glUseProgram(shaderProgram);

9.繪製三角形

要想繪製我們想要的物體,OpenGL給我們提供了glDrawArrays函式,它使用當前啟用的著色器,之前定義的頂點屬性配置,和VBO的頂點資料(通過VAO間接繫結)來繪製圖元。

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays函式第一個引數是我們打算繪製的OpenGL圖元的型別。由於我們在一開始時說過,我們希望繪製的是一個三角形,這裡傳遞GL_TRIANGLES給它。
第二個引數指定了頂點陣列的起始索引,我們這裡填0。
最後一個引數指定我們打算繪製多少個頂點,這裡是3(我們只從我們的資料中渲染一個三角形,它只有3個頂點長)。

三、程式碼展示

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// 6.頂點著色器程式碼
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";

// 7.片段著色器程式碼
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

int main()
{
	// 初始化GLFW
	// 我們將主版本號(Major)和次版本號(Minor)都設為3。
	// 我們同樣明確告訴GLFW我們使用的是核心模式(Core-profile)。
	// ------------------------------
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

	// 建立一個視窗物件
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	// 當用戶改變視窗的大小的時候,視口也應該被調整。
	// 我們可以對視窗註冊一個回撥函式(Callback Function),它會在每次視窗大小被調整的時候被呼叫。
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// 初始化GLAD
	// ---------------------------------------
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	//1.頂點資料
	float vertices[] = {
		-0.5f, -0.5f, 0.0f,
		0.5f, -0.5f, 0.0f,
		0.0f,  0.5f, 0.0f
	};

	//4.頂點陣列物件
	unsigned int VAO;
	glGenVertexArrays(1, &VAO); 
	glBindVertexArray(VAO);

	//2.頂點緩衝
	unsigned int VBO;
	glGenBuffers(1, &VBO);
	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);

	//解綁陣列和緩衝物件
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

	//7.編譯著色器

	//頂點著色器,vertex shader
	int vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
	glCompileShader(vertexShader);
	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;
	}

	//片段著色器,fragment shader
	int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);
	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	//8.著色器程式
	int shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);
	glLinkProgram(shaderProgram);
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);

	// 渲染迴圈
	// -----------
	while (!glfwWindowShouldClose(window))
	{
		// 鍵盤按鍵輸入處理
		processInput(window);

		// 設定清空螢幕所用的顏色
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		// 清空螢幕的顏色緩衝
		glClear(GL_COLOR_BUFFER_BIT);

		//9.繪製三角形
		glUseProgram(shaderProgram);
		glBindVertexArray(VAO); 
		glDrawArrays(GL_TRIANGLES, 0, 3);
		glBindVertexArray(0);

		// 交換顏色緩衝,雙緩衝,應用程式使用單緩衝繪圖時可能會存在影象閃爍的問題。
		glfwSwapBuffers(window);

		//檢查有沒有觸發什麼事件(比如鍵盤輸入、滑鼠移動等)、更新視窗狀態,並呼叫對應的回撥函式
		glfwPollEvents();
	}

	// 清理所有的資源並正確地退出應用程式
	glfwTerminate();
	return 0;
}

void processInput(GLFWwindow *window)
{
	// 按下ESC,關閉視窗
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	//OpenGL渲染視窗的尺寸大小,即視口(Viewport),這樣OpenGL才只能知道怎樣根據視窗大小顯示資料和座標
	glViewport(0, 0, width, height);
}

這裡寫圖片描述