1. 流程
1.1 定义
完成了assimp的配置,接下来就进入到模型环节了,首先是网格(Mesh),它代表的是单个的可绘制实体,我们现在先来定义一个我们自己的网格类(根据不同情况可以有不同定义,这里只给出一种示例)。
1.1.1 顶点数据结构
一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。
既然我们有了一个网格类的最低需求,我们可以在OpenGL中定义一个顶点了,我们将所有需要的向量储存到一个叫做Vertex的结构体中,我们可以用它来索引每个顶点属性。:
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
除了Vertex结构体之外,我们还需要将纹理数据整理到一个Texture结构体中(我们储存了纹理的id以及它的类型,比如是漫反射贴图或者是镜面光贴图)。
1.1.2 纹理数据结构
struct Texture {
unsigned int id;
string type;
};
1.1.3 网格类
知道了顶点和纹理的实现,我们可以开始定义网格类的结构了:
class Mesh {
public:
/* 网格数据 */
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
/* 函数 */
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);
void Draw(Shader shader);
private:
/* 渲染数据 */
unsigned int VAO, VBO, EBO;
/* 函数 */
void setupMesh();
};
我们在setupMesh函数中初始化缓冲,并最终使用Draw函数来绘制网格。注意我们将一个着色器传入了Draw函数中,将着色器传入网格类中可以让我们在绘制之前设置一些uniform(像是链接采样器到纹理单元)。
setupMesh函数:
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();
}
由于有了构造器,我们现在有一大列的网格数据用于渲染。在此之前我们还必须配置正确的缓冲,并通过顶点属性指针定义顶点着色器的布局。现在你应该对这些概念都很熟悉了,但我们这次会稍微有一点变动,使用结构体中的顶点数据:
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);
// 顶点位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// 顶点法线
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// 顶点纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
需要注意的是,在绑定顶点属性的时候我们使用了:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
其中offsetof是结构体的预定义宏,第一个参数是结构体,第二个参数是结构体成员变量,这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset),这样我们就可以更加直观的定义偏移量了
1.2 渲染
完成了数据的定义,接下来就是利用这些数据来进行渲染,基本的流程也与之前类似,只是在纹理采样器的设置上会出现一些问题,比如怎么在不知道当前网格有多少纹理的情况下去设置它的纹理单元?
这里给出一种命名格式:texture_diffuseN和texture_specularN,分别代表第N个漫反射纹理和镜面高光纹理
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); // 在绑定之前激活相应的纹理单元
// 获取纹理序号(diffuse_textureN 中的 N)
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++);
shader.setInt(("material." + name + number).c_str(), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
// 绘制网格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glActiveTexture(GL_TEXTURE0);//恢复默认设置是个好习惯
}
如上,我们可以利用string类为纹理变量按序命名,并将其拼接到纹理类型字符串上,来获取对应的uniform名称。接下来我们查找对应的采样器,将它的位置值设置为当前激活的纹理单元,并绑定纹理。