OpenSceneGraph: graphique de scène et pointeurs intelligents

image

Présentation


Dans un article précédent, nous avons examiné l'assemblage OpenSceneGraph à partir de la source et écrit un exemple élémentaire dans lequel un plan gris se bloque dans un monde violet vide. Je suis d'accord, pas trop impressionnant. Cependant, comme je l'ai dit plus tôt, dans ce petit exemple, il existe les principaux concepts sur lesquels ce moteur graphique est basé. Examinons-les plus en détail. Le matériel ci-dessous utilise des illustrations du blog d' Alexander Bobkov sur OSG (c'est dommage que l'auteur ait abandonné d'écrire sur OSG ...). L'article est également basé sur du matériel et des exemples du livre OpenSceneGraph 3.0. Guide du débutant

Je dois dire que la publication précédente a fait l'objet de certaines critiques, avec lesquelles je suis partiellement d'accord - le matériel est sorti non dit et sorti de son contexte. Je vais essayer de corriger cette omission sous la coupe.

1. En bref sur le graphique de la scène et ses nœuds


Le concept central du moteur est le soi-disant graphe de scène (ce n'est pas un hasard s'il s'est coincé au nom du cadre lui-même) - une structure arborescente hiérarchique qui vous permet d'organiser une représentation logique et spatiale d'une scène en trois dimensions. Le graphe de scène contient le nœud racine et ses nœuds ou nœuds intermédiaires et terminaux associés.

Par exemple



Ce graphique représente une scène composée d'une maison et d'une table. La maison a une certaine représentation géométrique et est située d'une certaine manière dans l'espace par rapport à un certain système de coordonnées de base associé au nœud racine (racine). Le tableau est également décrit par une géométrie, située d'une certaine manière par rapport à la maison, et avec la maison - par rapport au nœud racine. Tous les nœuds, ayant une propriété commune, car ils héritent d'une classe osg :: Node, sont divisés en types selon leur fonction

  1. Les nœuds de groupe (osg :: Group) - sont la classe de base pour tous les nœuds intermédiaires et sont conçus pour combiner d'autres nœuds en groupes
  2. Noeuds de transformation (osg :: Transform et ses descendants) - conçus pour décrire la transformation des coordonnées d'objet
  3. Noeuds géométriques (osg :: Geode) - noeuds terminaux (feuilles) du graphe de scène contenant des informations sur un ou plusieurs objets géométriques.

La géométrie des objets de scène dans OSG est décrite dans son propre système de coordonnées local de l'objet. Les nœuds de transformation situés entre cet objet et le nœud racine implémentent des transformations de coordonnées matricielles pour obtenir la position de l'objet dans le système de coordonnées de base.

Les nœuds remplissent de nombreuses fonctions importantes, en particulier, stockent l'état de l'affichage des objets, et cet état affecte uniquement le sous-graphique associé à ce nœud. Plusieurs rappels peuvent être associés aux nœuds du graphe de scène, des gestionnaires d'événements qui vous permettent de modifier l'état du nœud et le sous-graphique qui lui est associé.

Toutes les opérations globales sur le graphe de scène associées à l'obtention du résultat final à l'écran sont effectuées automatiquement par le moteur, en parcourant périodiquement le graphe en profondeur.

Dans l'exemple examiné la dernière fois , notre scène consistait en un seul objet - un modèle d'avion chargé à partir d'un fichier. En regardant très loin, je dirai que ce modèle est le nœud feuille du graphe de scène. Il est étroitement soudé au système de coordonnées de base global du moteur.

2. Gestion de la mémoire OSG


Étant donné que les nœuds du graphe de scène stockent beaucoup de données sur les objets de scène et les opérations sur eux, il est nécessaire d'allouer de la mémoire, y compris de manière dynamique, pour stocker ces données. Dans ce cas, lors de la manipulation du graphique de la scène et, par exemple, de la suppression de certains de ses nœuds, vous devez surveiller attentivement que les nœuds supprimés du graphique ne sont plus traités. Ce processus s'accompagne toujours d'erreurs et d'un débogage long, car il est assez difficile pour le développeur de suivre les pointeurs vers les objets qui font référence aux données existantes et ceux qui doivent être supprimés. Sans une gestion efficace de la mémoire, les erreurs de segmentation et les fuites de mémoire sont plus susceptibles de se produire.

La gestion de la mémoire est une tâche critique dans OSG et son concept repose sur deux points:

  1. Allocation de mémoire: assurer l'allocation de la quantité de mémoire nécessaire au stockage d'un objet.
  2. Libérez de la mémoire: renvoyez la mémoire allouée au système lorsqu'elle n'est pas nécessaire.

De nombreux langages de programmation modernes, tels que C #, Java, Visual Basic .Net et similaires, utilisent le soi-disant garbage collector pour libérer la mémoire allouée. Le concept du langage C ++ ne prévoit pas une telle approche, cependant, nous pouvons l'imiter en utilisant les soi-disant pointeurs intelligents.

Aujourd'hui, C ++ a des pointeurs intelligents dans son arsenal, qui est appelé «prêt à l'emploi» (et la norme C ++ 17 a déjà réussi à débarrasser le langage de certains types obsolètes de pointeurs intelligents), mais ce n'était pas toujours le cas. La première des versions officielles de l'OSG numérotée 0,9 est née en 2002, et il restait trois ans avant la première version officielle. À cette époque, la norme C ++ ne prévoyait pas encore de pointeurs intelligents, et même si vous croyez à une digression historique , le langage lui-même traversait des moments difficiles. Ainsi, l'apparition d'un vélo sous la forme de ses propres pointeurs intelligents, qui sont mis en œuvre dans OSG, n'est pas du tout surprenante. Ce mécanisme est profondément intégré dans la structure du moteur, il est donc absolument nécessaire de comprendre son fonctionnement dès le début.

3. Les classes osg :: ref_ptr <> et osg :: Referenced


OSG fournit son propre mécanisme de pointeur intelligent basé sur la classe de modèle osg :: ref_ptr <> pour implémenter le garbage collection automatique. Pour son bon fonctionnement, OSG fournit une autre classe osg :: Referenced pour gérer les blocs de mémoire dont la référence est comptée.

La classe osg :: ref_ptr <> fournit plusieurs opérateurs et méthodes.

  • get () est une méthode publique qui renvoie un pointeur brut, par exemple, lorsque vous utilisez le modèle osg :: Node comme argument, cette méthode renvoie osg :: Node *.
  • L'opérateur * () est en fait l'opérateur de déréférence.
  • operator -> () et operator = () - vous permettent d'utiliser osg :: ref_ptr <> comme pointeur classique lors de l'accès aux méthodes et propriétés des objets décrits par ce pointeur.
  • operator == (), operator! = () et operator! () - vous permettent d'effectuer des opérations de comparaison sur des pointeurs intelligents.
  • valid () est une méthode publique qui renvoie true si le pointeur géré a la valeur correcte (pas NULL). L'expression some_ptr.valid () est équivalente à l'expression some_ptr! = NULL si some_ptr est un pointeur intelligent.
  • release () est une méthode publique, utile lorsque vous souhaitez renvoyer une adresse gérée à partir d'une fonction. À ce sujet sera décrit plus en détail ultérieurement.

La classe osg :: Referenced est la classe de base pour tous les éléments du graphique de scène, tels que les nœuds, la géométrie, les états de rendu et les autres objets placés sur la scène. Ainsi, en créant le nœud racine de la scène, nous héritons indirectement de toutes les fonctionnalités fournies par la classe osg :: Referenced. Par conséquent, dans notre programme, il y a une annonce

osg::ref_ptr<osg::Node> root; 

La classe osg :: Referenced contient un compteur entier pour les références au bloc de mémoire alloué. Ce compteur est initialisé à zéro dans le constructeur de classe. Il est incrémenté de un lorsque l'objet osg :: ref_ptr <> est créé. Ce compteur diminue dès que toute référence à l'objet décrit par ce pointeur est supprimée. Un objet est automatiquement détruit lorsqu'un pointeur intelligent cesse de le référencer.

La classe osg :: Referenced a trois méthodes publiques:

  • ref () est une méthode publique qui incrémente de 1 référence.
  • unref () est une méthode publique, diminuant d'un décompte de références.
  • referenceCount () est une méthode publique qui renvoie la valeur actuelle du compteur de référence, ce qui est utile lors du débogage du code.

Ces méthodes sont disponibles dans toutes les classes dérivées de osg :: Referenced. Cependant, il faut se rappeler que le contrôle manuel du compteur de liens peut entraîner des conséquences imprévisibles, et en l'utilisant, vous devez comprendre clairement ce que vous faites.

4. Comment OSG collecte les ordures et pourquoi est-il nécessaire


Il existe plusieurs raisons pour lesquelles des pointeurs intelligents et la récupération de place doivent être utilisés:

  • Minimisation des erreurs critiques: l'utilisation de pointeurs intelligents vous permet d'automatiser l'allocation et la libération de mémoire. Il n'y a pas de pointeurs bruts dangereux.
  • Gestion efficace de la mémoire: la mémoire allouée à l'objet est libérée immédiatement, dès que l'objet devient inutile, ce qui conduit à une utilisation économique des ressources système.
  • Facilitation du débogage d'application: ayant la capacité de suivre clairement le nombre de liens vers un objet, nous avons des opportunités pour différents types d'optimisations et d'expériences.

Supposons qu'un graphe de scène se compose d'un nœud racine et de plusieurs niveaux de nœuds enfants. Si le nœud racine et tous les nœuds enfants sont gérés à l'aide de la classe osg :: ref_ptr <>, l'application peut uniquement suivre le pointeur vers le nœud racine. La suppression de ce nœud entraînera une suppression séquentielle et automatique de tous les nœuds enfants.



Les pointeurs intelligents peuvent être utilisés comme variables locales, variables globales, membres de classe et diminuer automatiquement le nombre de références lorsque le pointeur intelligent sort de la portée.

Les pointeurs intelligents sont fortement recommandés par les développeurs OSG pour une utilisation dans les projets, mais il y a quelques points fondamentaux auxquels vous devez faire attention:

  • Les instances d'osg :: Referenced et ses dérivés peuvent être créés exclusivement sur le tas. Ils ne peuvent pas être créés sur la pile en tant que variables locales, car les destructeurs de ces classes sont déclarés protégés. Par exemple

 osg::ref_ptr<osg::Node> node = new osg::Node; //  osg::Node node; //  

  • Vous pouvez créer des nœuds de scène temporaires à l'aide de pointeurs C ++ normaux, mais cette approche n'est pas sûre. Il est préférable d'utiliser des pointeurs intelligents pour vous assurer que le graphique de la scène est correctement géré.

 osg::Node *tmpNode = new osg::Node; //  ,  ... osg::ref_ptr<osg::Node> node = tmpNode; //         ! 

  • Vous ne devez en aucun cas utiliser des scènes de liens cycliques dans l'arborescence lorsque le nœud se réfère directement ou indirectement à plusieurs niveaux



Dans l'exemple de graphique du graphique de scène, le nœud Child 1.1 se réfère à lui-même et le nœud Child 2.2 fait également référence au nœud Child 1.2. Ce type de liens peut entraîner un calcul incorrect du nombre de liens et un comportement indéfini du programme.

5. Suivi des objets gérés


Pour illustrer le fonctionnement du mécanisme de pointeur intelligent dans OSG, nous écrivons l'exemple synthétique suivant

main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/ref_ptr> #include <osg/Referenced> #include <iostream> #endif // MAIN_H 

main.cpp

 #include "main.h" class MonitoringTarget : public osg::Referenced { public: MonitoringTarget(int id) : _id(id) { std::cout << "Constructing target " << _id << std::endl; } protected: virtual ~MonitoringTarget() { std::cout << "Dsetroying target " << _id << std::endl; } int _id; }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; osg::ref_ptr<MonitoringTarget> anotherTarget = target; std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; return 0; } 

Nous créons une classe descendante osg :: Referenced qui ne fait rien sauf dans le constructeur et le destructeur qu'elle signale que son instance est créée et affiche l'identifiant qui est déterminé lors de la création de l'instance. Créer une instance de la classe à l'aide du mécanisme de pointeur intelligent

 osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); 

Ensuite, nous affichons le compteur de référence pour l'objet cible

 std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; 

Après cela, créez un nouveau pointeur intelligent, en lui affectant la valeur du pointeur précédent

 osg::ref_ptr<MonitoringTarget> anotherTarget = target; 

et afficher à nouveau le compteur de référence

 std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; 

Voyons ce que nous avons obtenu en analysant la sortie du programme

 15:42:39:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Dsetroying target 0 15:42:42:   

Lorsque le constructeur de classe démarre, un message correspondant s'affiche, nous indiquant que la mémoire de l'objet est allouée et que le constructeur a bien fonctionné. De plus, après avoir créé un pointeur intelligent, nous voyons que le compteur de référence pour l'objet créé a augmenté de un. Créer un nouveau pointeur, lui attribuer la valeur de l'ancien pointeur, c'est essentiellement créer un nouveau lien vers le même objet, de sorte que le compteur de référence est incrémenté par un autre. Lorsque le programme se ferme, le destructeur de la classe MonitoringTarget est appelé.



Faisons une autre expérience en ajoutant un tel code à la fin de la fonction main ()

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i); } 

