OpenGL之旅(3):繪製一個三角形

title: OpenGL(3)三角形
date: 2020-06-28 16:01
category: 圖形學
tags: opengl
https://hashwaney.github.io/

1.概述

  • VAO: 頂點數組對象 Vertex Array Object
  • VBO: 頂點緩衝對象 Vertex Buffer Object
  • IBO(EBO): 索引緩衝對象 Element Buffer Object or Index Buffer Object

在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關於把3D座標轉換爲適應屏幕的2D像素。

3D座標轉爲2D座標的處理過程是由OpenGL的圖形渲染管線管理的;

上述意思:3D座標可以看成一堆的原始圖形數據經過一個輸送管道,在這個過程中經過座標變換,着色等等最終呈現在屏幕上。

  • 將3D座標轉換爲2D座標。

  • 把2D座標轉換爲實際的有顏色的像素。

流程:

頂點數據
------>
頂點着色器(1)
------>
圖元裝配(2)
------>
幾何着色器(3)
------>
光柵化(4)
------>
片段着色器(5)
------>
測試與混合(6)

以三角形爲例:

  1. 以數組的形式傳遞3個3D座標作爲圖形渲染管線的輸入來表示一個三角形,這個數據叫做頂點數據(vertex data);那麼頂點着色器
    是把3D座標轉換爲另一種3D座標(輸入的3D座標不是標準的設備座標,即不是在OpenGl的可見區域)

  2. 利用頂點着色器輸出的所有頂點作爲輸入,把所有的點裝配成指定圖元的形狀

  3. 將圖元裝配階段的輸出傳遞給幾何着色器,幾何着色器把圖元形式的一系列頂點的集合作爲輸入,通過產生新頂點構造新的圖元來生成其他形狀

  4. 幾何着色器的輸出被傳入到光柵化階段,將圖元映射爲最終屏幕上相應的像素,生成供片段着色器使用的片段,還要進行相應的裁切處理,裁切部分超出視圖以外的所有像素。其實就用來生成片段,以便片段着色器對座標點進行着色

  5. 用來計算一個像素的最終顏色,其中還包括3D場景數據(光照、陰影、光的顏色),這些數據可以用來計算最終像素的顏色

  6. 在所有對應的顏色值確定之後,最終的對象將會被傳到最後一個階段,Alpha測試和混合測試,這個階段檢查片段對應的深度,用來判斷這個像素是在其他物體的前面還是後面,決定是否應該丟棄,這個階段也會檢查alpha值(透明度)並對物體進行混合。

2.頂點輸入

  • 繪製圖形之前,需要給OpenGL輸入一些頂點數據,由於OpenGL是一個3D圖形庫,因此OpenGL指定的所有的座標點都是3D座標(x,y,z)
    注意:OpenGL不是簡單的把所有的3D座標變換爲屏幕上的2D像素,OpenGL僅當3D座標在3個軸(x,y和z)上都爲-1.0和1.0的範圍內纔會處理它。(歸一化設備座標)渲染一個三角形,指定三個頂點,每個頂點有一個3D位置;
float vertices[]={
	0.5f,0.5f,0.0f, //(x,y,z)
	-0.5f,-0.5f,0.0f,
	-0.5f,0.5f,0.0f
}

z爲0,由於渲染的是一個2D三角形,因此z座標設置爲0,z座標可以理解爲深度(depth),代表一個像素在空間中與你的距離。

  • 定義好了這樣的頂點數據之後,將數據發送到圖形渲染管線的第一個處理階段:頂點着色器。

    • 在GPU上創建內存用於儲存頂點數據
    • 配置OpenGL如何解釋解釋這些內存,指定如何將頂點着色器如何發送給顯卡
    • 頂點着色器處理在內存中指定數量的頂點。
  • **頂點緩衝對象(VBO)**管理在GPU上創建的內存,在內存中存儲大量的頂點。

    • 好處:一次性發送一大批數據到顯卡上,原因是從CPU把數據發送到顯卡相對較慢(針對每次發送一個頂點而言)

    • 是一個OpenGL對象,有一個獨一無二的ID,使用glGenBuffers函數和一個緩衝ID生成一個VBO對象

