Primitives de style OpenGL RAII

Bonjour, utilisateurs Habra. Je n'ai pas écrit depuis longtemps et peut-être que quelqu'un attendait des articles de moi - bien sûr que non. Depuis que mon temps libre est devenu un peu plus et que mon GitHub est complètement vide, j'ai décidé d'écrire mon clone Mein kampf Minecraft. Avec une forte probabilité, je documenterai cela - suivez mes articles sur habr.com . Aujourd'hui, je vais montrer comment j'ai encapsulé des primitives OpenGL dans le style RAII, si c'est intéressant - sous cat.

Écrivez dans les commentaires ce que vous aimeriez lire. Dois-je écrire Minecraft à partir d'articles à partir de zéro? Commençons maintenant. Je vais montrer l' objet tampon comme exemple . La première chose que nous ferons est de faire les appels principaux ( glGenBuffers , glDeleteBuffers ) dans une classe séparée.

Quelque part comme ça:

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

Avantages:

  • Pas besoin de surveiller la suppression du tampon
  • L'activation de l'en-tête OpenGL ne dépasse pas (si nous transférons la définition des fonctions vers * .cpp)

Moins:

  • Nous ne pouvons pas spécifier le type de tampon (tableau, élément ...) lors de la liaison
  • Impossible de créer plusieurs tampons avec un seul appel à glGenBuffers
  • Problèmes d'accès à un objet distant si le tampon a été copié

Afin de résoudre le problème de la copie, nous l'interdisons simplement. Pour ce faire, créez une classe non copiable et héritez-la.

Exemple de classe non copiable:

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

Super, moins un problème. Nous pensons plus loin, comment pourrions-nous résoudre le problème de la liaison ... ajoutez les fonctions de liaison / dissociation:

void bind(buffer_type type) noexcept {
    glBindBuffer(static_cast<GLenum>(type), m_object);
}
    
void unbind() noexcept {
    glBindBuffer(static_cast<GLenum>(type), 0);
}

buffer_type est une classe enum, mais vous pouvez faire extern const (dans * .hpp), et dans (* .cpp) faire quelque chose comme:

const uint32_t array_buffer = GL_ARRAY_BUFFER;

Encore une fois, cela ne ressortirait pas

#include <GL/glew.h>

Avantages:

  • Pas besoin de surveiller la suppression du tampon
  • L'activation de l'en-tête OpenGL ne dépasse pas (si nous transférons la définition des fonctions vers * .cpp)
  • On peut indiquer le type de buffer (tableau, élément ...) lors de la liaison
  • Pas de copie - pas de problèmes de copie

Moins:

  • Impossible de créer plusieurs tampons avec un seul appel à glGenBuffers

Donc, presque tout est super, mais nous ne pouvons toujours pas créer plusieurs tampons ... Corrigeons-le.

glGenBuffers peut prendre un pointeur sur un tableau et le nombre de tampons à générer.
Nous avons besoin d'un tableau, nous pourrions utiliser std :: vector, mais nous n'avons besoin d'allouer de la mémoire qu'une seule fois et je préférerais std :: array ici, bien qu'à l'avenir nous devrons faire un niveau d'abstraction supplémentaire à cause de cela.

Nous réécrivons notre classe dans std :: array et ajoutons un peu de modèles:

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

Et voici ce que nous avons.

Avantages:

  • Pas besoin de surveiller la suppression du tampon
  • On peut indiquer le type de buffer (tableau, élément ...) lors de la liaison
  • Pas de copie - pas de problèmes de copie
  • Nous pouvons créer plusieurs tampons avec un seul appel à glGenBuffers

Moins:

  • Impossible de lier différents tampons sur le même objet
  • L'activation de l'en-tête OpenGL se prolonge

Bon ... trop d'inconvénients, que faire?

Vous pouvez supprimer l'inclusion de GL / glew.h en ajoutant un autre niveau d'abstraction dans lequel les fonctions OpenGL seront appelées (dans tous les cas, cela devrait être fait si la prise en charge d'OpenGL + DirectX est prévue). La liaison avec différents tampons est un peu plus compliquée, car nous pouvons oublier quel index avec quel tampon a été interdit, en option, ajouter un autre tableau et y écrire le type de tampon. Je ne l'ai pas encore fait, à ce stade de développement, cela me suffit.

Prime


Pour une utilisation plus pratique, j'ai créé une classe de liaison de portée. Le voilà:

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

Certains peuvent être intrigués par cette ligne:

m_ref.template bind<index>();

Si nous ne

spécifions pas le mot clé de modèle, nous interceptons l'erreur suivante: binder.hpp: 22: 46: Mot clé 'modèle' manquant avant le nom de modèle dépendant 'bind'
Cela signifie que dans ce cas, lorsque T est un type dépendant. Le compilateur ne sait pas encore de quel type est m_ref. Puisqu'il n'y avait pas encore de liaison, le compilateur la traite purement syntaxiquement, donc <est interprété comme un opérateur inférieur à. Pour indiquer au compilateur qu'il s'agit en fait d'une spécialisation d'un modèle de fonction membre, vous devez ajouter le mot clé de modèle immédiatement après l'opérateur point.

Et un exemple d'utilisation:

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

Ce code n'utilise pas l'objet pour Vertex Array, car il ne fonctionne pas, je n'ai pas encore trouvé les raisons, mais je le découvrirai bientôt xD

Wrappers pour Buffer Data et les appels OpenGL ne sont pas encore prêts, les appels ne sont pas réécrits en DSA (Direct State Access) .

Merci à ceux qui l'ont lu jusqu'au bout. Je serai très heureux des critiques et des commentaires.

All Articles