conduisant à un tel programme "d'échappement"

 16:04:30:   Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Constructing target 1 Dsetroying target 1 Constructing target 2 Dsetroying target 2 Constructing target 3 Dsetroying target 3 Constructing target 4 Dsetroying target 4 Dsetroying target 0 16:04:32:   

Nous créons plusieurs objets dans le corps de la boucle, à l'aide d'un pointeur intelligent. Étant donné que la portée du pointeur ne s'étend dans ce cas qu'au corps de la boucle, à sa sortie, le destructeur est automatiquement appelé. Cela n'arriverait pas, bien évidemment, nous utiliserions les pointeurs habituels.

La libération automatique de la mémoire est une autre caractéristique importante du travail avec les pointeurs intelligents. Étant donné que le destructeur de classe dérivée osg :: Referenced est protégé, nous ne pouvons pas appeler explicitement l'opérateur de suppression pour supprimer l'objet. La seule façon de supprimer un objet est de réinitialiser le nombre de liens vers celui-ci. Mais alors notre code devient dangereux lors du traitement de données multithread - nous pouvons accéder à un objet déjà supprimé à partir d'un autre thread.

Heureusement, OSG fournit une solution à ce problème à l'aide de son planificateur de suppression d'objets. Ce planificateur est basé sur l'utilisation de la classe osg :: DeleteHandler. Il fonctionne de telle manière qu'il n'effectue pas l'opération de suppression d'un objet immédiatement, mais l'exécute après un certain temps. Tous les objets à supprimer sont stockés temporairement jusqu'au moment de la suppression en toute sécurité, puis ils sont tous supprimés en même temps. Le planificateur de suppression osg :: DeleteHandler est contrôlé par le backend de rendu OSG.

6. Retour de fonction


Ajoutez la fonction suivante à notre exemple de code

 MonitoringTarget *createMonitoringTarget(int id) { osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id); return target.release(); } 

et remplacer l'appel au nouvel opérateur dans la boucle par l'appel à cette fonction

 for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i); } 

L'appel release () réduira le nombre de références à l'objet à zéro, mais au lieu de supprimer la mémoire, il renvoie directement le pointeur réel à la mémoire allouée. Si ce pointeur est affecté à un autre pointeur intelligent, il n'y aura aucune fuite de mémoire.

Conclusions


Les concepts de graphe de scène et de pointeurs intelligents sont fondamentaux pour comprendre le principe de fonctionnement, et donc l'utilisation efficace d'OpenSceneGraph. En ce qui concerne les pointeurs intelligents OSG, n'oubliez pas que leur utilisation est absolument

  • Le stockage à long terme de l'installation est prévu.
  • Un objet stocke un lien vers un autre objet
  • Vous devez renvoyer un pointeur depuis une fonction

L'exemple de code fourni dans l'article est disponible ici .

À suivre ...

Source: https://habr.com/ru/post/fr429914/


All Articles