版本記錄
| 版本號(hào) | 時(shí)間 |
|---|---|
| V1.0 | 2017.12.31 |
前言
OpenGL 圖形庫(kù)項(xiàng)目中一直也沒用過,最近也想學(xué)著使用這個(gè)圖形庫(kù),感覺還是很有意思,也就自然想著好好的總結(jié)一下,希望對(duì)大家能有所幫助。下面內(nèi)容來(lái)自歡迎來(lái)到OpenGL的世界。
1. OpenGL 圖形庫(kù)使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫(kù)使用(二) —— 渲染模式、對(duì)象、擴(kuò)展和狀態(tài)機(jī)
3. OpenGL 圖形庫(kù)使用(三) —— 著色器、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫(kù)使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫(kù)使用(五) —— 紋理
6. OpenGL 圖形庫(kù)使用(六) —— 變換
7. OpenGL 圖形庫(kù)的使用(七)—— 坐標(biāo)系統(tǒng)之五種不同的坐標(biāo)系統(tǒng)(一)
8. OpenGL 圖形庫(kù)的使用(八)—— 坐標(biāo)系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫(kù)的使用(九)—— 攝像機(jī)(一)
10. OpenGL 圖形庫(kù)的使用(十)—— 攝像機(jī)(二)
11. OpenGL 圖形庫(kù)的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫(kù)的使用(十二)—— 光照之基礎(chǔ)光照
13. OpenGL 圖形庫(kù)的使用(十三)—— 光照之材質(zhì)
14. OpenGL 圖形庫(kù)的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫(kù)的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫(kù)的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫(kù)的使用(十七)—— 光照之復(fù)習(xí)總結(jié)
18. OpenGL 圖形庫(kù)的使用(十八)—— 模型加載之Assimp
網(wǎng)格
通過使用Assimp,我們可以加載不同的模型到程序中,但是載入后它們都被儲(chǔ)存為Assimp的數(shù)據(jù)結(jié)構(gòu)。我們最終仍要將這些數(shù)據(jù)轉(zhuǎn)換為OpenGL能夠理解的格式,這樣才能渲染這個(gè)物體。我們從上一節(jié)中學(xué)到,網(wǎng)格(Mesh)代表的是單個(gè)的可繪制實(shí)體,我們現(xiàn)在先來(lái)定義一個(gè)我們自己的網(wǎng)格類。
首先我們來(lái)回顧一下我們目前學(xué)到的知識(shí),想想一個(gè)網(wǎng)格最少需要什么數(shù)據(jù)。一個(gè)網(wǎng)格應(yīng)該至少需要一系列的頂點(diǎn),每個(gè)頂點(diǎn)包含一個(gè)位置向量、一個(gè)法向量和一個(gè)紋理坐標(biāo)向量。一個(gè)網(wǎng)格還應(yīng)該包含用于索引繪制的索引以及紋理形式的材質(zhì)數(shù)據(jù)(漫反射/鏡面光貼圖)。
既然我們有了一個(gè)網(wǎng)格類的最低需求,我們可以在OpenGL中定義一個(gè)頂點(diǎn)了:
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
我們將所有需要的向量?jī)?chǔ)存到一個(gè)叫做Vertex的結(jié)構(gòu)體中,我們可以用它來(lái)索引每個(gè)頂點(diǎn)屬性。除了Vertex結(jié)構(gòu)體之外,我們還需要將紋理數(shù)據(jù)整理到一個(gè)Texture結(jié)構(gòu)體中。
struct Texture {
unsigned int id;
string type;
};
我們儲(chǔ)存了紋理的id以及它的類型,比如是漫反射貼圖或者是鏡面光貼圖。
知道了頂點(diǎn)和紋理的實(shí)現(xiàn),我們可以開始定義網(wǎng)格類的結(jié)構(gòu)了:
class Mesh {
public:
/* 網(wǎng)格數(shù)據(jù) */
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
/* 函數(shù) */
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);
void Draw(Shader shader);
private:
/* 渲染數(shù)據(jù) */
unsigned int VAO, VBO, EBO;
/* 函數(shù) */
void setupMesh();
};
你可以看到這個(gè)類并不復(fù)雜。在構(gòu)造器中,我們將所有必須的數(shù)據(jù)賦予了網(wǎng)格,我們?cè)?code>setupMesh函數(shù)中初始化緩沖,并最終使用Draw函數(shù)來(lái)繪制網(wǎng)格。注意我們將一個(gè)著色器傳入了Draw函數(shù)中,將著色器傳入網(wǎng)格類中可以讓我們?cè)诶L制之前設(shè)置一些uniform(像是鏈接采樣器到紋理單元)。
構(gòu)造器的內(nèi)容非常易于理解。我們只需要使用構(gòu)造器的參數(shù)設(shè)置類的公有變量就可以了。我們?cè)跇?gòu)造器中還調(diào)用了setupMesh函數(shù):
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();
}
這里沒什么可說(shuō)的。我們接下來(lái)討論setupMesh函數(shù)。
初始化
由于有了構(gòu)造器,我們現(xiàn)在有一大列的網(wǎng)格數(shù)據(jù)用于渲染。在此之前我們還必須配置正確的緩沖,并通過頂點(diǎn)屬性指針定義頂點(diǎn)著色器的布局。現(xiàn)在你應(yīng)該對(duì)這些概念都很熟悉了,但我們這次會(huì)稍微有一點(diǎn)變動(dòng),使用結(jié)構(gòu)體中的頂點(diǎn)數(shù)據(jù):
void setupMesh()
{
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
&indices[0], GL_STATIC_DRAW);
// 頂點(diǎn)位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// 頂點(diǎn)法線
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// 頂點(diǎn)紋理坐標(biāo)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
代碼應(yīng)該和你所想得沒什么不同,但有了Vertex結(jié)構(gòu)體的幫助,我們使用了一些小技巧。
C++結(jié)構(gòu)體有一個(gè)很棒的特性,它們的內(nèi)存布局是連續(xù)的(Sequential)。也就是說(shuō),如果我們將結(jié)構(gòu)體作為一個(gè)數(shù)據(jù)數(shù)組使用,那么它將會(huì)以順序排列結(jié)構(gòu)體的變量,這將會(huì)直接轉(zhuǎn)換為我們?cè)跀?shù)組緩沖中所需要的float(實(shí)際上是字節(jié))數(shù)組。比如說(shuō),如果我們有一個(gè)填充后的Vertex結(jié)構(gòu)體,那么它的內(nèi)存布局將會(huì)等于:
Vertex vertex;
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f);
vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
由于有了這個(gè)有用的特性,我們能夠直接傳入一大列的Vertex結(jié)構(gòu)體的指針作為緩沖的數(shù)據(jù),它們將會(huì)完美地轉(zhuǎn)換為glBufferData所能用的參數(shù):
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
自然sizeof運(yùn)算也可以用在結(jié)構(gòu)體上來(lái)計(jì)算它的字節(jié)大小。這個(gè)應(yīng)該是32字節(jié)的(8個(gè)float * 每個(gè)4字節(jié))。
結(jié)構(gòu)體的另外一個(gè)很好的用途是它的預(yù)處理指令offsetof(s, m),它的第一個(gè)參數(shù)是一個(gè)結(jié)構(gòu)體,第二個(gè)參數(shù)是這個(gè)結(jié)構(gòu)體中變量的名字。這個(gè)宏會(huì)返回那個(gè)變量距結(jié)構(gòu)體頭部的字節(jié)偏移量(Byte Offset)。這正好可以用在定義glVertexAttribPointer函數(shù)中的偏移參數(shù):
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
偏移量現(xiàn)在是使用offsetof來(lái)定義了,在這里它會(huì)將法向量的字節(jié)偏移量設(shè)置為結(jié)構(gòu)體中法向量的偏移量,也就是3個(gè)float,即12字節(jié)。注意,我們同樣將步長(zhǎng)參數(shù)設(shè)置為了Vertex結(jié)構(gòu)體的大小。
使用這樣的一個(gè)結(jié)構(gòu)體不僅能夠提供可讀性更高的代碼,也允許我們很容易地拓展這個(gè)結(jié)構(gòu)。如果我們希望添加另一個(gè)頂點(diǎn)屬性,我們只需要將它添加到結(jié)構(gòu)體中就可以了。由于它的靈活性,渲染的代碼不會(huì)被破壞。
渲染
我們需要為Mesh類定義最后一個(gè)函數(shù),它的Draw函數(shù)。在真正渲染這個(gè)網(wǎng)格之前,我們需要在調(diào)用glDrawElements函數(shù)之前先綁定相應(yīng)的紋理。然而,這實(shí)際上有些困難,我們一開始并不知道這個(gè)網(wǎng)格(如果有的話)有多少紋理、紋理是什么類型的。所以我們?cè)撊绾卧谥髦性O(shè)置紋理單元和采樣器呢?
為了解決這個(gè)問題,我們需要設(shè)定一個(gè)命名標(biāo)準(zhǔn):每個(gè)漫反射紋理被命名為texture_diffuseN,每個(gè)鏡面光紋理應(yīng)該被命名為texture_specularN,其中N的范圍是1到紋理采樣器最大允許的數(shù)字。比如說(shuō)我們對(duì)某一個(gè)網(wǎng)格有3個(gè)漫反射紋理,2個(gè)鏡面光紋理,它們的紋理采樣器應(yīng)該之后會(huì)被調(diào)用:
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
根據(jù)這個(gè)標(biāo)準(zhǔn),我們可以在著色器中定義任意需要數(shù)量的紋理采樣器,如果一個(gè)網(wǎng)格真的包含了(這么多)紋理,我們也能知道它們的名字是什么。根據(jù)這個(gè)標(biāo)準(zhǔn),我們也能在一個(gè)網(wǎng)格中處理任意數(shù)量的紋理,開發(fā)者也可以自由選擇需要使用的數(shù)量,他只需要定義正確的采樣器就可以了(雖然定義少的話會(huì)有點(diǎn)浪費(fèi)綁定和uniform調(diào)用)。
像這樣的問題有很多種不同的解決方案。如果你不喜歡這個(gè)解決方案,你可以自己想一個(gè)你自己的解決辦法。
最終的渲染代碼是這樣的:
void Draw(Shader shader)
{
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
for(unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // 在綁定之前激活相應(yīng)的紋理單元
// 獲取紋理序號(hào)(diffuse_textureN 中的 N)
stringstream ss;
string number;
string name = textures[i].type;
if(name == "texture_diffuse")
ss << diffuseNr++; // 將 unsigned int 插入到流中
else if(name == "texture_specular")
ss << specularNr++; // 將 unsigned int 插入到流中
number = ss.str();
shader.setFloat(("material." + name + number).c_str(), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
glActiveTexture(GL_TEXTURE0);
// 繪制網(wǎng)格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
這并不是最漂亮的代碼,但這部分要?dú)w咎于C++轉(zhuǎn)換int到string類型時(shí)太丑了。我們首先計(jì)算了每個(gè)紋理類型的N-分量,并將其拼接到紋理類型字符串上,來(lái)獲取對(duì)應(yīng)的uniform名稱。接下來(lái)我們查找對(duì)應(yīng)的采樣器,將它的位置值設(shè)置為當(dāng)前激活的紋理單元,并綁定紋理。這也是我們?cè)贒raw函數(shù)中需要著色器的原因。我們也將"material."添加到了最終的uniform名稱中,因?yàn)槲覀兿M麑⒓y理儲(chǔ)存在一個(gè)材質(zhì)結(jié)構(gòu)體中(這在每個(gè)實(shí)現(xiàn)中可能都不同)。
注意我們?cè)趯⒙瓷溆?jì)數(shù)器和鏡面光計(jì)數(shù)器插入
stringstream時(shí),對(duì)它們進(jìn)行了遞增。在C++中,這個(gè)遞增操作:variable++將會(huì)返回變量本身,之后再遞增,而++variable則是先遞增,再返回值。在我們的例子中是首先將原本的計(jì)數(shù)器值插入stringstream,之后再遞增它,供下一次循環(huán)使用。
你可以在這里找到Mesh類的完整源代碼
#ifndef MESH_H
#define MESH_H
#include <glad/glad.h> // holds all OpenGL type declarations
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <learnopengl/shader.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>
using namespace std;
struct Vertex {
// position
glm::vec3 Position;
// normal
glm::vec3 Normal;
// texCoords
glm::vec2 TexCoords;
// tangent
glm::vec3 Tangent;
// bitangent
glm::vec3 Bitangent;
};
struct Texture {
unsigned int id;
string type;
string path;
};
class Mesh {
public:
/* Mesh Data */
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
unsigned int VAO;
/* Functions */
// constructor
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
// now that we have all the required data, set the vertex buffers and its attribute pointers.
setupMesh();
}
// render the mesh
void Draw(Shader shader)
{
// bind appropriate textures
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
unsigned int normalNr = 1;
unsigned int heightNr = 1;
for(unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding
// retrieve texture number (the N in diffuse_textureN)
string number;
string name = textures[i].type;
if(name == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if(name == "texture_specular")
number = std::to_string(specularNr++); // transfer unsigned int to stream
else if(name == "texture_normal")
number = std::to_string(normalNr++); // transfer unsigned int to stream
else if(name == "texture_height")
number = std::to_string(heightNr++); // transfer unsigned int to stream
// now set the sampler to the correct texture unit
glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);
// and finally bind the texture
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
// draw mesh
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// always good practice to set everything back to defaults once configured.
glActiveTexture(GL_TEXTURE0);
}
private:
/* Render data */
unsigned int VBO, EBO;
/* Functions */
// initializes all the buffer objects/arrays
void setupMesh()
{
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// A great thing about structs is that their memory layout is sequential for all its items.
// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which
// again translates to 3/2 floats which translates to a byte array.
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// vertex texture coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
// vertex tangent
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
// vertex bitangent
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
glBindVertexArray(0);
}
};
#endif
我們剛定義的Mesh類是我們之前討論的很多話題的抽象結(jié)果。在下一節(jié)中,我們將創(chuàng)建一個(gè)模型,作為多個(gè)網(wǎng)格對(duì)象的容器,并真正地實(shí)現(xiàn)Assimp的加載接口。
后記
未完,待續(xù)~~~
