Intégrer Ogre à Qt

Denys Bulant

Ce tutoriel est destiné à illustrer l'intégration du moteur 3D Ogre à Qt. Les concepts utilisés ici sont tout à fait utilisables pour d'autres moteurs (comme IrrLicht) et/ou pour d'autres toolkits graphiques (comme WxWidget ou gtkmm).

- Titre : Intégrer Ogre à Qt

- Auteur : Denys Bulant

- Parution : 11 juillet 2008

- Mise à jour : 6 janvier 2020

- 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 ® 2008 Denys Bulant. 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

- I. Introduction

- I-A. Pourquoi intégrer Ogre à Qt?

- I-B. Prérequis

- II. Initialiser le système

- II-A. Objectif : écran noir (mais grâce à Ogre !)

- II-B. Première interaction : modification de la couleur de fond

- III. Affichage d'un objet et gestion de la caméra

- III-A. Ajouter de la lumière et un objet

- III-B. Gérer la caméra (gestion des événements et utilisation d'un dockwidget)

- III-B-1. Contrôle du positionnement de la caméra par des spinbox

- III-B-2. Contrôle du positionnement de la caméra par le clavier

- III-B-3. Contrôle du positionnement de la caméra par la souris

- IV. Sélection d'entités

- V. Conclusion

- VI. Pour les utilisateurs Qt 4.5

- VII. Remerciements

I - Introduction

I-A - Pourquoi intégrer Ogre à Qt?

Interagir avec un jeu ou une scène 3D se fait couramment par le biais d'interfaces. L'utilisation majoritaire de ces interfaces est plutôt à chercher du côté de la conception d'interfaces in-game (tels les systèmes d'inventaires ou les menus que vous pouvez voir dans n'importe quel jeu). À ces interfaces, il faut ajouter ce que les concepteurs vont utiliser, c'est-à-dire des outils qui vont leur faciliter la création de scènes ou de niveaux, de matériaux, etc. Vous pouvez penser à l'éditeur Aurora de Neverwinter Nights, ou encore à l'Unreal Editor.
Ces deux besoins d'interfaces appellent deux réponses différentes. Pour le premier cas d'utilisation (interfaces in-game), on peut trouver des frameworks très performants, tel CEGUI, qui remplissent parfaitement leur rôle de framework d'interfaces dédiées aux menus de jeux. Cependant, pour des besoins plus lourds tels les éditeurs, les fonctionnalités fournies ne répondent plus vraiment aux besoins des concepteurs, et plus particulièrement des concepteurs d'outils. C'est pourquoi, afin de gagner en concision, en souplesse et afin de ne pas réinventer constamment la roue, il est indispensable d'intégrer le moteur graphique dans un framework de conception d'applications (tels Qt, WxWidgets, gtkmm, .Net etc.).
Ce tutoriel s'appliquera, vous l'aurez deviné, à l'intégration du moteur 3D Ogre dans Qt. Plus que l'intégration, nous verrons par la suite quelques cas standards (bien que basiques) d'utilisation.

I-B - Prérequis

- Une installation de Qt 4 fonctionnelle.

- une installation d'Ogre3D fonctionnelle (tuto réalisé avec la 1.4.5 et 1.4.8).

- En plus des bases en Qt (signaux/slots, création de widgets), il est utile de connaître un minimum Ogre.

Avant d'entrer plus loin dans ce sujet, voici les tutoriels qu'il est bon d'avoir suivis. Les mécanismes présentés ici s'appuient sur des concepts basiques, mais les mécanismes spécifiques à Qt et Ogre seront assez peu abordés :

- Tutoriels Ogre sur DVP ;

- Tutoriel avancé n° 2 et n° 3 sur le Wiki Ogre (utilisation de RaySceneQuery utilisé dans la partie IV de ce tuto).

II - Initialiser le système

L'objectif de cette section sera d'obtenir un widget affichant effectivement ce que Ogre dessine. À la fin nous aurons donc un écran noir ainsi que la possibilité de changer la couleur de fond de notre viewport.
L'approche que je vais aborder ici est extrêmement simple, dans un souci de réduire le code montrant l'essentiel. Nous allons créer un widget stockant l'intégralité du système Ogre (un objet root, scene manager, etc.). Il est évident que dans un vrai code il est de loin préférable de découpler tout ceci, mais ce ne devrait pas être un problème une fois que vous aurez compris les principes. Il y a toutefois une précaution à prendre avant d'entrer dans le sujet. N'importez jamais l’espace de noms Ogre dans le’espace de noms global. Faire ceci vous mènera droit à des conflits de types définis en plusieurs endroits (par Ogre et Qt donc). Après vous avoir montré l'en-tête de la classe, je détaillerai les méthodes étape par étape.

En-tête utilisé pour l'initialisation du système

class OgreWidget : public QWidget
{
Q_OBJECT

public:
OgreWidget(QWidget *parent = 0);
~OgreWidget();

public slots:
void setBackgroundColour(QColor c);

protected:
virtual void paintEvent(QPaintEvent *e);
virtual void resizeEvent(QResizeEvent *e);
virtual void showEvent(QShowEvent *e);

private:
void initOgreSystem();

private:
Ogre::Root         *ogreRoot;
Ogre::SceneManager *ogreSceneManager;
Ogre::RenderWindow *ogreRenderWindow;
Ogre::Viewport     *ogreViewport;
Ogre::Camera       *ogreCamera;
};

II-A - Objectif : écran noir (mais grâce à Ogre !)

Je vais détailler dans un premier temps la création du widget qui servira de réceptacle à l'affichage. Ce widget doit dériver de QWidget (surpris ? :)), et réimplémenter quelques méthodes ; le minimum étant showEvent, paintEvent, moveEvent et resizeEvent (toutes quatre sont des méthodes virtuelles protégées).
Ce widget devra avoir la propriété permettant de ne pas laisser le système dessiner un fond automatiquement : Qt::WA_OpaquePaintEvent et de ne pas laisser Qt gérer un double buffer : Qt::WA_PaintOnScreen.

OgreWidget::OgreWidget(QWidget *parent)
:QWidget(parent),
ogreRoot(0), ogreSceneManager(0), ogreRenderWindow(0), ogreViewport(0),
ogreCamera(0)
{
setAttribute(Qt::WA_OpaquePaintEvent);
setAttribute(Qt::WA_PaintOnScreen);
setMinimumSize(240,240);
}

Nous allons réimplémenter showEvent afin d'initialiser le système Ogre. En effet, son initialisation dépend du fait que nous ayons un handle système pour la fenêtre. Or cet handle n'est récupérable que lorsque la fenêtre est créée au niveau du système, et le moment le plus sûr est sa première apparition.

void OgreWidget::showEvent(QShowEvent *e)
{
if(!ogreRoot)
{
initOgreSystem(); // l'initialisation d'Ogre est détaillée plus bas
}

QWidget::showEvent(e);
}

paintEvent sera réimplémenté afin de demander un rafraîchissement du viewport d'Ogre. En effet, si Ogre n'est pas maître de la boucle d'événement (ce qui va être le cas), il faut demander explicitement la mise à jour des viewports utilisés. De plus, on informe le reste du moteur qu'une nouvelle frame est dessinée.

void OgreWidget::paintEvent(QPaintEvent *e)
{
ogreRoot->_fireFrameStarted();
ogreRenderWindow->update();
ogreRoot->_fireFrameEnded();

e->accept();
}

Il est aussi nécessaire de réimplémenter resizeEvent et moveEvent afin de signifier à Ogre un changement au niveau de la résolution, et prendre en compte le nouvel aspect pour la caméra.

void OgreWidget::moveEvent(QMoveEvent *e)
{
QWidget::moveEvent(e);

if(e->isAccepted() && ogreRenderWindow)
{
ogreRenderWindow->windowMovedOrResized();
update();
}
}

void OgreWidget::resizeEvent(QResizeEvent *e)
{
QWidget::resizeEvent(e);

if(e->isAccepted())
{
const QSize &newSize = e->size();
if(ogreRenderWindow)
{
ogreRenderWindow->resize(newSize.width(), newSize.height());
ogreRenderWindow->windowMovedOrResized();
}
if(ogreCamera)
{
Ogre::Real aspectRatio = Ogre::Real(newSize.width()) / Ogre::Real(newSize.height());
ogreCamera->setAspectRatio(aspectRatio);
}
}
}

Passons maintenant à l'initialisation d'Ogre. Si vous avez déjà programmé avec Ogre sans utiliser le framework d'exemple, vous vous y retrouverez très certainement. Dans le cas contraire, il y aura quelques différences avec un développement se basant sur ledit framework. Nous allons nous-mêmes créer un objet Ogre::Root, ainsi que le Ogre::RenderingSystem et le Ogre::SceneManager. L'ensemble du code concernant l'initialisation d'Ogre est celui de la fonction OgreWidget::initOgreSystem().
La création de Ogre::Root est simplissime. Nous utiliserons ici le constructeur par défaut. Il faudra donc que vous fournissiez les fichiers « ogre.cfg » (bien que le contenu de celui-ci ne soit pas utilisé et n'est donc pas indispensable physiquement) et « plugins.cfg » (celui-ci reste par contre indispensable).

void OgreWidget::initOgreSystem()
{
ogreRoot = new Ogre::Root();

Le RenderSystem se crée en demandant à root une instance d'un rendering manager correspondant à un nom donné. Ici, j'utiliserai directement le nom du rendersystem opengl pour rester cross platform et que nous n'ayons pas à proposer une liste de rendering system. Lors de l'appel à Root::initialise, notez que nous spécifions que nous ne voulons pas qu'Ogre crée une fenêtre automatiquement.
Si vous voulez le faire dans votre propre outil, il vous faudra en récupérer la liste grâce à Ogre::Root::getAvailableRenderers.

Ogre::RenderSystem *renderSystem = ogreRoot->getRenderSystemByName("OpenGL Rendering Subsystem");
ogreRoot->setRenderSystem(renderSystem);
ogreRoot->initialise(false);

Il n'y a pas de difficulté particulière au niveau de la création du scene manager. Ici encore, je code en dur le nom d'un SM standard afin de simplifier le code d'exemple. Pour proposer à l'utilisateur tous les SM disponibles, il vous faudra utiliser Ogre::SceneManagerEnumerator. Contrairement aux rendersystems, il vous faudra obtenir un itérateur sur les métadonnées des SceneManager, grâce à Ogre::SceneManagerEnumerator::getMetaDataIterator. Une fois déréférencé, cet itérateur vous permettra d'obtenir un Ogre::SceneManagerMetaData qui contiendra tout ce qui permet de décrire un SceneManager.

ogreSceneManager = ogreRoot->createSceneManager(Ogre::ST_GENERIC);

Vient maintenant le moment d'embarquer une vue Ogre au sein d'une fenêtre Qt : la création d'une Ogre::RenderWindow. Comme pour tous les objets vus précédemment, c'est sur demande à l'objet Root qu'une renderwindow est instanciée. Je vous encourage à jeter un œil à la doc de cette méthode. Nous allons donc devoir remplir une NameValuePairList dans laquelle nous spécifierons l'attribut externalWindowHandle, et bien sûr nous ne demanderons pas un affichage fullscreen.

Ogre::NameValuePairList viewConfig;
size_t widgetHandle;
#ifdef Q_WS_WIN
widgetHandle = (size_t)((HWND)winId());
#else
QWidget *q_parent = dynamic_cast <QWidget *> (parent());
QX11Info xInfo = x11Info();

widgetHandle = Ogre::StringConverter::toString ((unsigned long)xInfo.display()) +
":" + Ogre::StringConverter::toString ((unsigned int)xInfo.screen()) +
":" + Ogre::StringConverter::toString ((unsigned long)q_parent->winId());
#endif
viewConfig["externalWindowHandle"] = Ogre::StringConverter::toString(widgetHandle);
ogreRenderWindow = ogreRoot->createRenderWindow("Ogre rendering window",
width(), height(), false, &viewConfig);

Il ne reste plus après qu'à créer une caméra (sur demande au SceneManager) et un viewport (sur demande auprès de l'objet RenderWindow nouvellement créé).

ogreCamera = ogreSceneManager->createCamera("myCamera");

ogreViewport = ogreRenderWindow->addViewport(ogreCamera);
ogreViewport->setBackgroundColour(Ogre::ColourValue(0,0,0));
ogreCamera->setAspectRatio(Ogre::Real(width()) / Ogre::Real(height()));
}

II-B - Première interaction : modification de la couleur de fond

Les lecteurs attentifs auront remarqué dans l'en-tête la présence d'un slot setBackgroundColour(QColor c). J'ai choisi QColor comme type afin de faciliter l'intégration avec le reste de Qt, et tout particulièrement QColorDialog. Le seul problème de cette boîte de dialogue est qu'elle ne nous permettra pas de mettre à jour la vue en temps réel (elle est exécutée de façon modale et n'émet pas de signal correspondant à un changement de couleur). Mais à titre d'exemple, cela suffira amplement. L'intérêt du slot est nul dans ce cas de figure, mais si vous faites votre propre boîte de dialogue non modale, simplement avoir un slot à connecter à un signal sera un plaisir !
Il y a une chose utile à connaître ici : QColor stocke et permet d'obtenir la couleur grâce à un seul appel sous le format AARRGGBB. Ogre nous permet de spécifier une couleur dans ce format, cela nous évite donc de récupérer et spécifier chaque canal.

