OpenSceneGraph: Nœuds de groupe, nœuds de transformation et nœuds de commutation

image

Présentation


Lorsqu'un point, une ligne ou un polygone complexe est dessiné dans un monde en trois dimensions, le résultat final sera finalement affiché sur un écran plat en deux dimensions. En conséquence, les objets tridimensionnels passent par un certain chemin de transformation, se transformant en un ensemble de pixels affichés dans une fenêtre bidimensionnelle.

Le développement d'outils logiciels qui implémentent des graphiques tridimensionnels est arrivé, quel que soit celui que vous choisissez, à peu près au même concept de descriptions à la fois mathématiques et algorithmiques des transformations ci-dessus. Sur le plan idéologique, les API graphiques «propres» comme OpenGL et les moteurs de jeux sympas comme Unity et Unreal, utilisent des mécanismes similaires pour décrire la transformation d'une scène en trois dimensions. OpenSceneGraph ne fait pas exception.

Dans cet article, nous passerons en revue les mécanismes de regroupement et de transformation d'objets tridimensionnels dans OSG.

1. Matrice de modèle, matrice de vue et matrice de projection


Trois matrices de base impliquées dans la transformation des coordonnées sont impliquées dans la transformation entre différents systèmes de coordonnées. Souvent, en termes OpenGL, ils sont appelés matrice de modèle, matrice de vue et matrice de projection .

La matrice du modèle est utilisée pour décrire l'emplacement de l'objet dans le monde 3D. Il convertit les sommets du système de coordonnées local de l'objet en système de coordonnées mondial . Soit dit en passant, tous les systèmes de coordonnées dans OSG sont droitiers .

L'étape suivante est la transformation des coordonnées du monde en un espace de vue, effectuée à l'aide de la matrice de vue. Supposons que nous ayons une caméra située à l'origine du système de coordonnées mondial. La matrice inverse de la matrice de transformation de la caméra est en fait utilisée comme matrice de vue. Dans un système de coordonnées droitier, OpenGL, par défaut, définit toujours une caméra située au point (0, 0, 0) du système de coordonnées global et dirigée le long de la direction négative de l'axe Z.

Je note que dans OpenGL la matrice de modèle et la matrice de vue ne sont pas séparées. Cependant, la matrice modèle-vue y est déterminée, ce qui convertit les coordonnées locales de l'objet en coordonnées de l'espace de vue. Cette matrice est en fait le produit de la matrice modèle et de la matrice de la forme. Ainsi, la transformation d'un sommet V de coordonnées locales en un espace de la forme peut être conditionnellement écrite comme le produit

Ve = V * modelViewMatrix 

La prochaine tâche importante consiste à déterminer comment les objets 3D seront projetés sur le plan de l'écran et à calculer la soi-disant pyramide d'écrêtage - une zone d'espace contenant des objets à afficher à l'écran. La matrice de projection est utilisée pour spécifier la pyramide d'écrêtage définie dans l'espace mondial par six plans: gauche, droite, inférieure, supérieure, proche et éloignée. OpenGL fournit la fonction gluPerapective (), qui vous permet de spécifier une pyramide de découpage et un moyen de projeter un monde en trois dimensions sur un plan.

Le système de coordonnées obtenu après les transformations ci-dessus est appelé système de coordonnées normalisé de l'appareil , a une plage de coordonnées de -1 à 1 sur chaque axe et est gaucher. Et, comme dernière étape, les données reçues sont projetées dans le port d'affichage (fenêtre) de la fenêtre, défini par le rectangle de la zone cliente de la fenêtre. Après cela, le monde 3D apparaît sur notre écran 2D. La valeur finale des coordonnées d'écran des sommets Vs peut être exprimée par la transformation suivante

 Vs = V * modelViewMatrix * projectionMatrix * windowMatrix 

ou

 Vs = V * MVPW 

où MVPW est la matrice de transformation équivalente égale au produit de trois matrices: matrices vue modèle, matrices de projection et matrices de fenêtre.

