正常来说,在OpenGL中,指定一块顶点缓存的布局(假设5种属性)
glBindBuffer(GL_ARRAY_BUFFER, QuadVertexBufferID);
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 法线属性
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
// 纹理坐标属性
glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(9 * sizeof(float)));
glEnableVertexAttribArray(3);
// 切线向量属性
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(11 * sizeof(float)));
glEnableVertexAttribArray(4);
非常简单,口算就能算出来,但是当顶点缓存的布局类型变多之后,属性数量变复杂后,就会非常冗杂、可读性差、可维护性差
如果能够这样指定一个顶点缓存的布局,两个字“优雅”
QuadVertexBuffer->SetLayout({
{ ShaderDataType::Float3, "a_Position" },
{ ShaderDataType::Float4, "a_Color" },
{ ShaderDataType::Float2, "a_TexCoord" },
{ ShaderDataType::Float, "a_TexIndex" },
{ ShaderDataType::Float, "a_TilingFactor" },
{ ShaderDataType::Int, "a_EntityID" }
});
如何实现?
- 首先是一个
OpenGLVertexBuffer
类,继承自一个抽象类VertexBuffer
省略不重要接口函数和构造析构代码,只看本文相关的内容很简单,就是维护一个缓存布局对象及其Set Get
方法class OpenGLVertexBuffer : public VertexBuffer { public: // ....... 构造、析构函数以及其他接口函数 virtual const BufferLayout& GetLayout() const override { return m_Layout; } virtual void SetLayout(const BufferLayout& layout) override { m_Layout = layout; } private: BufferLayout m_Layout; };
- 然后是缓存布局类
BufferLayout
要了解一个顶点缓存的布局,只需要指明2点:属性数量
、每个属性的尺寸
,其他的都可以计算出来-
构造函数中,形参为
std::initializer_list
- 为了支持任意数量的参数,需要在形参这里提供一个容器作为形参,一般正常来说都直接用std::vector,但是这个容器比较"重",因为它支持增删改查,动态扩容等特性
initializer_list
C++11引入的,它更轻量级,在这里充当桥梁,用于在初始化时传递一系列的值给类内的std::vector
成员
-
构造函数内部调用函数,计算了每个属性的偏移量(
offset
),以及顶点的步长(m_Stride
)
对应glVertexAttribPointer()函数的最后两个参数
// 针对每个顶点而言的。有多少个属性,其名字、数据类型、尺寸、偏移量 class BufferLayout { public: BufferLayout() {} // BufferElement代表顶点属性 BufferLayout(std::initializer_list<BufferElement> elements) : m_Elements(elements) { CalculateOffsetsAndStride(); } // 允许在本类BufferLayout的实例上使用迭代器,使用时,bufferLayout对象就相当于一个容器 // 实际上返回的是m_Elements的迭代器 std::vector<BufferElement>::iterator begin() { return m_Elements.begin(); } std::vector<BufferElement>::iterator end() { return m_Elements.end(); } std::vector<BufferElement>::const_iterator begin() const { return m_Elements.begin(); } std::vector<BufferElement>::const_iterator end() const { return m_Elements.end(); } private: void CalculateOffsetsAndStride() { size_t offset = 0; m_Stride = 0; for (auto& element : m_Elements) { element.Offset = offset; // 顶点属性0的起始位置,到该属性的起始位置的偏移量 offset += element.Size; m_Stride += element.Size; // 步长为所有属性的size之和 } } private: std::vector<BufferElement> m_Elements; uint32_t m_Stride = 0; };
-
Element
结构体,对应于顶点的一个属性enum class ShaderDataType { None = 0, Float, Float2, Float3, Float4, Mat3, Mat4, Int, Int2, Int3, Int4, Bool }; static uint32_t ShaderDataTypeSize(ShaderDataType type) { // 判断ShaderDataType种类,返回它的尺寸 } struct BufferElement { std::string Name; ShaderDataType Type; uint32_t Size; size_t Offset; BufferElement() = default; BufferElement(ShaderDataType type, const std::string& name, bool normalized = false) : Name(name), Type(type), Size(ShaderDataTypeSize(type)), Offset(0), Normalized(normalized) { } };
- 最后在适当的地方,给程序中的每一个vertexBuffer的每个属性调用
glVertexAttribPointer()
,可以选择把ConfigureVertexAttribPointers
这个函数设计成顶点缓存类的一个成员函数,也可以直接合到SetLayout()函数内,主要看程序员如何设计自己的代码了// utils static GLenum ShaderDataTypeToOpenGLBaseType(ShaderDataType type); void ConfigureVertexAttribPointers(const std::shared_ptr<VertexBuffer>& vertexBuffer) { vertexBuffer->Bind(); const auto& layout = vertexBuffer->GetLayout(); // 可以用范围循直接遍历layout对象,因为该类实现了迭代器begin() end()方法 uint32_t vertexBufferIndex = 0; for (const auto& element : layout) { switch (element.Type) { case ShaderDataType::Float: case ShaderDataType::Float2: case ShaderDataType::Float3: case ShaderDataType::Float4: { glEnableVertexAttribArray(vertexBufferIndex); glVertexAttribPointer(vertexBufferIndex, // slot element.GetComponentCount(), // 分量个数 ShaderDataTypeToOpenGLBaseType(element.Type), // 分量内部格式 element.Normalized ? GL_TRUE : GL_FALSE, layout.GetStride(), // 等于顶点结构体大小,即本属性距离下个顶点的同个属性的距离 (const void*)element.Offset); vertexBufferIndex++; break; } case ShaderDataType::Int: case ShaderDataType::Int2: case ShaderDataType::Int3: case ShaderDataType::Int4: case ShaderDataType::Bool: { glEnableVertexAttribArray(vertexBufferIndex); glVertexAttribIPointer(vertexBufferIndex, element.GetComponentCount(), ShaderDataTypeToOpenGLBaseType(element.Type), layout.GetStride(), (const void*)element.Offset); vertexBufferIndex++; break; } case ShaderDataType::Mat3: case ShaderDataType::Mat4: { uint8_t count = element.GetComponentCount(); for (uint8_t i = 0; i < count; i++) { glEnableVertexAttribArray(vertexBufferIndex); glVertexAttribPointer(vertexBufferIndex, count, ShaderDataTypeToOpenGLBaseType(element.Type), element.Normalized ? GL_TRUE : GL_FALSE, layout.GetStride(), (const void*)(element.Offset + sizeof(float) * count * i)); glVertexAttribDivisor(vertexBufferIndex, 1); vertexBufferIndex++; } break; } default: LOG(false, "Unknown ShaderDataType!"); } } } static GLenum ShaderDataTypeToOpenGLBaseType(ShaderDataType type) { switch (type) { case ShaderDataType::Float: return GL_FLOAT; case ShaderDataType::Float2: return GL_FLOAT; case ShaderDataType::Float3: return GL_FLOAT; case ShaderDataType::Float4: return GL_FLOAT; case ShaderDataType::Mat3: return GL_FLOAT; case ShaderDataType::Mat4: return GL_FLOAT; case ShaderDataType::Int: return GL_INT; case ShaderDataType::Int2: return GL_INT; case ShaderDataType::Int3: return GL_INT; case ShaderDataType::Int4: return GL_INT; case ShaderDataType::Bool: return GL_BOOL; } LOG(false, "Unknown ShaderDataType!"); return 0; }