在上一节中我们成功配置了assimp库,在使用assimp库倒导入模型时,它通常会将整个模型加载到一个场景scene中,这个场景scene中包含了所有需要的模型数据。同时,assimp会将场景在入为一系列的节点Node,在每个节点中存储真实模型数据数据的索引,每个节点又可以有任意数量的子节点。
assimp数据结构如下:
在scene中的mMeshes[]数组存储了真正的mesh对象,而节点Node中的mMeshes[]存储的是场景中网格数组索引,可以通过遍历索引访问mesh对象。一个Mesh对象本身又包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面和物体的材质。其中所谓面,就是物体的渲染图元(三角形、方形等),而顶点与索引的关系,是可以使用索引缓冲来描绘,即mIndices[]。
此外还有一个material对象,包含了物体的材质属性以及纹理贴图等。
网格
想要将模型加载到程序中,我们需要将模型所转化的assimp数据结构进行转换,转换成openGL能处理的格式。其中Mesh代表的是单个可绘制的实体。
而一个网格至少需要一系列的顶点,每个顶点都包含一个位置向量、一个法向量、一个纹理坐标向量。还应包含用于索引绘制的索引以及纹理形式的材质数据。因此我们我们在openGL中定义我们的网格类。
首先定义一个顶点结构体,将所需向量都包含进去。
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
其次需要一个纹理结构体,存储纹理的id以及纹理类型(漫反射、镜面反射),并设置一个path,用来存储已加载纹理的路径。
struct Texture {
unsigned int id;
string type;
string path;
};
有了顶点和纹理,接下来定义网格类
在构造器中进行一些初始化的opengl设置,并设置函数Draw来实现绘制。
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();
};
在构造器Mesh(...)中加载数据然后存入网格中,之后进行初始化。
Mesh(vector<Vertex> vertices, vector<unsignedint> 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);
}
其中可以看到,我们可以方便的通过vertex结构体的一些特性来设置参数。
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
自然sizeof运算也可以用在结构体上来计算它的字节大小。这个应该是32字节的(8个float * 每个4字节)。
结构体的另外一个很好的用途是它的预处理指令offsetof(s, m),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)。这正好可以用在定义glVertexAttribPointer函数中的偏移参数:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
渲染
我们需要为Mesh类定义最后一个函数,它的Draw函数。在真正渲染这个网格之前,我们需要在调用glDrawElements函数之前先绑定相应的纹理。然而,这实际上有些困难,我们一开始并不知道这个网格(如果有的话)有多少纹理、纹理是什么类型的。所以我们该如何在着色器中设置纹理单元和采样器呢?
为了解决这个问题,我们需要设定一个命名标准:每个漫反射纹理被命名为texture_diffuseN,每个镜面光纹理应该被命名为texture_specularN,其中N的范围是1到纹理采样器最大允许的数字。比如说我们对某一个网格有3个漫反射纹理,2个镜面光纹理,它们的纹理采样器应该之后会被调用:
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
根据这个标准,我们可以在着色器中定义任意需要数量的纹理采样器,如果一个网格真的包含了(这么多)纹理,我们也能知道它们的名字是什么。根据这个标准,我们也能在一个网格中处理任意数量的纹理,开发者也可以自由选择需要使用的数量,他只需要定义正确的采样器就可以了(虽然定义少的话会有点浪费绑定和uniform调用)。
渲染实现
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);
}
glActiveTexture(GL_TEXTURE0);
// 绘制网格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
我们首先计算了每个纹理类型的N-分量,并将其拼接到纹理类型字符串上,来获取对应的uniform名称。接下来我们查找对应的采样器,将它的位置值设置为当前激活的纹理单元,并绑定纹理。这也是我们在Draw函数中需要着色器的原因。我们也将"material."添加到了最终的uniform名称中,因为我们希望将纹理储存在一个材质结构体中(这在每个实现中可能都不同)。