Добрый день, хабра юзеры. Я давно не писал и возможно кто-то заждался статей от меня — конечно же нет. Так как свободного времени стало чутка поболее, а мой GitHub совершенно пуст, я решил написать свой клон Mein kampf Minecraft. С большой вероятностью, я задокументирую это — следите за моими статьями на habr.com. Сегодня покажу как я обернул OpenGL примитивы в RAII стиле, если интересно — под кат.

Пишите в комментариях, о чем бы вы хотели почитать. Стоит ли писать статьи «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).

Спасибо тем, кто дочитал это до конца. Очень буду рад критике и комментариям.