Vs dans cette situation est un vecteur tridimensionnel qui détermine la position d'un pixel 2D avec une valeur de profondeur. En inversant l'opération de transformation des coordonnées, nous obtenons une ligne dans l'espace tridimensionnel. Par conséquent, un point 2D peut être considéré comme deux points - l'un sur le proche (Zs = 0), l'autre sur le plan de délimitation éloigné (Zs = 1). Les coordonnées de ces points dans l'espace tridimensionnel

 V0 = (Xs, Ys, 0) * invMVPW V1 = (Xs, Ys, 1) * invMVPW 

oĂą invMVPW est l'inverse de MVPW.

Dans tous les exemples discutés jusqu'à présent, nous avons créé un seul objet tridimensionnel dans les scènes. Dans ces exemples, les coordonnées locales de l'objet coïncident toujours avec les coordonnées globales globales. Il est maintenant temps de parler d'outils qui vous permettent de placer de nombreux objets dans la scène et de changer leur position dans l'espace.

2. NĹ“uds de groupe


La classe osg :: Group est le soi-disant nœud de groupe d'un graphe de scène dans OSG. Il peut avoir n'importe quel nombre de nœuds enfants, y compris des nœuds de feuille de géométrie ou d'autres nœuds de groupe. Ce sont les nœuds les plus couramment utilisés avec des fonctionnalités étendues.

La classe osg :: Group est dérivée de la classe osg :: Node et hérite en conséquence de la classe osg :: Referenced. osg :: Group contient une liste de nœuds enfants, où chaque nœud enfant est contrôlé par un pointeur intelligent. Cela garantit qu'il n'y a pas de fuite de mémoire lors de la mise en cascade d'une branche d'une arborescence de scènes. Cette classe fournit au développeur un certain nombre de méthodes publiques.
  1. addChild () - ajoute le nœud à la fin de la liste des nœuds enfants. D'un autre côté, il y a la méthode insertChild (), qui place le nœud enfant à une position spécifique dans la liste, qui est spécifiée par un index entier ou un pointeur sur le nœud, passé en paramètre.
  2. removeChild () et removeChildren () - suppriment un seul nœud ou groupe de nœuds.
  3. getChild () - obtenir un pointeur vers un nœud par son index dans la liste
  4. getNumChildren () - obtenir le nombre de nœuds enfants attachés à ce groupe.

Gestion des nœuds parents


Comme nous le savons déjà, la classe osg :: Group gère des groupes de ses objets enfants, parmi lesquels il peut y avoir des instances osg :: Geode qui contrôlent la géométrie des objets de scène. Les deux classes ont une interface pour gérer les nœuds parents.

OSG permet aux nœuds de scène d'avoir plusieurs nœuds parents (nous en reparlerons un jour plus tard). En attendant, nous examinerons les méthodes définies dans osg :: Node qui sont utilisées pour manipuler les nœuds parents:
  1. getParent () - retourne un pointeur de type osg :: Group contenant une liste de nœuds parents.
  2. getNumParants () - renvoie le nombre de nœuds parents.
  3. getParentalNodePath () - renvoie tous les chemins possibles vers le nœud racine de la scène à partir du nœud actuel. Il renvoie une liste de variables de type osg :: NodePath.

osg :: NodePath est un vecteur std :: de pointeurs vers les nœuds de scène.



Par exemple, pour la scène illustrée dans la figure, le code suivant

 osg::NodePath &nodePath = child3->getParentalNodePaths()[0]; for (unsigned int i = 0; i < nodePath.size(); ++i) { osg::Node *node = nodePath[i]; // -    } 

renverra les nœuds Root, Child1, Child2.

Vous ne devez pas utiliser de mécanismes de gestion de la mémoire pour référencer les nœuds parents. Lorsqu'un nœud parent est supprimé, tous les nœuds enfants sont automatiquement supprimés, ce qui peut entraîner un blocage de l'application.

3. Ajout de plusieurs modèles à l'arborescence de la scène


Nous illustrons le mécanisme d'utilisation des groupes avec l'exemple suivant.

Exemple de groupe complet
main.h
 #ifndef MAIN_H #define MAIN_H #include <osg/Group> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" int main(int argc, char *argv[]) { (void) argc, (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cow.osg"); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model1.get()); root->addChild(model2.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Fondamentalement, l'exemple diffère de tous les précédents en ce que nous chargeons deux modèles tridimensionnels et pour les ajouter à la scène, nous créons un nœud racine de groupe et nous y ajoutons nos modèles en tant que nœuds enfants

 osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model1.get()); root->addChild(model2.get()); 



