[OpenGL入门教程] 5. 统一进行类封装

上一篇博客中,已经将着色器部分的代码抽象成了类,这使得主程序部分精简了许多,并且也更加便于管理和改动。这里,进一步将剩余的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