Primitivas do estilo OpenGL RAII

Boa tarde, usuários Habra. Não escrevo há muito tempo e talvez alguém estivesse esperando artigos meus - claro que não. Como meu tempo livre ficou um pouco mais e meu GitHub está completamente vazio, decidi escrever meu clone Mein kampf Minecraft. Com alta probabilidade, vou documentar isso - siga meus artigos no habr.com . Hoje vou mostrar como envolvi as primitivas OpenGL no estilo RAII, se for interessante - sob o gato.

Escreva nos comentários sobre o que você gostaria de ler. Devo escrever artigos do zero do Minecraft? Agora vamos começar. Vou mostrar o Buffer Object como um exemplo . A primeira coisa que faremos é fazer as chamadas principais ( glGenBuffers , glDeleteBuffers ) em uma classe separada.

Em algum lugar como este:

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;
};

Prós:

  • Não há necessidade de monitorar a exclusão do buffer
  • A ativação do cabeçalho OpenGL não se destaca (se transferirmos a definição de funções para * .cpp)

Minuses:

  • Não podemos especificar o tipo de buffer (matriz, elemento ...) ao vincular
  • Não é possível criar vários buffers com uma única chamada para glGenBuffers
  • Problemas ao acessar um objeto remoto se o buffer foi copiado

Para resolver o problema da cópia, simplesmente o proibimos. Para fazer isso, crie uma classe não copiável e a herde.

Exemplo de classe não copiável:

struct noncopyable
{
    noncopyable() = default;
            
    noncopyable(noncopyable&&) = default;
    noncopyable& operator = (noncopyable&&) = default;
            
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator = (const noncopyable&) = delete;
};

Ótimo, menos um problema. Pensamos ainda mais, como resolveríamos o problema com a ligação ... adicione as funções 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 é uma classe enum, mas você pode fazer const externo (em * .hpp) e em (* .cpp) fazer algo como:

const uint32_t array_buffer = GL_ARRAY_BUFFER;

Mais uma vez, isso não se destacaria

#include <GL/glew.h>

Prós:

  • Não há necessidade de monitorar a exclusão do buffer
  • A ativação do cabeçalho OpenGL não se destaca (se transferirmos a definição de funções para * .cpp)
  • Podemos indicar o tipo de buffer (matriz, elemento ...) ao vincular
  • Sem cópia - sem problemas de cópia

Minuses:

  • Não é possível criar vários buffers com uma única chamada para glGenBuffers

Então, quase tudo é super, mas ainda não podemos criar vários buffers ... Vamos corrigi-lo.

O glGenBuffers pode levar um ponteiro para uma matriz e o número de buffers a serem gerados.
Precisamos de uma matriz, poderíamos usar std :: vector, mas precisamos alocar memória apenas uma vez, e eu preferiria std :: array aqui, embora no futuro tenhamos que fazer mais um nível de abstração por causa disso.

Reescrevemos nossa classe para std :: array e adicionamos alguns modelos:

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;
};

E aqui está o que conseguimos.

Prós:

  • Não há necessidade de monitorar a exclusão do buffer
  • Podemos indicar o tipo de buffer (matriz, elemento ...) ao vincular
  • Sem cópia - sem problemas de cópia
  • Podemos criar vários buffers com uma única chamada para glGenBuffers

Minuses:

  • Não é possível vincular buffers diferentes no mesmo objeto
  • Habilitando o cabeçalho do OpenGL

Bem ... muitos contras, o que pode ser feito?

Você pode remover a inclusão de GL / glew.h adicionando outro nível de abstração no qual as funções do OpenGL serão chamadas (em qualquer caso, isso deve ser feito se o suporte ao OpenGL + DirectX estiver planejado). A ligação com buffers diferentes é um pouco mais complicada, pois podemos esquecer qual índice com qual buffer foi banido, como opção, adicionar outra matriz e gravar o tipo de buffer. Eu ainda não o fiz; neste estágio de desenvolvimento, isso é suficiente para mim.

Bônus


Para uso mais conveniente, criei uma classe de ligação com escopo definido. Ali está ele:

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;
};

Alguns podem ficar intrigados com esta linha:

m_ref.template bind<index>();

Se não especificarmos a palavra-chave do modelo, capturaremos o seguinte erro:

binder.hpp: 22: 46 : Palavra-chave 'template' ausente antes do nome do modelo dependente 'bind'
Isso significa que, nesse caso, quando T é um tipo dependente. O compilador ainda não sabe qual é o tipo m_ref. Como ainda não havia ligação, o compilador a processa de forma puramente sintática, portanto, <é interpretado como um operador menor que. Para indicar ao compilador que isso é, de fato, uma especialização de um modelo de função de membro, você deve adicionar a palavra-chave template imediatamente após o operador de ponto.

E um exemplo de uso:

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);
}

Esse código não usa o objeto para a matriz de vértices, porque não funciona, não descobri os motivos, mas logo descobrirei o xD

Além disso, os wrappers para chamadas de dados de buffer e OpenGL ainda não estão prontos, eles não são reescritos no DSA (Direct State Access) .

Obrigado a quem leu até o fim. Ficarei muito feliz em receber críticas e comentários.

All Articles