unsigned int VBO;

glGenBuffers(1,&VBO);

頂點緩衝對象的緩衝類型爲GL_ARRAY_BUFFER,OpenGL允許同時綁定多個緩衝,只要是不同的緩衝類型。使用glBindBuffer函數把新創建的緩衝綁定到GL_ARRAY_BUFFER;

unsigned int VBO;

glBindBuffer(GL_ARRAY_BUFFER,VBO); // 綁定的是一個頂點緩衝對象類型爲GL_ARRAY_BUFFER

glBindBuffer(GL_COPY_READ_BUFFER,VBO);

//2.生成一個VBO對象
glGenBuffers(1,&VBO);

爲這個VBO對象綁定多種緩衝類型 VBO對象配置一些關於緩衝類型的信息 glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer(GL_COPY_READ_BUFFER, VBO);

以上兩步只要配置了,那麼使用任何在這種緩衝類型的緩衝調用都會用來配置到當前綁定的VBO上

其實就是隻要是這兩種或者還配置其他緩衝類型的數據,只要這些緩衝類型有數據,VBO對象都是使用這些數據。
最後調用glGenBuffers來生成一個VBO對象.

要想使用這些數據,就需要把用戶輸入的數據存儲到對應的緩存類型的緩衝內存中,只需調用glBufferData函數,就可以把之前定義好的頂點數據複製到緩衝內存中

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

//glBufferData(GL_COPY_READ_BUFFER,sizeof(vetices),vetices,GL_STATIC_DRAW);?? 

上述參數解釋:

  1. 目標緩衝類型:頂點緩衝對象當前綁定到GL_ARRAY_BUFFER目標上。

  2. 指定傳輸數據的大小(以字節爲單位),用sizeof計算出頂點數據大小。

  3. 發送的實際數據(當前來說就是頂點數組)

  4. 顯卡如何管理給定的數據

渲染保持原樣

GL_STATIC_DRAW:數據不會或幾乎不會改變

數據頻繁被改變,確保顯卡把數據放在能夠高速寫入的內存部分

GL_DYNAMIC_DRAW:數據會改變很多

GL_STREAM_DRAW:數據每次繪製是都會改變。

綜上:頂點數據存儲在顯卡的內存中,用VBO這個頂點緩衝對象管理。

3.頂點着色器

頂點着色器(Vertex Shader)是一種可編程的着色器的一種。

需要通過着色器語言GLSL(OpenGL Shading Language)編寫頂點着色器,然後編譯這個着色器,最後使用。看以下示例:

#version 330 core //1

layout (layout=0) in vec3 aPos; //2

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

第一行:版本 以及聲明 使用的是核心模式

第二行in 關鍵字 表示的是輸入頂點屬性(Input Vertex Attribute),vec3 代表的是一個3D座標。

第三行:vec4:表示GLSL的一個向量數據類型爲4分量的float類型,即(x,y,z,w)就是爲了歸一化處理,處理的原始3D座標歸一化之後能夠被OpenGL處理。

彩蛋

向量(Vector)表達的是任意空間中的位置和方向,上述的vec3和vec4.
gl_Position 內置的變量不能被寫錯和改變,唯一被OpenGL識別。通過gl_Position將位置數據傳遞給OpenGL。

4.編譯着色器

編寫一個頂點着色器源碼,需要進行動態編譯才能被OpenGL使用。

面向對象的思想:

  • 創建一個着色器對象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER); //每次調用肯定是長生不同的id,如何搞得,可能就根據時間戳。

傳遞的參數:GL_VERTEX_SHADER,

  • 將編寫好的着色器源碼與着色器對象進行綁定
const char* vertexShaderSource ;
int vertexShader;
glShaderSource(vertexShader,1,&vertexShaderSource,NULL);

