顶点数组对象( VAO )是一种特殊类型对象,它封装了与顶点处理器有关的所有数据,它仅仅是记录顶点缓存区和索引缓冲区的引用,以及顶点的各种属性的布局而不是实际的数据。这样做的好处是一旦你为一个 mesh 设置一个 VAO ,你就可以通过简单的绑定 VAO 来导入 mesh 的所有状态。之后你就可以直接渲染 mesh 对象而不需要担心它的状态,VAO 为你记住了它。如果你的程序需要对顶点属性布局不同的 mesh 进行处理,VAO 同样可以帮忙,你只需要确保在创建 VAO 的时候为其设置了正确的布局即可,之后就完全不用管它了,因为这些信息都已经被保存在了 VAO 中。
当你正确使用时,VAo 也可以代表一种对 GPU 驱动的优化,如果 VAO 被创建了一次,并且在之后被多次使用,由于驱动程序知道了顶点缓存与索引缓存之间的映射关系以及缓存中顶点属性的布局,所以就可以对其进行一些优化。很显然,这都取决于你使用的驱动程序,而且不同的驱动其优化方式也不能保证完全一样。但是不管怎么说, 复用 VAO 总是最好的选择。
在这一课中,我们基于上面所讲的 VAO 对 Mesh类进行更新。除此之外,我们将使用 SOA(存放数组的结构体)方法组织缓冲区中的顶点数据。现在为止我们的顶点数据都是用一个包含各种顶点属性(如位置属性等)的结构体表示的,顶点缓存中包含了一个又一个的顶点结构体,缓存中的这种数据保存方式称作 AOS(存放结构体的数组)。 SOA 是这种模式的一个简单变换,这种模式下缓存中存放的是一个包含了多个数组的结构体,每个数组中都只存放顶点的某一个属性,为了得到某一个顶点的所有属性,GPU 会使用这个顶点的索引从每个数组中读取数据,这个方式对某些 3D 模型文件来说效率会更高一些,而且对于同样的问题,了解一些不同的解决方法也是很有趣的。
下面的图片介绍了 AOS 和 SOA:
class Mesh |
{ |
public: |
Mesh(); |
~Mesh(); |
bool LoadMesh(const std::string& Filename); |
void Render(); |
private: |
bool InitFromScene(const aiScene* pScene, const std::string& Filename); |
void InitMesh(const aiMesh* paiMesh, |
std::vector& Positions, |
std::vector& Normals, |
std::vector& TexCoords, |
std::vector& Indices); |
bool InitMaterials(const aiScene* pScene, const std::string& Filename); |
void Clear(); |
#define INVALID_MATERIAL 0xFFFFFFFF |
#define INDEX_BUFFER 0 |
#define POS_VB 1 |
#define NORMAL_VB 2 |
#define TEXCOORD_VB 3 |
GLuint m_VAO; |
GLuint m_Buffers[4]; |
struct MeshEntry { |
MeshEntry() |
{ |
NumIndices = 0; |
BaseVertex = 0; |
BaseIndex = 0; |
MaterialIndex = INVALID_MATERIAL; |
} |
unsigned int BaseVertex; |
unsigned int BaseIndex; |
unsigned int NumIndices; |
unsigned int MaterialIndex; |
}; |
std::vector m_Entries; |
std::vector m_Textures; |
}; |
bool Mesh::LoadMesh(const string& Filename) |
{ |
// Release the previously loaded mesh (if it exists) |
Clear(); |
// Create the VAO |
glGenVertexArrays(1, &m_VAO); |
glBindVertexArray(m_VAO); |
// Create the buffers for the vertices atttributes |
glGenBuffers(ARRAY_SIZE_IN_ELEMENTS(m_Buffers), m_Buffers); |
bool Ret = false; |
Assimp::Importer Importer; |
const aiScene* pScene = Importer.ReadFile(Filename.c_str(), aiProcess_Triangulate | |
aiProcess_GenSmoothNormals | aiProcess_FlipUVs); |
if (pScene) { |
Ret = InitFromScene(pScene, Filename); |
} |
else { |
printf("Error parsing '%s': '%s'\n", Filename.c_str(), Importer.GetErrorString()); |
} |
// Make sure the VAO is not changed from outside code |
glBindVertexArray(0); |
return Ret; |
} |
bool Mesh::InitFromScene(const aiScene* pScene, const string& Filename) |
{ |
m_Entries.resize(pScene->mNumMeshes); |
m_Textures.resize(pScene->mNumMaterials); |
// Prepare vectors for vertex attributes and indices |
vector Positions; |
vector Normals; |
vector TexCoords; |
vector Indices; |
unsigned int NumVertices = 0; |
unsigned int NumIndices = 0; |
// Count the number of vertices and indices |
for (unsigned int i = 0 ; i < m_Entries.size() ; i++) { |
m_Entries[i].MaterialIndex = pScene->mMeshes[i]->mMaterialIndex; |
m_Entries[i].NumIndices = pScene->mMeshes[i]->mNumFaces * 3; |
m_Entries[i].BaseVertex = NumVertices; |
m_Entries[i].BaseIndex = NumIndices; |
NumVertices += pScene->mMeshes[i]->mNumVertices; |
NumIndices += m_Entries[i].BaseIndex; |
} |
// Reserve space in the vectors for the vertex attributes and indices |
Positions.reserve(NumVertices); |
Normals.reserve(NumVertices); |
TexCoords.reserve(NumVertices); |
Indices.reserve(NumIndices); |
// Initialize the meshes in the scene one by one |
for (unsigned int i = 0 ; i < m_Entries.size() ; i++) { |
const aiMesh* paiMesh = pScene->mMeshes[i]; |
InitMesh(paiMesh, Positions, Normals, TexCoords, Indices); |
} |
if (!InitMaterials(pScene, Filename)) { |
return false; |
} |
// Generate and populate the buffers with vertex attributes and the indices |
glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[POS_VB]); |
glBufferData(GL_ARRAY_BUFFER, sizeof(Positions[0]) * Positions.size(), &Positions[0], |
GL_STATIC_DRAW); |
glEnableVertexAttribArray(0); |
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); |
glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[TEXCOORD_VB]); |
glBufferData(GL_ARRAY_BUFFER, sizeof(TexCoords[0]) * TexCoords.size(), &TexCoords[0], |
GL_STATIC_DRAW); |
glEnableVertexAttribArray(1); |
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); |
glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[NORMAL_VB]); |
glBufferData(GL_ARRAY_BUFFER, sizeof(Normals[0]) * Normals.size(), &Normals[0], |
GL_STATIC_DRAW); |
glEnableVertexAttribArray(2); |
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Buffers[INDEX_BUFFER]); |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices[0]) * Indices.size(), &Indices[0], |
GL_STATIC_DRAW); |
return true; |
} |
这个函数的最后一部分才是真正有趣的地方,位置缓存、法线向量缓存、纹理坐标缓存都被一个接一个的绑定到 GL_ARRAY_BUFFER 目标上,对这个目标的所有操作都会影响到当前被绑定的缓存,而且就算之后其他缓存绑定到了这个目标上,之前对那个缓存的改变依然会有效。对于这三个缓存,我们都进行了如下工作:
索引缓存是通过将其绑定到 GL_ELEMENT_ARRAY_BUFFER 目标上来进行初始化的,我们只需要使用索引数据填充它即可。现在所有的缓存都已经初始化了,而且他们都被封装在了 VAO 中。
void Mesh::InitMesh(const aiMesh* paiMesh, |
vector& Positions, |
vector& Normals, |
vector& TexCoords, |
vector& Indices) |
{ |
const aiVector3D Zero3D(0.0f, 0.0f, 0.0f); |
// Populate the vertex attribute vectors |
for (unsigned int i = 0 ; i < paiMesh->mNumVertices ; i++) { |
const aiVector3D* pPos = &(paiMesh->mVertices[i]); |
const aiVector3D* pNormal = &(paiMesh->mNormals[i]); |
const aiVector3D* pTexCoord = paiMesh->HasTextureCoords(0) ? |
&(paiMesh->mTextureCoords[0][i]) : &Zero3D; |
Positions.push_back(Vector3f(pPos->x, pPos->y, pPos->z)); |
Normals.push_back(Vector3f(pNormal->x, pNormal->y, pNormal->z)); |
TexCoords.push_back(Vector2f(pTexCoord->x, pTexCoord->y)); |
} |
// Populate the index buffer |
for (unsigned int i = 0 ; i < paiMesh->mNumFaces ; i++) { |
const aiFace& Face = paiMesh->mFaces[i]; |
assert(Face.mNumIndices == 3); |
Indices.push_back(Face.mIndices[0]); |
Indices.push_back(Face.mIndices[1]); |
Indices.push_back(Face.mIndices[2]); |
} |
} |
void Mesh::Render() |
{ |
glBindVertexArray(m_VAO); |
for (unsigned int i = 0 ; i < m_Entries.size() ; i++) { |
const unsigned int MaterialIndex = m_Entries[i].MaterialIndex; |
assert(MaterialIndex < m_Textures.size()); |
if (m_Textures[MaterialIndex]) { |
m_Textures[MaterialIndex]->Bind(GL_TEXTURE0); |
} |
glDrawElementsBaseVertex(GL_TRIANGLES, |
m_Entries[i].NumIndices, |
GL_UNSIGNED_INT, |
(void*)(sizeof(unsigned int) * m_Entries[i].BaseIndex), |
m_Entries[i].BaseVertex); |
} |
// Make sure the VAO is not changed from the outside |
glBindVertexArray(0); |
} |
glDeleteVertexArrays(1, &m_VAO); |