En conséquence, nous obtenons une scène composée de deux modèles - un avion et une drôle de vache miroir. Soit dit en passant, une vache miroir ne sera pas miroir, sauf si vous copiez sa texture depuis OpenSceneGraph-Data / Images / reflect.rgb dans le répertoire data / Images de notre projet.

La classe osg :: Group peut accepter tous les types de nœuds en tant qu'enfants, y compris les nœuds de son type. Au contraire, la classe osg :: Geode ne contient aucun nœud enfant - c'est un nœud terminal contenant la géométrie de l'objet scène. Ce fait est pratique lorsque vous demandez si le nœud est un nœud de type osg :: Group ou un autre type de dérivé d'osg :: Node. Regardons un petit exemple.

 osg::ref_ptr<osg::Group> model = dynamic_cast<osg::Group *>(osgDB::readNodeFile("../data/cessna.osg")); 

La valeur renvoyée par la fonction osgDB :: readNodeFile () est toujours de type osg :: Node *, mais elle peut être convertie en son descendant osg :: Group *. Si le nœud du modèle Cessna est un nœud de groupe, la conversion réussira, sinon la conversion renverra NULL.

Vous pouvez Ă©galement effectuer la mĂŞme astuce qui fonctionne sur la plupart des compilateurs

 //      osg::ref_ptr<osg::Group> group = ...; //     osg::Node* node1 = dynamic_cast<osg::Node*>( group.get() ); //      osg::Node* node2 = group.get(); 

Dans les emplacements de code critiques pour les performances, il est préférable d'utiliser des méthodes de conversion spéciales

 osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg"); osg::Group* convModel1 = model->asGroup(); //   osg::Geode* convModel2 = model->asGeode(); //  NULL. 

4. NĹ“uds de transformation


Les nœuds Osg :: Group ne peuvent effectuer aucune transformation, sauf la possibilité d'accéder à leurs nœuds enfants. OSG fournit la classe osg :: Transform pour le mouvement de la géométrie spatiale. Cette classe est un successeur de la classe osg :: Group, mais elle est également abstraite - en pratique, ses héritiers sont utilisés à la place, qui implémentent diverses transformations spatiales de la géométrie. Lors de la traversée du graphe de la scène, le nœud osg :: Transform ajoute sa transformation à la matrice de transformation OpenGL actuelle. Cela équivaut à multiplier les matrices de transformation OpenGL par la commande glMultMatrix ()



Cet exemple de graphique de scène peut être traduit dans le code OpenGL suivant

 glPushMatrix(); glMultMatrix( matrixOfTransform1 ); renderGeode1(); glPushMatrix(); glMultMatrix( matrixOfTransform2 ); renderGeode2(); glPopMatrix(); glPopMatrix(); 

Nous pouvons dire que la position de Geode1 est définie dans le système de coordonnées Transform1, et la position de Geode2 est définie dans le système de coordonnées Transform2, décalée par rapport à Transform1. Dans le même temps, le positionnement en coordonnées absolues peut être activé dans OSG, ce qui conduira au comportement de l'objet, équivalent au résultat de la commande OpenGL glGlobalMatrix ()

 transformNode->setReferenceFrame( osg::Transform::ABSOLUTE_RF ); 

Vous pouvez revenir au mode de positionnement relatif

 transformNode->setReferenceFrame( osg::Transform::RELATIVE_RF ); 

5. Le concept d'une matrice de transformation de coordonnées


