OpenGL Moderne

Appendice E : Cliquer sur un objet avec un hack OpenGL

Opengl-tutorial.org

Traducteur

Alexandre Laurent

Cette technique n'est pas vraiment conseillée, mais c'est une méthode facile et rapide pour ajouter un picking simple.

Commentez

- Titre : OpenGL Moderne

- Auteur : Opengl-tutorial.org

- Traducteur : Alexandre Laurent

- Parution : 3 décembre 2014

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 opengl-tutorial.org. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Picking en utilisant un hack OpenGL

- I-A. Idée de base

- II. Implémentation

- II-A. Donner un identifiant à chaque objet

- II-B. Détecter le clic

- II-C. Convertir l'identifiant en une couleur spécifique

- II-D. Afficher la scène avec cette couleur

- II-E. Obtenir la couleur sous le curseur de souris

- II-F. Convertir la couleur en un identifiant

- II-G. Utiliser cet identifiant

- III. Avantages et inconvénients

- IV. Remarques finales

- V. Remerciements

- Navigation

Navigation

Sommaire

I - Picking en utilisant un hack OpenGL

Cette technique n'est pas vraiment conseillée, mais c'est une méthode facile et rapide pour ajouter un picking simple. Dans tous les cas, évitez d'utiliser cela dans les jeux, car cela peut provoquer une chute de FPS. Toutefois, si vous avez une simulation et que vous ne vous souciez pas des performances liées au picking, cette technique peut être la solution parfaite.

Le code source pour ce tutoriel est disponible dans le fichier au nom lourd de sens : misc05_picking/misc05_picking_slow_easy.cpp.

I-A - Idée de base

L'idée derrière cette technique est d'afficher la scène, comme d'habitude, mais au lieu d'utiliser un joli rendu, vous dessinez chaque objet avec une couleur spécifique et unique.

Ensuite, vous récupérez la couleur du pixel sous le curseur de la souris et vous utilisez cette couleur pour identifier l'objet. Vous obtenez ainsi l'objet sur lequel l'utilisateur a cliqué.

Voici un exemple :

Dans cette capture d'écran, chaque singe possède une couleur légèrement différente permettant de les identifier.

Bien sûr, vous ne voulez pas voir l'image avec toutes ces couleurs étranges donc vous devez nettoyer l'écran et redessiner comme d'habitude.

II - Implémentation

II-A - Donner un identifiant à chaque objet

Chaque objet de la scène doit avoir un identifiant unique. La façon la plus facile de le faire est de donner à chaque objet un entier et de le convertir en une couleur. Cette couleur ne doit pas avoir de sens ; cette technique est simplement un hack de toute façon.

Dans le code source accompagnant ce tutoriel, 100 objets sont créés et conservés dans un std::vector dont l'identifiant n'est que l'index de l'objet dans ce vecteur. Si vous avez une hiérarchie plus complexe, vous devez certainement ajouter l'identifiant dans votre classe de Mesh et maintenir une sorte de std::map pour associer l'identifiant à l'objet.

II-B - Détecter le clic

Dans cet exemple simple, le picking est effectué à chaque image lorsque le bouton gauche de la souris est appuyé :

if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT)){

Dans une application réelle, vous souhaitez certainement le faire uniquement lorsque l'utilisateur relâche le bouton et donc vous devez garder un booléen estCeQueLeBoutonGaucheAEteAppuyeLorsDeLImagePrecedente, ou mieux, utilisez glfwSetMouseButtonCallback() (lisez le manuel GLFW pour connaître comment l'utiliser).

II-C - Convertir l'identifiant en une couleur spécifique

Comme nous devons afficher chaque modèle avec une couleur différente, la première étape est de calculer cette couleur. Une méthode simple est de placer les bits de poids faible dans le canal rouge et les bits de poids fort dans le canal bleu :

// Converti « i », l'identifiant de modèle en une couleur RBG
int r = (i & 0x000000FF) >>  0;
int g = (i & 0x0000FF00) >>  8;
int b = (i & 0x00FF0000) >> 16;

Cela peut paraître effrayant, mais c'est un code de manipulation de bits standard. Vous obtenez finalement trois entiers, chacun compris entre 0 et 255. Avec cette méthode, vous pouvez représenter 255^3 = 16 millions d'objets différents, ce qui est certainement suffisant.

II-D - Afficher la scène avec cette couleur

Nous avons maintenant besoin d'un shader pour utiliser cette couleur. C'est très simple. Le vertex shader ne fait rien :

#version 330 core

// Données d'entrée du sommet, différentes pour chaque exécution de ce shader.
layout(location = 0) in vec3 vertexPosition_modelspace;

// Valeurs constantes pour tout le modèle.
uniform mat4 MVP;

void main(){

// Position de sortie du sommet, dans l'espace clip : MVP * position
gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

}

Et le fragment shader écrit simplement la couleur voulue dans le tampon d'image :