void OgreWidget::setBackgroundColour(QColor c)
{
if(ogreViewport)
{
Ogre::ColourValue ogreColour;
ogreColour.setAsARGB(c.rgba());
ogreViewport->setBackgroundColour(ogreColour);
}
}

Pour profiter de tout ça, il ne vous reste plus qu'à construire une QMainWindow avec notre OgreWidget pour widget principal. Pour spécifier la couleur, vous pouvez créer une action dont l'activation affichera une QColorDialog, et vous permettra d'en récupérer la valeur pour la spécifier à l'instance de OgreWidget.
Une archive contenant tout le code nécessaire à cette partie est disponible (miroir http). Prenez simplement garde à configurer les fichiers qt_ogre.pro et plugins.cfg selon vos besoins, et n'oubliez pas de le mettre dans le répertoire d'exécution de votre application.

III - Affichage d'un objet et gestion de la caméra

Le but à la fin de cette section sera d'être capable de gérer la position et l'orientation de la caméra à la souris, au clavier ou par des spinbox placées dans un dock widget. La première partie est dédiée à avoir un objet qui s'affiche. Le gros du travail sera fait dans la partie 2 où j'aborderai chacune des méthodes d'entrées.

III-A - Ajouter de la lumière et un objet

Au code présenté ci-dessus, il va nous falloir ajouter une fonction qui va initialiser les ressources (pour l'objet et son matériau) ainsi qu'une fonction qui va créer la scène. La fonction initialisant les ressources n'est pas très intéressante en elle-même, il vous suffit de reprendre le code du framework d'exemple (ExempleApplication::setupResources dans le fichier ExampleApplication.h). Nous pourrions utiliser QSettings pour lire le fichier, mais il n'y a pas grand-chose à y gagner ici. La scène créée sera très simple : il s'agit juste d'une lumière ambiante et d'un objet non animé.
Le chargement des ressources et la création de la scène se font à la fin de notre fonction d'initialisation. Ce sont des manipulations purement liées à Ogre et l'intégration dans un toolkit graphique n'a pas la moindre importance. Vous devriez obtenir l'image suivante en compilant et en exécutant l'archive fournie ici (miroir http). Dans cette dernière, j'ai ajouté le besoin du plugin Cg ainsi que les matériaux et objets nécessaires.

III-B - Gérer la caméra (gestion des événements et utilisation d'un dockwidget)

Cette partie va vous permettre de voir plusieurs façons de manipuler un objet. Ici, nous allons voir comment déplacer la caméra de trois façons différentes :

- utilisation de spinbox ;

- utilisation du clavier ;

- utilisation de la souris.

Une fois que nous aurons vu le positionnement, vous ne devriez pas avoir de problème pour implémenter l'orientation. La première modification à apporter concerne la classe OgreWidget. En effet, quelle que soit la méthode, nous allons devoir communiquer par le biais d'un signal la nouvelle position de la caméra. Le but ici est de conserver les spinbox synchronisées avec la position de la caméra, quelle que soit la méthode qui sert au déplacement. Voici la signature dudit signal :

signals:
void cameraPositionChanged(const Ogre::Vector3 &pos);

III-B-1 - Contrôle du positionnement de la caméra par des spinbox

Le but ici est d'avoir trois QDoubleSpinBox qui nous permettront de spécifier la position sur chacun des trois axes de la caméra.
Afin de simplifier ce contrôle, mais aussi dans le but de pouvoir le réutiliser pour autre chose que la caméra, vous pouvez créer un widget qui regroupera trois de ces spinbox (la classe Coordinate3DModifier dans l'exemple). Ce widget s'est vu doté d'un signal coordinateChanged(const Ogre::Vector3&) qui est émis à chaque modification de la valeur de l'une des spinbox. La fonction suivante est donc ce slot appelé à chaque modification de la valeur de l'une des spinbox :

void Coordinate3DModifier::onCoordinateChanged()
{
Ogre::Vector3 newCoord(sbx->value(),
sby->value(),
sbz->value());
emit coordinateChanged(newCoord);
}

Ce signal est connecté à un slot qui a été créé dans notre OgreWidget dont voici le code :

void OgreWidget::setCameraPosition(const Ogre::Vector3 &pos)
{
ogreCamera->setPosition(pos);
ogreCamera->lookAt(0,50,0);
update();
emit cameraPositionChanged(pos);
}

Dans ce slot, nous repositionnons la caméra à l'endroit désiré, tout en lockant l'orientation vers le centre (approximativement) du robot (le modèle utilisé dans les exemples fournis). Nous demandons ensuite à mettre à jour l'affichage de notre widget, ce qui entraîne le rendu d'une frame par Ogre. Nous finissons par émettre la nouvelle position de la caméra à qui sera intéressé.

J'ai précisé plus haut que, quelle que soit la méthode de déplacement, il faut que les spinbox indiquant les coordonnées de la caméra restent synchrones avec sa position réelle. Nous allons donc connecter le signal OgreWidget::cameraPositionChanged à un slot qui s'occupera de mettre à jour lesdites spinbox. Exemple tiré du code fourni plus bas :

void Coordinate3DModifier::setNewCoordinate(const Ogre::Vector3 &coordinate)
{
blockSignals(true);
sbx->setValue(coordinate.x);
sby->setValue(coordinate.y);
sbz->setValue(coordinate.z);
blockSignals(false);
}

Je tiens à souligner l'utilisation des méthodes QObject::blockSignals. En effet, un appel à QDoubleSpinBox::setValue entraîne l'émission d'un signal QDoubleSpinBox::valueChanged. Ce signal entraîne l'exécution de notre slot Coordinate3DModifier::onCoordinateChanged. Ce dernier entraîne l'émission du signal Coordinate3DModifier::coordinateChanged et donc l'exécution du slot OgreWidget::setCameraPosition. Ce dernier va émettre le signal OgreWidget::cameraPositionChanged, lequel entraîne l'exécution du slot Coordinate3DModifier::setNewCoordinate. Si nous ne bloquons pas immédiatement tout déclenchement de signal, je vous laisse le soin d'imaginer la tête qu'aura la pile d'appels (enfin, le peu de temps qu'elle et le programme vont exister).
Il est donc assez important de documenter le fait que la modification des coordonnées par le slot Coordinate3DModifier::setNewCoordinate n'entraîne pas l'émission du signal Coordinate3DModifier::coordinateChanged. Mais que pour rester à l'écoute de la position de la caméra, il vaut mieux se connecter directement au signal fourni par OgreWidget.

Voici le résultat obtenu à la fin de cette partie :

Une archive contenant tout le code nécessaire à cette partie est disponible (miroir http).

III-B-2 - Contrôle du positionnement de la caméra par le clavier

Ici, nous allons voir un autre moyen de contrôler la position de la caméra. Le but est que si la touche reste enfoncée, le mouvement doit être continuellement répété. Les touches utilisées seront standard (désolé pour ceux qui utilisent un clavier QWERTY par contre, il vous faudra adapter) :

- Z et S serviront à se déplacer en -Z et +Z respectivement ;

- Q et D pour -X et +X respectivement ;

- Pg Down et Pg Up pour -Y et +Y respectivement.

L'implémentation de cette méthode demande deux choses :

- définir une police de focus pour OgreWidget de façon à ce que Qt transmette les événements ;

- réimplémenter keyPressEvent.

La spécification de la police de focus est triviale : il s'agit juste d'un appel dans le constructeur de OgreWidget. Les flags à passer dépendent de ce que vous voulez assigner comme focus. Dans cet exemple, j'utiliserai simplement la prise de focus lors d'un clic de la souris dans le widget (pour les autres types de prises de focus possibles, je vous renvoie à la liste des Qt::FocusPolicy). Le constructeur d'OgreWidget se présente ainsi à cette étape :

OgreWidget::OgreWidget(QWidget *parent)
:QWidget(parent),
ogreRoot(0), ogreSceneManager(0), ogreRenderWindow(0), ogreViewport(0),
ogreCamera(0)
{
setAttribute(Qt::WA_OpaquePaintEvent);
setAttribute(Qt::WA_PaintOnScreen);
setMinimumSize(240,240);

setFocusPolicy(Qt::ClickFocus);
}

Quant au déplacement proprement dit, tous les déplacements sont codés en dur, toujours à titre d'exemple. Le principe est assez facile. Trop pour une utilisation sérieuse : nous stockons sous forme de map une correspondance entre une touche et un vecteur, et nous ne nous déplaçons que sur un seul axe à la fois (celui correspondant à la dernière touche pressée). La map en question contient donc un keycode allié à un vecteur de déplacement qui est relatif à la position actuelle de la caméra. Ce vecteur est ensuite ajouté à la position actuelle de la caméra et est ensuite transmis à la fonction OgreWidget::setCameraPosition afin de continuellement verrouiller l'orientation de la caméra vers le point qui a été spécifié, demander un rafraîchissement, ainsi qu'émettre la nouvelle position de la caméra (ceci dans le but de garder les spinbox indiquant la position synchrone avec la position).
Voici le code de la fonction permettant ceci :

void OgreWidget::keyPressEvent(QKeyEvent *e)
{
static QMap<int, Ogre::Vector3> keyCoordModificationMapping;
static bool mappingInitialised = false;

if(!mappingInitialised)
{
keyCoordModificationMapping[Qt::Key_Z]           = Ogre::Vector3( 0, 0,-5);
keyCoordModificationMapping[Qt::Key_S]           = Ogre::Vector3( 0, 0, 5);
keyCoordModificationMapping[Qt::Key_Q]           = Ogre::Vector3(-5, 0, 0);
keyCoordModificationMapping[Qt::Key_D]        = Ogre::Vector3( 5, 0, 0);
keyCoordModificationMapping[Qt::Key_PageUp]   = Ogre::Vector3( 0, 5, 0);
keyCoordModificationMapping[Qt::Key_PageDown] = Ogre::Vector3( 0,-5, 0);

mappingInitialised = true;
}

QMap<int, Ogre::Vector3>::iterator keyPressed =
keyCoordModificationMapping.find(e->key());
if(keyPressed != keyCoordModificationMapping.end() && ogreCamera)
{
const Ogre::Vector3 &actualCamPos = ogreCamera->getPosition();
setCameraPosition(actualCamPos + keyPressed.value());

e->accept();
}
else
{
e->ignore();
}
}

Une archive contenant tout le code nécessaire à cette partie est disponible (miroir http).

III-B-3 - Contrôle du positionnement de la caméra par la souris

La dernière méthode de positionnement de la caméra est donc l'utilisation de la souris. Voici le contrôle décidé :

- roulette vers l'avant : déplacement vers -Z ;

- roulette vers l'arrière : déplacement vers +Z ;

- LMB et déplacement vers la gauche : déplacement vers -X ;

- LMB et déplacement vers la droite : déplacement vers +X ;

- LMB et déplacement vers le haut : déplacement vers +Y ;

- LMB et déplacement vers le bas : déplacement vers -Y.

Si vous appuyez sur la touche Ctrl en même temps, le déplacement sera multiplié par 10 (défini comme la constante de classe turboModifier). Il va nous falloir réimplémenter quatre fonctions pour ce faire :

- wheelEvent ;

- mouseMoveEvent ;

- mousePressEvent ;

- mouseReleaseEvent.

Dans un premier temps, nous allons nous occuper de la roulette (peu importe laquelle, nous ne tenons pas compte de plusieurs roulettes). Le réglage du code ci-dessous permet de se déplacer de deux unités à chaque pas de roulette franchi (bien qu'avec certaines souris, il soit possible que ce soit une autre valeur). Pour régler vous-même la sensibilité, je vous renvoie à la documentation de la fonction QWheelEvent::delta(). La façon de procéder reste similaire au déplacement par le clavier, à savoir la création d'un vecteur de translation relatif à la position actuelle, puis addition de ce vecteur à la position actuelle de la caméra pour en définir la nouvelle position :

void OgreWidget::wheelEvent(QWheelEvent *e)
{
Ogre::Vector3 zTranslation(0,0, -e->delta() / 60);

if(e->modifiers().testFlag(Qt::ControlModifier))
{
zTranslation.z *= turboModifier;
}

const Ogre::Vector3 &actualCamPos = ogreCamera->getPosition();
setCameraPosition(actualCamPos + zTranslation);

e->accept();
}

Maintenant, nous allons aborder le déplacement sur les axes XY en maintenant le bouton gauche de la souris enfoncé et en déplaçant la souris. Ceci nécessite l'utilisation d'une variable qui va stocker le dernier point connu du mouvement de la souris. Il est égal à QPoint(-1,-1) (ce que j'ai défini ici comme point invalide) au début du programme et à chaque fois que l'utilisateur relâche le bouton gauche de la souris (passage dans OgreWidget::mouseReleaseEvent). Lors de l'appui sur le bouton gauche (OgreWidget::mousePressEvent), nous stockons la position de la souris. Ensuite, à chaque passage dans la fonction OgreWidget::mouseMoveEvent, il nous suffit de récupérer le delta entre ces deux points, d'éventuellement le multiplier par 10 si la touche Ctrl est enfoncée, et de définir le vecteur de translation. Ce dernier est, comme à l'accoutumée, ajouté à la position actuelle de la caméra pour en définir une nouvelle. Le code nécessaire est présenté ici (oldPos est la variable stockant l'ancienne position du curseur) :

void OgreWidget::mouseMoveEvent(QMouseEvent *e)
{
if(e->buttons().testFlag(Qt::LeftButton) && oldPos != invalidMousePoint)
{
const QPoint &pos = e->pos();
Ogre::Real deltaX = pos.x() - oldPos.x();
Ogre::Real deltaY = pos.y() - oldPos.y();

if(e->modifiers().testFlag(Qt::ControlModifier))
{
deltaX *= turboModifier;
deltaY *= turboModifier;
}

Ogre::Vector3 camTranslation(deltaX, deltaY, 0);
const Ogre::Vector3 &actualCamPos = ogreCamera->getPosition();
setCameraPosition(actualCamPos + camTranslation);

oldPos = pos;
e->accept();
}
else
{
e->ignore();
}
}

void OgreWidget::mousePressEvent(QMouseEvent *e)
{
if(e->buttons().testFlag(Qt::LeftButton))
{
oldPos = e->pos();
e->accept();
}
else
{
e->ignore();
}
}

void OgreWidget::mouseReleaseEvent(QMouseEvent *e)
{
if(!e->buttons().testFlag(Qt::LeftButton))
{
oldPos = QPoint(invalidMousePoint);
e->accept();
}
else
{
e->ignore();
}
}

Une archive contenant tout le code nécessaire à cette partie est disponible (miroir http).

IV - Sélection d'entités

Le but de cette partie est d'arriver à sélectionner notre robot en double cliquant dessus avec le bouton gauche, et à le désélectionner en double cliquant ailleurs que dans le volume de sa bounding box. Petit rappel sur le mécanisme : Ogre permet ceci par un RaySceneQuery qui collecte toute géométrie dont la bounding box est traversée par un rayon. Nous allons construire le rayon grâce à la caméra qui attend des coordonnées x et y comprises entre 0 et 1 (c'est-à-dire le ratio entre la position « physique » et la largeur de la fenêtre). Il ne nous reste plus alors qu'à vérifier que le rayon a bien intersecté un MovableObject, auquel cas, ce ne peut être que notre robot. Il ne reste plus qu'à récupérer la SceneNode associée, et à lui demander d'afficher la bounding box qui lui est associée. Si aucune intersection n'a lieu, nous désélectionnons l'objet SceneNode précédemment sélectionné. Un petit rafraîchissement de l'affichage à la fin de la fonction, et le tour est joué. La construction des coordonnées relatives se fait simplement en prenant la position du clic et en divisant par la largeur puis la hauteur.
Voici le code de cette partie (selectedNode est un Ogre::SceneNode membre de la classe qui n'est manipulée qu'ici) :

void OgreWidget::mouseDoubleClickEvent(QMouseEvent *e)
{
if(e->buttons().testFlag(Qt::LeftButton))
{
Ogre::Real x = e->pos().x() / (float)width();
Ogre::Real y = e->pos().y() / (float)height();

Ogre::Ray ray = ogreCamera->getCameraToViewportRay(x, y);
Ogre::RaySceneQuery *query = ogreSceneManager->createRayQuery(ray);
Ogre::RaySceneQueryResult &queryResult = query->execute();
Ogre::RaySceneQueryResult::iterator queryResultIterator = queryResult.begin();

if(queryResultIterator != queryResult.end())
{
if(queryResultIterator->movable)
{
selectedNode = queryResultIterator->movable->getParentSceneNode();
selectedNode->showBoundingBox(true);
}
}
else
{
selectedNode->showBoundingBox(false);
selectedNode = 0;
}

ogreSceneManager->destroyQuery(query);

update();
e->accept();
}
else
{
e->ignore();
}
}

Une archive contenant tout le code nécessaire à cette partie est disponible (miroir http).

V - Conclusion

Bravo si vous avez eu le courage de lire jusqu'ici, ce tuto n'est pas des plus courts ! :)
J'espère avoir ici réussi à faire le tour des principales méthodes pour vous permettre de faire cohabiter Qt et Ogre. Comme je l'ai précisé au début, cette approche n'est pas idéale dans le cadre d'un vrai programme. Son aspect monolithique sera très vite paralysant, et les interactions possibles se densifieront avec les features. De plus, il est impossible avec ce code d'avoir plusieurs widgets affichant la même scène.

VI - Pour les utilisateurs Qt 4.5

Avec Qt 4.5, le code de ce tuto entraîne un flickering. Le fix consiste à surcharger la méthode paintEngine() dans le widget Ogre de cette façon :

QPaintEngine *OgreWidget:: paintEngine() const
{
return 0;
}

VII - Remerciements

Merci à Trolltech et les développeurs Ogre pour leur fantastique toolkit respectif.
Ainsi qu'à Raptor70, Loka, Alp et Mongaulois et Diogene pour leurs relectures.

Merci à Caesius d'avoir donné l'info concernant Qt 4.5 (origine: forum officiel Ogre).