Le type osg :: Matrix est un type OSG de base non contrôlé par des pointeurs intelligents. Il fournit une interface pour les opérations sur des matrices 4x4 qui décrivent la transformation des coordonnées, telles que le déplacement, la rotation, la mise à l'échelle et le calcul des projections. La matrice peut être spécifiée explicitement.

 //   44 osg::Matrix mat(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f ); 

La classe osg :: Matrix fournit les méthodes publiques suivantes:

  1. postMult () et operator * () - la bonne multiplication de la matrice courante par la matrice ou le vecteur passé en paramètre. La méthode preMult () effectue une multiplication à gauche.
  2. makeTranslate (), makeRotate () et makeScale () - réinitialisez la matrice actuelle et créez une matrice 4x4 qui décrit le mouvement, la rotation et la mise à l'échelle. leurs versions statiques translate (), rotate () et scale () peuvent être utilisées pour créer un objet matriciel avec des paramètres spécifiques.
  3. invert () - calcule l'inverse de la matrice actuelle. Sa version statique d'inverse () prend une matrice comme paramètre et renvoie une nouvelle matrice inverse à la donnée.

OSG comprend les matrices comme des matrices de chaînes et les vecteurs comme des chaînes.Par conséquent, pour appliquer une transformation matricielle à un vecteur, procédez comme suit

 osg::Matrix mat = …; osg::Vec3 vec = …; osg::Vec3 resultVec = vec * mat; 

L'ordre des opérations matricielles est facile à comprendre en regardant comment les matrices sont multipliées pour obtenir une conversion équivalente

 osg::Matrix mat1 = osg::Matrix::scale(sx, sy, sz); osg::Matrix mat2 = osg::Matrix::translate(x, y, z); osg::Matrix resultMat = mat1 * mat2; 

Le développeur doit lire le processus de transformation de gauche à droite. C'est-à-dire que dans le fragment de code décrit, le vecteur est d'abord mis à l'échelle, puis son mouvement.

osg :: Matrixf contient des éléments de type float.

6. Utilisation de la classe osg :: MatrixTransform


Nous appliquons les connaissances théoriques acquises en pratique en chargeant deux modèles d'avion à différents points de la scène.

Texte intégral de l'exemple de transformation
main.h
 #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform; transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0)); transform1->addChild(model.get()); osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform; transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0)); transform2->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(transform1.get()); root->addChild(transform2.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


L'exemple est en fait assez banal. Chargement du modèle d'avion à partir du fichier

 osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); 

Créer un nœud de transformation

 osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform; 

Nous définissons comme matrice de transformation le mouvement du modèle le long de l'axe X de 25 unités vers la gauche

 transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0)); 

Nous définissons notre modèle pour le nœud de transformation en tant que nœud enfant

 transform1->addChild(model.get()); 

Nous faisons de même avec la deuxième transformation, mais en tant que matrice, nous réglons le mouvement vers la droite de 25 unités

 osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform; transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0)); transform2->addChild(model.get()); 

Nous créons un nœud racine et en tant que nœuds de transformation, nous définissons des nœuds de transformation transform1 et transform2

 osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(transform1.get()); root->addChild(transform2.get()); 

Créez une visionneuse et passez-lui le nœud racine en tant que données de scène

 osgViewer::Viewer viewer; viewer.setSceneData(root.get()); 

L'exécution du programme donne une telle image



La structure du graphique de scène dans cet exemple est la suivante.



Il ne faut pas confondre le fait que les nœuds de transformation (Child 1.1 et Child 1.2) se réfèrent au même objet enfant du modèle d'avion (Child 2). Il s'agit d'un mécanisme OSG standard, lorsqu'un nœud enfant d'un graphe de scène peut avoir plusieurs nœuds parents. Ainsi, nous n'avons pas besoin de stocker deux instances du modèle dans notre mémoire pour obtenir deux plans identiques dans la scène. Ce mécanisme vous permet d'allouer très efficacement la mémoire dans l'application. Le modèle ne sera pas supprimé de la mémoire tant qu'il ne sera pas appelé enfant, au moins un nœud.