vertexShaderSource :代表的是編寫的頂點着色器的源碼以字符串的形式提供(這個意味着各個平臺通用)

  • 編譯這個着色器對象
int vertexShader;

glCompileShader(vertexShader);

最後進行編譯頂點着色器

  • 編譯結果
int success;
char info[1024];
glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success);
if (!success) // 失敗
{
	glGetShaderInfoLog(vertexShader,1024,NULL,info);
	std::cout<<" shader compile error "<<info<<std::endl;
}

5.片段着色器

爲了計算第四步中的頂點所處位置的顏色,並且將這些顏色進行輸出
片段着色器(Fragment Shader)用於渲染三角形;

計算機圖形中顏色表示爲4個元素的數組:紅色,綠色,藍色和aplha(透明度)分量RGBA,在OpenGL或者GLSL定義一個顏色的時候,顏色分量會在0.0-1.0之間。

#version 330 core // 1

out ver4 FragColor ; // 2

void mian(){
	FragColor = vec4(1.0f,0.5f,0.3f,1.0f);
}

第一步
如上分析可得;

第二步

out 關鍵字聲明爲輸出變量,命名爲FragColor,輸出的是一個vec4的顏色組合。

片段着色器的創建與編譯過程與上述的頂點着色器相似,此處不在做具體分析,給出示例代碼

unsigned int fragShader;

const char* fragShaderSource;

fragShader = glCreateShader(GL_FRAGMENT_SHADER);

glShaderSource(fragShader,1,&fragShaderSource,NULL);

glCompileShader(fragShader);

着色器程序
上述兩個着色器完成了編譯,但是還沒有鏈接在一起,所以發揮不了作用,因此將這兩個着色器對象鏈接到一個渲染的着色器程序中。
好處:將着色器渲染功能封裝到一個程序中,當有了渲染需求的時候就激活這個程序,已經被激活的着色器程序的着色器就會被調用,進行工作,你把着色器鏈接到程序中,就把每個着色器的輸鏈接到下一個着色器的輸入。(頂點輸出—>需要進行着色—>片元着色器接收(輸入)–>上色)

流程:

usigned int shaderProgram;

shaderProgram = glCreateProgram();
//0.
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragShader);

//1.
glLinkProgram(shaderProgram);

int success;
char info[1024];
glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success);

if (!success)
{
	glGetProgramInfoLog(shaderProgram,1024,NULL,info);
	std::cout<<"link program error "<<info <<std::endl;
	shaderProgram=-1;
}
if (shaderProgram!-1)
{
	//2.使用着色器程序
	glUseProgram(shaderProgram);
}



//3. 將着色器對象鏈接到着色器程序,要刪除着色器對象
glDeleteShader(vertexShader);
glDeleteShader(fragShader);

上述流程是將輸入的頂點數據發送給
GPU,並且告訴GPU如何在頂點和片段着色器中處理它,但是OpenGL不知道如何解釋內存中的頂點數據(輸入的頂點數據),以及該如何將頂點數據鏈接到頂點着色器的屬性上,看下面的6.鏈接頂點屬性