#version 330 core

// Données de sortie
out vec4 color;

// Valeurs constantes pour tout le modèle.
uniform vec4 PickingColor;

void main(){

color = PickingColor;

}

Facile !

La seule astuce est que vous devez envoyer vos couleurs en nombres flottants (compris dans [0, 1]) alors que vous avez des entiers (compris dans [0, 255]), donc vous devez effectuer une petite division lors de l'appel à glUniformXX() :

// OpenGL attend des couleurs entre [0,1], donc divisez-les par 255.
glUniform4f(pickingColorID, r/255.0f, g/255.0f, b/255.0f, 1.0f);

Vous pouvez maintenant dessiner les modèles comme d'habitude (glBindBuffer, glVertexAttribBuffer, glDrawElements) et vous obtiendrez l'étrange image ci-dessus.

II-E - Obtenir la couleur sous le curseur de souris

Lorsque vous avez dessiné tous les modèles (certainement avec une boucle for()), vous devez appeler glReadPixels(), permettant d'obtenir les pixels rasterizés sur le CPU. Mais pour que cela fonctionne, quelques étapes supplémentaires sont nécessaires.

Premièrement, vous devez appeler glFlush(). Cela indiquera au pilote OpenGL d'envoyer les commandes en attente (incluant votre dernier glDrawXX) au GPU. Cela n'est typiquement pas fait automatiquement, car les commandes sont envoyées en groupe et non immédiatement (cela signifie que lorsque vous appelez glDrawElements(), rien n'est dessiné. Cela VA être dessiné quelques millisecondes plus tard). Cette opération est LENTE.

Ensuite, vous devez appeler glFinish(), qui attendra jusqu'à ce que tout soit vraiment affiché. La différence avec glFlush() est que glFlush() envoie simplement les commandes ; glFinish() attend que les commandes soient exécutées. Cette opération est LEEENTE.

Vous devez aussi configurer comment glReadPixels() se comportera par rapport à l'alignement mémoire. C'est quelque peu hors sujet, mais simplement, vous devez appeler glPixelStore(GL_UNPACK_ALIGNMENT, 1).

Finalement, vous pouvez appeler glReadPixels() ! Voici le code complet :

// Attend jusqu'à ce que toutes les commandes de dessin soient vraiment exécutées.
// C'est ultra méga lent !
// Il y a habituellement un long moment entre glDrawElements() et le rendu de tous les fragments.
glFlush();
glFinish();

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

// Lit le pixel au centre de l'écran.
// Vous pouvez aussi utiliser glfwGetMousePos().
// C'est aussi ultra méga lent, même pour 1 pixel,
// car le tampon d'image est sur le GPU.
unsigned char data[4];
glReadPixels(1024/2, 768/2,1,1, GL_RGBA, GL_UNSIGNED_BYTE, data);

Votre couleur est maintenant dans le tableau data. Ici, vous pouvez voir que l'identifiant est 19 :

II-F - Convertir la couleur en un identifiant

Vous pouvez maintenant retrouver votre identifiant à partir du tampon data. Le code est exactement l'opposé du code de conversion de l'identifiant en une couleur :

// Convertit la couleur en un identifiant
int pickedID =
data[0] +
data[1] * 256 +
data[2] * 256*256;

II-G - Utiliser cet identifiant

Vous pouvez maintenant utiliser l'identifiant comme vous le souhaitez. Dans l'exemple, le texte de l'interface est mis à jour, mais bien sûr, vous pouvez faire ce que vous souhaitez.

if (pickedID == 0x00ffffff){ // Blanc, cela doit être le fond !
message = "background";
}else{
std::ostringstream oss; // Les chaines de caractères C++ sont nulles
oss << "mesh " << pickedID;
message = oss.str();
}

III - Avantages et inconvénients

- Avantages :

- facile et rapide à implémenter ;

- aucun besoin de bibliothèque externe ni de mathématique complexe.

- Inconvénients :

- utilise glFlush(), glFinish(), glReadPixels() qui sont toutes des fonctions notoirement lentes, car elles forcent le CPU à attendre le GPU, détériorant ainsi les performances ;

- vous n'avez pas une information précise : quel triangle est touché, la normale en ce point, etc.

IV - Remarques finales

Bien que cela ne soit pas recommandé, cette technique peut être vraiment utile, mais elle est complètement restreinte au picking. Les méthodes dans les deux prochains tutoriels peuvent être utilisées pour d'autres buts, comme la détection des collisions, faire marcher un avatar sur le sol, effectuer des requêtes de visibilité pour l'intelligence artificielle, etc.

Si finalement vous utilisez cette technique et que vous devez obtenir plusieurs points dans une même image, vous devez obtenir ces points en une seule fois. Par exemple, si vous devez gérer cinq points de toucher, ne dessinez pas cinq fois la scène !

V - Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.

Navigation

Sommaire