Dans son action, la classe osg :: MatrixTransform est équivalente aux commandes OpenGL glMultMatrix () et glLoadMatrix (), implémente tous les types de transformations spatiales, mais est difficile à utiliser en raison de la nécessité de calculer la matrice de transformation.

La classe osg :: PositionAttitudeTransform fonctionne comme les fonctions OpenGL glTranslate (), glScale (), glRotate (). Il fournit des méthodes publiques pour convertir les nœuds enfants:

  1. setPosition () - déplace le nœud vers un point donné dans l'espace spécifié par le paramètre osg :: Vec3
  2. setScale () - redimensionne l'objet le long des axes de coordonnées. Les facteurs d'échelle le long des axes correspondants sont définis par un paramètre de type osg :: Vec3
  3. setAttitude () - définit l'orientation spatiale de l'objet. En tant que paramètre, la transformation de rotation osg :: Quat quaternion est utilisée, dont le constructeur a plusieurs surcharges qui vous permettent de spécifier le quaternion à la fois directement (composant par composant) et, par exemple, via les angles d'Euler osg :: Quat (xAngle, osg :: X_AXIS, yAngle, osg :: Y_AXIS, zAngle, osg :: Z_AXIS) (les angles sont donnés en radians!)


7. Changer de nœuds


Prenons une autre classe - osg :: Switch, qui vous permet d'afficher ou d'ignorer le rendu d'un nœud de scène, en fonction d'une condition logique. C'est un descendant de la classe osg :: Group et attache une valeur logique à chacun de ses enfants. Il a plusieurs méthodes publiques utiles:
  1. Surchargé, addChild (), en tant que deuxième paramètre, prend une clé logique qui indique s'il faut ou non afficher ce nœud.
  2. setValue () - définit la clé de visibilité / invisibilité. Il prend l'index du nœud enfant qui nous intéresse et la valeur de clé souhaitée. En conséquence, getValue () vous permet d'obtenir la valeur de clé actuelle par l'index du nœud qui nous intéresse.
  3. setNewChildDefaultValue () - définit la valeur par défaut pour la clé de visibilité de tous les nouveaux objets ajoutés en tant qu'enfants.

Considérez l'application de cette classe avec un exemple.

Exemple de commutateur complet
main.h
 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 


main.cpp
 #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), false); root->addChild(model2.get(), true); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


L'exemple est trivial - nous chargeons deux modèles: un cessna conventionnel et un cessna avec l'effet d'un moteur en feu

 osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); 

Cependant, nous créons osg :: Switch en tant que nœud racine, ce qui nous permet, lors de l'ajout de modèles en tant que nœuds enfants, de définir la clé de visibilité pour chacun d'eux

 osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), false); root->addChild(model2.get(), true); 

Autrement dit, model1 ne sera pas rendu, et model2 sera, que nous observerons en exécutant le programme



En échangeant les valeurs des clés, nous verrons l'image opposée

 root->addChild(model1.get(), true); root->addChild(model2.get(), false); 



Armant les deux touches, nous verrons deux modèles en même temps

 root->addChild(model1.get(), true); root->addChild(model2.get(), true); 



Vous pouvez activer la visibilité et l'invisibilité d'un nœud, un enfant d'osg :: Switch, à tout moment en utilisant la méthode setValue ()

 switchNode->setValue(0, false); switchNode->setValue(0, true); switchNode->setValue(1, true); switchNode->setValue(1, false); 

Conclusion


Dans ce didacticiel, nous avons examiné toutes les principales classes de nœuds intermédiaires utilisées dans OpenSceeneGraph. Ainsi, nous avons posé une autre brique de base dans la fondation des connaissances sur le dispositif de ce moteur graphique sans aucun doute intéressant. Les exemples discutés dans l'article, comme précédemment, sont disponibles dans mon référentiel sur Github .

Ă€ suivre ...

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


All Articles