6.鏈接頂點屬性

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KWONZX0d-1593486438103)(https://i.loli.net/2020/06/30/iH7TycXs8d2RknY.png)]

  • 位置數據被存儲爲32位(4字節)比如說VerTex 1 這個頂點的X分量

  • 每個頂點位置包含3個這樣的值,比如VerTex 1 的X,Y,Z

  • 在這3個值之間沒有空隙(獲取其他值),這幾個值在數組中緊密排列

  • 數據中的第一個值在緩衝開始的位置

通過上述的圖以及表述,可以得知OpenGL按照這種數據排列規律來解析並處理頂點數據,通過glVertexAttribPointer函數


//1. 解釋頂點數據
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(viod*)0);
//2.將頂點數據鏈接到頂點着色器的屬性
glEnableVertexAttribArray(0);

  • 第一個參數指定配置的頂點屬性,由3.頂點着色器中的編寫的glsl代碼中 layout(location=0)定義了position頂點屬性的位置location,可以將頂點屬性的位置設置爲0,那麼數據傳遞到這個頂點屬性中。

  • 第二個參數指定頂點屬性的大小,頂點屬性是一個vec3,由3個值組成,所以其大小是3。

  • 第三個參數指定數據的類型,這裏是GL_FLOAT(GLSL中的vec* 都是由浮點數值組成)

  • 第四個參數指定是否需要數據標準化,設置GL_TRUE,所有數據都會被映射到0(對於有符號signed數據是-1)到1之間,這邊不採用。

  • 第五個參數叫做步長(stride),告訴連續的頂點屬性之間的間隔,由於下個組位置數據(即VerTex 2 是經歷VerTex 1的三個float之後),因此步長爲3*sizeof(float).

  • 第六個參數類型爲void*,表示緩衝中起始位置的偏移量,由於位置數據在數組的開頭,所以這裏爲0。

使用glEnableVertexAttribArray(0)啓用(意味着可以鏈接)頂點屬性,(默認是禁用的)。即頂點數據可以鏈接到頂點屬性

繪製物體流程:


// 複製頂點數組到緩衝
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBindData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

// 設置頂點屬性
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0); // layout location(0) 往前面翻一翻

// 渲染一個物體需要使用着色器程序
glUseProgram(shaderProgram);

// 繪製物體
drawSomeOne();


頂點數組對象(Vertex Array Object,VAO)

用來管理一系列的頂點數據和存儲頂點屬性的對象,這些頂點數據保存在緩存對象中,並且由當前綁定的頂點數組對象進行管理。
頂點屬性對應相關的頂點數據。

創建VAO

unsigned int VAO;

glGenVertexArrays(1,&VAO);

使用VAO,需要進行綁定,綁定之後,還需要綁定對應的VBO

//1. 綁定VAO
glBindVertexArray(VAO);
//2.把頂點數組複製到緩衝
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);

//4.使用着色器程序進行繪製
glUseProgram(shaderProgram);

glfwSwapBuffers(window);



//5.解綁VAO VBO
glBindVertexArray(0);
glBindBuffer(0);

7.索引緩衝對象

索引緩衝對象(Element Buffer Object EBO,
Index Buffer Object IBO)

例子:假設我們繪製的是一個矩形而不是一個三角形,通過
繪製兩個三角形來組成一個矩形(OpenGL主要處理三角形)


float vertices[] ={

// 1、第一個三角形
	0.5f,0.5f,0.0f, // 右上角
	0.5f,-0.5f,0.0f,//右下角
	-0.5f,0.5f,0.0f,//左上角
//2、第二個三角形
	0.5f,-0.5f,0.0f,//右下角
	-0.5f,-0.5f,0.0f,//左下角
	-0.5f,0.5f,0.0f//左上角
}	     

由上述可知,對角線的右下角和左上角指定了兩次,
一個矩陣只有4個而不是6個頂點,這樣產生了50%的額外開銷。

解決方案
只存儲4個頂點,並且指定繪製的順序即可,利用索引緩衝對象。

EBO和頂點緩衝一樣,但是其專門存儲索引,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//左上角
};

unsigned int indices[] ={
	0,1,3, // first triangle
	1,2,3 //  second triangle

}

//創建索引緩衝對象
unsigned int EBO;
glGenBuffers(1,&EBO);

//綁定EBO然後用glBufferData把索引複製到緩衝裏,把這些函數調用
//放在綁定與解綁之間(TODO??),緩衝類型爲GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,
	GL_STATIC_DRAW);


//注意上面傳遞的是GL_ELEMENT_ARRAY_BUFFER當作緩衝目標 而不是GL_ARRAY_BUFFER,因此在繪製過程中調用的是glDrawElements而不是
//glDrawArrays()
//1.繪製的圖形
//2.繪製頂點的個數,共需要繪製6個頂點
//3.索引類型
//4.EBO偏移量
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章