上一篇博客中,已经将着色器部分的代码抽象成了类,这使得主程序部分精简了许多,并且也更加便于管理和改动。这里,进一步将剩余的OpenGL的代码进行抽象,全部封装成类。
顶点缓冲区类
顶点缓冲区的类设计很简单,就是单纯的创建缓冲区和绑定。下面直接展示代码:
// VertexBuff.h
#pragma once
class VertexBuff
{
private:
unsigned int m_RendererID;
public:
VertexBuff(const void* data, unsigned int size);
~VertexBuff();
void Bind() const;
void Unbind() const;
};
// VertexBuff.cpp
#include "VertexBuff.h"
#include "renderer.h"
VertexBuff::VertexBuff(const void* data, unsigned int size):m_RendererID(0)
{
GLCall(glGenBuffers(1, &m_RendererID));
GLCall(glBindBuffer(GL_ARRAY_BUFFER, m_RendererID));
GLCall(glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW));
}
VertexBuff::~VertexBuff()
{
GLCall(glDeleteBuffers(1, &m_RendererID));
}
void VertexBuff::Bind() const
{
GLCall(glBindBuffer(GL_ARRAY_BUFFER, m_RendererID));
}
void VertexBuff::Unbind() const
{
GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
}
在构造函数中,调用之前主函数的构建代码来创建缓冲区,析构时自动删除,从而避免了我们的手动操作。
索引缓冲区类
索引缓冲区类的设计与顶点缓冲区差不多,唯一的区别是顶点缓冲区直接传入顶点数据的大小,而索引缓冲区传入的是索引的个数。
// IndexBuff.h
#pragma once
#include <iostream>
class IndexBuff
{
private:
unsigned int m_RendererID;
unsigned int m_Count;
public:
IndexBuff(const unsigned int* data, unsigned int count);
~IndexBuff();
void Bind() const;
void Unbind() const;
unsigned int GetCount() const{
return m_Count;
}
};
// IndexBuff.cpp
#include "IndexBuff.h"
#include "renderer.h"
IndexBuff::IndexBuff(const unsigned int* data, unsigned int count):m_Count(count), m_RendererID(0)
{
ASSERT(sizeof(GLuint) == sizeof(unsigned int));
GLCall(glGenBuffers(1, &m_RendererID));
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID));
GLCall(glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, GL_STATIC_DRAW));
}
IndexBuff::~IndexBuff()
{
GLCall(glDeleteBuffers(1, &m_RendererID));
}
void IndexBuff::Bind() const
{
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID));
}
void IndexBuff::Unbind() const
{
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
}
顶点布局类
考虑到顶点数组类型必须指定其布局才能使用,因此,在创建顶点数组类之前,先创建一个顶点布局类用于指定布局。
这个类比较简单,出于方便考虑,直接将定义写在了头文件中。
#pragma once
#include <vector>
#include "renderer.h"
#include <GL/glew.h>
struct VertexBuffElement
{
unsigned int type;
unsigned int count;
unsigned char normalized;
VertexBuffElement(unsigned int type,unsigned int count, unsigned char normalized):type(type),count(count),normalized(normalized){}
static unsigned int GetSizeOfType(unsigned int type) {
switch (type)
{
case GL_FLOAT: return 4;
case GL_UNSIGNED_INT: return 4;
case GL_UNSIGNED_BYTE: return 1;
}
ASSERT(false);
return 0;
}
};
class VertexBuffLayout {
private:
std::vector<VertexBuffElement> m_Elements;
unsigned int m_Stride;
public:
VertexBuffLayout() :m_Stride(0){};
template<typename T>
void push(unsigned int count)
{
//static_assert(true,"1");
}
template<>
void push<float>(unsigned int count)
{
m_Elements.push_back({ GL_FLOAT,count,GL_FALSE });
m_Stride += VertexBuffElement::GetSizeOfType(GL_FLOAT) * count;
}
template<>
void push<unsigned int>(unsigned int count)
{
m_Elements.push_back({ GL_UNSIGNED_INT,count,GL_FALSE });
m_Stride += VertexBuffElement::GetSizeOfType(GL_UNSIGNED_INT) * count;
}
template<>
void push<unsigned char>(unsigned int count)
{
m_Elements.push_back({ GL_UNSIGNED_BYTE,count,GL_TRUE });
m_Stride += VertexBuffElement::GetSizeOfType(GL_UNSIGNED_BYTE) * count;
}
unsigned int GetStride() const {
return m_Stride;
}
const std::vector<VertexBuffElement>& GetElements() const {
return m_Elements;
}
};
这个头文件中,首先创建了VertexBuffElement
类用于表示顶点属性,主要包括顶点属性的类型,元素个数以及是否进行归一化。
接着创建布局类,布局类中用一个vector
储存所有的顶点属性,并使用一个变量记录步长。
然后定义了插入布局的方法,为了统一地使用这种方法,将其定义为了函数模板,并为其实例化。每次添加属性时,同时累加步长。
为了使用方便,还定义了接口对外返回步长和属性。
顶点数组对象类
顶点数组对象主要负责构建一个顶点数组对象,并提供设置新的属性的功能即可。
// VertexArray.h
#pragma once
#include "VertexBuff.h"
class VertexBuffLayout;
class VertexArray{
private:
unsigned int m_RendererID;
public:
VertexArray();
~VertexArray();
void AddBuffer(const VertexBuff& vb, const VertexBuffLayout& layout);
void Bind() const;
void Unbind() const;
};
// VertexArray.cpp
#include "VertexArray.h"
#include "VertexBuffLayout.h"
#include "renderer.h"
VertexArray::VertexArray()
{
GLCall(glGenVertexArrays(1, &m_RendererID));
}
VertexArray::~VertexArray()
{
GLCall(glDeleteVertexArrays(1, &m_RendererID));
}
void VertexArray::AddBuffer(const VertexBuff& vb, const VertexBuffLayout& layout)
{
Bind();
vb.Bind();
const auto& elements = layout.GetElements();
unsigned int offset = 0;
for (unsigned int i = 0; i < elements.size(); ++i) {
const auto& e = elements[i];
GLCall(glEnableVertexAttribArray(i));
GLCall(glVertexAttribPointer(i, e.count, e.type, e.normalized , layout.GetStride(), (const void *)offset));
offset += e.count * VertexBuffElement::GetSizeOfType(e.type);
}
}
void VertexArray::Bind() const
{
GLCall(glBindVertexArray(m_RendererID));
}
void VertexArray::Unbind() const
{
GLCall(glBindVertexArray(0));
}
在AddBuffer
中,依次取出布局中设置好的属性,设置属性指针并启用,然后附加对应偏移量,避免了手工执行的繁琐,也更不容易出错。
其余部分与主程序中一致,直接拿过来用即可。
渲染器类
最后,将这些所有执行部分抽象成一个渲染器,它负责完成所有的OpenGL工作,渲染出对应图形,从而将整个过程封装起来。
// renderer.h
#pragma once
#include <GL/glew.h>
#include "VertexArray.h"
#include "IndexBuff.h"
#include "Shader.h"
#define ASSERT(x) if(!(x)) __debugbreak();
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLPrintError(#x,__FILE__,__LINE__))
void GLClearError();
bool GLPrintError(const char* func_name, const char* file, int line);
class Renderer {
public:
void draw(const VertexArray& va, const IndexBuff& ib, const Shader &shader) const;
void clear() const;
};
// renderer.cpp
#include "renderer.h"
#include <iostream>
void GLClearError() {
while (glGetError() != GL_NO_ERROR); //GL_NO_ERROR = 0
}
bool GLPrintError(const char* func_name, const char* file, int line) {
while (GLenum error = glGetError()) { //因为 GL_NO_ERROR = 0, 故循环可以写成这个形式
std::cout << "[OPENGL Error] (" << error << "): " << func_name << " " << file << ":" << line << std::endl;
return false;
}
return true;
}
void Renderer::draw(const VertexArray& va, const IndexBuff& ib, const Shader& shader) const
{
shader.Bind();
va.Bind();
ib.Bind();
GLCall(glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr));
}
void Renderer::clear() const
{
glClearColor(0.1f, 0.2f, 0.3f, 1.0f);
GLCall(glClear(GL_COLOR_BUFFER_BIT));
}
这里渲染器目前只负责调用前面构造好的类,绑定好并执行绘画,以及负责清除上次的画面。
到这,已经将所有的OpenGL代码统一封装成类了,后续使用只需要调用这些封装好的类即可。当然,后续随着功能的增多,这些类也还会扩展,但是大体结构是这样的。
这样抽象以后,目前主程序的代码已经非常精简了。
参考资料
[1] https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/03%20Hello%20Window/
[2] http://bit.ly/2lt7ccM