Добрый день, хабра юзеры. Я давно не писал и возможно кто-то заждался статей от меня — конечно же нет. Так как свободного времени стало чутка поболее, а мой GitHub совершенно пуст, я решил написать свой клон Mein kampf Minecraft. С большой вероятностью, я задокументирую это — следите за моими статьями на habr.com. Сегодня покажу как я обернул OpenGL примитивы в RAII стиле, если интересно — под кат.
Пишите в комментариях, о чем бы вы хотели почитать. Стоит ли писать статьи «Minecraft from scratch»? А теперь приступим. Я буду показывать на примере Buffer Object. Первое, что мы сделаем — вынесем основные вызовы ( glGenBuffers , glDeleteBuffers ) в отдельный класс.
Где-то вот так:
Плюсы:
Минусы:
Что б решит проблему с копированием, мы просто его запретим. Для этого создадим класс noncopyable и унаследуем его.
Пример класса noncopyable:
Отлично, минус одна проблема. Думаем дальше, как бы нам решить проблему с биндингом… добавим функции bind/unbind:
buffer_type — enum class, но можно сделать extern const (в *.hpp), а в (*.cpp) сделать что-то типа:
Опять же, что бы наружу не торчал
Плюсы:
Минусы:
Так, почти все супер, но мы еще не можем создавать несколько буферов… Исправим же это.
glGenBuffers может принимать указатель на массив и количество буферов для генерации.
Нам нужен массив, мы могли бы использовать std::vector, но нам надо аллоцировать память всего один раз и я бы предпочел тут std::array, хотя в дальнейшем нам прийдется из-за этого делать еще один уровень абстракции.
Перепишем наш класс на std::array и добавим чуточку шаблонов:
И вот, что мы получили.
Плюсы:
Минусы:
Ну… минусов многовато, что тут можно сделать?
Убрать включение GL/glew.h, можно добавив еще один уровень абстракции, в котором будут вызываться функции OpenGL (в любом случае это нужно делать, если планируется поддержка OpenGL + DirectX). С биндингом разных буфером чуть посложнее, так как мы можем забыть какой индекс с каким буфером был забинджен, как вариант — добавить еще один массив и в него записывать тип буфера. Я пока не занимался этим, на данном этапе разработки мне хватает и этого.
Для более удобного использования, я сделал scoped bind класс. Вот он:
У некоторых может вызвать недоумение вот эта строка:
Если мы не укажем ключевое слова template, то словим такую ошибку:
binder.hpp:22:46: Missing 'template' keyword prior to dependent template name 'bind'
Это означает, что в таком случае, когда T является зависимым типом. Компилятор еще не знает, что такое тип m_ref. Так как связывания еще не было, компилятор обрабатывает ее чисто синтаксически, поэтому < интерпретируется как оператор меньше чем. Чтобы указать компилятору, что это, по сути, вызов специализации шаблона функции-члена, необходимо добавить ключевое слово template сразу после оператор точки.
И пример использования:
В данном коде не используется объект для Vertex Array, потому что он не работает, причины я еще не выяснил, но скоро с этим разберусь xD
Так же еще не готовы обертки для Buffer Data и OpenGL вызовы не переписаны на DSA (Direct State Access).
Спасибо тем, кто дочитал это до конца. Очень буду рад критике и комментариям.
Пишите в комментариях, о чем бы вы хотели почитать. Стоит ли писать статьи «Minecraft from scratch»? А теперь приступим. Я буду показывать на примере Buffer Object. Первое, что мы сделаем — вынесем основные вызовы ( glGenBuffers , glDeleteBuffers ) в отдельный класс.
Где-то вот так:
class buffer_object
{
public:
buffer_object() noexcept {
glGenBuffers(1, m_object);
}
~buffer_object() noexcept {
glDeleteBuffers(1, m_object);
}
uint32_t get_object() const noexcept {
return m_object;
}
private:
uint32_t m_object;
};
Плюсы:
- Не нужно следить за удалением буфера
- Включение OpenGL хедера не торчит наружу (если мы перенесем определение функций в *.cpp)
Минусы:
- Не можем указать тип буфера (array, element ...) при биндинге
- Не можем создать несколько буферов одним вызовом glGenBuffers
- Проблемы с обращением к удаленному объекту, если буфер был скопирован
Что б решит проблему с копированием, мы просто его запретим. Для этого создадим класс noncopyable и унаследуем его.
Пример класса noncopyable:
struct noncopyable
{
noncopyable() = default;
noncopyable(noncopyable&&) = default;
noncopyable& operator = (noncopyable&&) = default;
noncopyable(const noncopyable&) = delete;
noncopyable& operator = (const noncopyable&) = delete;
};
Отлично, минус одна проблема. Думаем дальше, как бы нам решить проблему с биндингом… добавим функции bind/unbind:
void bind(buffer_type type) noexcept {
glBindBuffer(static_cast<GLenum>(type), m_object);
}
void unbind() noexcept {
glBindBuffer(static_cast<GLenum>(type), 0);
}
buffer_type — enum class, но можно сделать extern const (в *.hpp), а в (*.cpp) сделать что-то типа:
const uint32_t array_buffer = GL_ARRAY_BUFFER;
Опять же, что бы наружу не торчал
#include <GL/glew.h>
Плюсы:
- Не нужно следить за удалением буфера
- Включение OpenGL хедера не торчит наружу (если мы перенесем определение функций в *.cpp)
- Можем указать тип буфера(array, element ...) при биндинге
- Нет копирования — нет проблем с копированием
Минусы:
- Не можем создать несколько буферов одним вызовом glGenBuffers
Так, почти все супер, но мы еще не можем создавать несколько буферов… Исправим же это.
glGenBuffers может принимать указатель на массив и количество буферов для генерации.
Нам нужен массив, мы могли бы использовать std::vector, но нам надо аллоцировать память всего один раз и я бы предпочел тут std::array, хотя в дальнейшем нам прийдется из-за этого делать еще один уровень абстракции.
Перепишем наш класс на std::array и добавим чуточку шаблонов:
constexpr uint8_t default_object_count = 1;
constexpr size_t default_index = 0;
template <type::buffer_type type, uint32_t count = default_object_count>
class buffer_object : noncopyable
{
public:
buffer_object() noexcept {
glGenBuffers(count, m_object.data());
}
~buffer_object() noexcept {
glDeleteBuffers(count, m_object.data());
}
template <size_t index = default_index>
void bind() noexcept
{
static_assert(index < count, "index larger than array size");
glBindBuffer(static_cast<GLenum>(type), m_object[index]);
}
void unbind() noexcept {
glBindBuffer(static_cast<GLenum>(type), 0);
}
buffer_object(buffer_object&&) = default;
buffer_object& operator = (buffer_object&&) = default;
private:
std::array<uint32_t, count> m_object;
};
И вот, что мы получили.
Плюсы:
- Не нужно следить за удалением буфера
- Можем указать тип буфера(array, element ...) при биндинге
- Нет копирования — нет проблем с копированием
- Можем создать несколько буферов одним вызовом glGenBuffers
Минусы:
- Не можем биндить разные буферы у одного объекта
- Включение OpenGL хедера торчит наружу
Ну… минусов многовато, что тут можно сделать?
Убрать включение GL/glew.h, можно добавив еще один уровень абстракции, в котором будут вызываться функции OpenGL (в любом случае это нужно делать, если планируется поддержка OpenGL + DirectX). С биндингом разных буфером чуть посложнее, так как мы можем забыть какой индекс с каким буфером был забинджен, как вариант — добавить еще один массив и в него записывать тип буфера. Я пока не занимался этим, на данном этапе разработки мне хватает и этого.
Бонус
Для более удобного использования, я сделал scoped bind класс. Вот он:
constexpr uint32_t default_bind_index = 0;
template <class T>
class binder : utils::noncopyable
{
public:
template <uint32_t index = default_bind_index>
binder(T& ref) : m_ref(ref) { m_ref.template bind<index>(); }
~binder() { m_ref.unbind();}
private:
T& m_ref;
};
У некоторых может вызвать недоумение вот эта строка:
m_ref.template bind<index>();
Если мы не укажем ключевое слова template, то словим такую ошибку:
binder.hpp:22:46: Missing 'template' keyword prior to dependent template name 'bind'
Это означает, что в таком случае, когда T является зависимым типом. Компилятор еще не знает, что такое тип m_ref. Так как связывания еще не было, компилятор обрабатывает ее чисто синтаксически, поэтому < интерпретируется как оператор меньше чем. Чтобы указать компилятору, что это, по сути, вызов специализации шаблона функции-члена, необходимо добавить ключевое слово template сразу после оператор точки.
И пример использования:
GLuint VAO;
primitive::buffer_object<type::buffer_type::array> vbo;
primitive::buffer_object<type::buffer_type::element> ebo;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
{
primitive::binder bind{vbo};
glBufferData(GL_ARRAY_BUFFER, ...);
glVertexAttribPointer(...);
glEnableVertexAttribArray(...);
glVertexAttribPointer(...);
glEnableVertexAttribArray(...);
}
{
primitive::binder bind{ebo};
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ...);
glBindVertexArray(0);
}
В данном коде не используется объект для Vertex Array, потому что он не работает, причины я еще не выяснил, но скоро с этим разберусь xD
Так же еще не готовы обертки для Buffer Data и OpenGL вызовы не переписаны на DSA (Direct State Access).
Спасибо тем, кто дочитал это до конца. Очень буду рад критике и комментариям.
lgorSL
Можно вместо scoped_bind сделать метод, который принимает лямбду, биндит, вызывает лямбду и разбиндивает обратно. Конкретно в С++ это не будет удобнее, но в некоторых других языках может не быть RAII.
kiwhy Автор
Такой функционал я скорее всего добавлю, так как он будет удобен при отрисовке объекта.
Но при переписывании на DSA бинд будет вызываться всего 1 раз (при отрисовке), все настройки происходят без вызова glBindBuffer