IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Integrer Ogre à Qt - Partie 2


précédentsommairesuivant

I. Une autre gestion des événements

I-1. Inconvénients de l'approche précédente et motif pour changer

A la fin de l'article précédent, nous pouvions déplacer la caméra par 3 méthodes différentes. Cependant, l'approche qui a été choisie n'est pas viable à long terme dû à une conception trop monolithique. En effet, simplement ajouter la possibilité de déplacer l'entité sélectionnée demanderait de complexifier les implémentations d'événements existants. Le résultat serait des fonctions particulièrement "bloatée", et au mieux un enchaînement de if ou un switch pour basculer entre divers gestionnaires d'événements selon le but à atteindre.
Nous allons changer cette approche pour une méthode plus souple. Cette méthode mettra en oeuvre le design pattern Strategie afin de découpler au maximum les réponses aux événements du widget en lui même. Cette technique va nous permettre de déléguer la gestion des événements du widget. Chaque manipulation du widget se verra isolée dans sa propre classe, et les interactions possibles en hériteront. Du point de vue de la classe OgreWidget, cela nécessite de garder à jour un pointeur sur la classe de base afin de lui transférer les événements (si délégué actif il y a; nous nous laissons la possibilité de ne pas avoir de gestion d'événements en cas de pointeur nul).
Pour plus d'infos sur le pattern Stratégie ainsi que des exemples en C++, je vous recommande de lire cette section d'un tutoriel de David Come.

I-2. Implémentation

I-2-1. Définition de l'interface nécessaire et modification au code actuel

Nous allons donc définir une classe indiquant toutes les fonctions requises à un gestionnaire d'entrées. Il s'agira dans notre cas des méthodes interagissant sur la scène, à l'exception du double clic (ce dernier est "réservé" à la (dé)sélection d'entité). La classe en question est déclarée ainsi:

 
Sélectionnez
class EventHandler
{
public:
    virtual bool keyPressEvent(QKeyEvent *e);
    virtual bool mouseMoveEvent(QMouseEvent *e);
    virtual bool mousePressEvent(QMouseEvent *e);
    virtual bool mouseReleaseEvent(QMouseEvent *e);
    virtual bool wheelEvent(QWheelEvent *e);
};

Ces méthodes ne sont pas sensées spécifier si l'événement est accepté ou non. Cette réponse est laissée au soin de notre widget qui indiquera l'acceptation de l'événement selon que la fonction renvoie true ou false. Vous l'aurez peut-être remarqué, EventHandler n'est pas une classe abstraite, mais une classe no-op. En effet, chacune des méthodes est implémentée ici pour retourner false. Ca permet de ne réimplémenter qu'une partie des fonctions dans les classes dérivées. Je me passerais donc ici de montrer l'implémentation (une série de return false n'étant pas des plus intéressante ;)).

Il va nous falloir maintenant intégrer ce type à OgreWidget. Pour ce faire, il faut commencer par ajouter une variable à cette classe sous la forme d'un pointeur; ceci nous permettra de changer le gestionnaire d'événements selon les besoins. Le corps des méthodes gérant les événements listés dans EventHandler est donc tous remplacé par un code similaire à celui-ci (aucun intérêt à tous les lister):

 
Sélectionnez
void OgreWidget::keyPressEvent(QKeyEvent *e)
{
    if(eventHandler && eventHandler->keyPressEvent(e))
    {
        e->accept();
    }
    else
    {
        e->ignore();
    }
}

La vérification sur l'existence d'un eventHandler (en supposant que vous assigniez bien vos pointeurs à 0 une fois supprimés) permet de facilement assigner à OgreWidget un EventHandler (ou classe dérivée) qui ne fait rien, sans pour autant assigner un EventHandler. Il nous faut aussi une méthode pour changer dynamiquement ce comportement, et c'est réalisé par la fonction OgreWidget::setEventHandler:

 
Sélectionnez
void OgreWidget::setEventHandler(EventHandler *newEventHandler)
{
    delete eventHandler;
    eventHandler = newEventHandler;
}

I-2-2. Implémentation du déplacement de la caméra

Après avoir apporté les modifications ci-dessus, nous ne pouvons plus que sélectionner et désélectionner le robot. Le but de cette section est donc de rétablir le comportement précédent. Cependant, en ayant délocalisé la gestion des entrées, nous n'avons plus la main sur l'ensemble des chemins possibles déclenchant une modification de la caméra. Il en résulte une impossibilité de garder la position synchrone avec les spinbox. Nous allons donc découpler un peu tout ça, et commencer par créer un wrapper pour la caméra. Nous n'allons redéfinir que les fonctions que nous allons utiliser ici, et en profiter pour implémenter des slots et signaux:

 
Sélectionnez
class MyCamera : public QObject
{
Q_OBJECT

public:
    MyCamera(Ogre::Camera &camera, QObject *parent = 0);
    
    Ogre::Camera* getOgreCamera() const;
    const Ogre::Vector3& getPosition() const;
    Ogre::Ray getCameraToViewportRay(Ogre::Real screenx, Ogre::Real screeny) const;

public slots:
    void setPosition(const Ogre::Vector3 &pos);
    void lookAt(const Ogre::Vector3 &targetPoint);
    void setAspectRatio(Ogre::Real ratio);
    
signals:
    void positionChanged(const Ogre::Vector3 &pos);
    void visiblePropertyChanged();

private:
    Ogre::Camera *ogreCamera;
};

Quelques explications sur cette classe restent à faire. Le constructeur prend une référence afin de rendre impossible le passage d'un pointeur null. Les setters sont implémentés comme des slots afin de faciliter le réglage par des widgets externes afin de ne pas encombrer la classe principale (ici OgreWidget) de code qui ne lui est pas spécifique. Le signal visiblePropertyChanged() est là pour signaler qu'un changement a eu lieu sur une propriété qui modifie potentiellement le rendu (changement de position, d'orientation, etc.. etc...). Les getters ne sont pas vraiment intéressants, ils se contentent de transmettre l'appel à la caméra Ogre. L'implémentation des setters par contre se présente ainsi:

 
Sélectionnez
void MyCamera::setPosition(const Ogre::Vector3 &pos)
{
    ogreCamera->setPosition(pos);
    lookAt(Ogre::Vector3(0,50,0));
    
    emit positionChanged(pos);
    emit visiblePropertyChanged();
}

void MyCamera::lookAt(const Ogre::Vector3 &targetPoint)
{
    ogreCamera->lookAt(targetPoint);
    emit visiblePropertyChanged();
}

void MyCamera::setAspectRatio(Ogre::Real ratio)
{
    ogreCamera->setAspectRatio(ratio);
    emit visiblePropertyChanged();
}

Après ces modifications, il va falloir nettoyer un peu la classe OgreWidget afin de ne pas laisser de code inutile. Les modifications consistent à:

  • Remplacer le pointeur Ogre::Camera* par un MyCamera*
  • Ajouter une méthode permettant d'obtenir le pointeur vers MyCamera associé à la vue
  • Supprimer le slot setCameraPosition ainsi que le signal cameraPositionChanged
  • Supprimer les variables liées à la gestion du déplacement de la caméra (oldPos, turboModifier et invalidMousePoint)
  • Et ajouter une méthode setupCamera() afin de séparer un peu la création de la caméra de l'initialisation dans un souci de propreté. Voici le code de cette méthode:
 
Sélectionnez
void OgreWidget::setupCamera()
{
    delete camera;
    camera = new MyCamera(*ogreSceneManager->createCamera("myCamera"), this);
    
    connect(camera, SIGNAL(visiblePropertyChanged()), this, SLOT(update()));
    
    camera->setPosition(Ogre::Vector3(0, 50,150));
    camera->lookAt(Ogre::Vector3(0,50,0));
    camera->setAspectRatio(Ogre::Real(width()) / Ogre::Real(height()));
}

Et la déclaration actuelle de OgreWidget est donc:

 
Sélectionnez
class OgreWidget : public QWidget
{
[...]    
    MyCamera *getCamera();
    void setEventHandler(EventHandler *newEventHandler);

[...]
    
private:
[...]
    void setupCamera();

private:
    Ogre::Root          *ogreRoot;
    Ogre::SceneManager  *ogreSceneManager;
    Ogre::RenderWindow  *ogreRenderWindow;
    Ogre::Viewport      *ogreViewport;
	
    EventHandler 		*eventHandler;
    MyCamera            *camera;
	
    Ogre::SceneNode     *selectedNode;
};

Il nous reste maintenant 2 étapes à franchir avant de pouvoir à nouveau déplacer la caméra: créer un event handler permettant d'agir sur la caméra, et modifier la classe MainWindow afin de prendre tout ceci en compte.
Nous allons commencer par créer la classe CameraEventHandler, qui va permettre de déplacer la caméra par les méthodes déjà présentées. Cette classe reprend tout simplement l'ancien corps des méthodes d'événements, à l'exception des event->accept() et event->ignore(). Je ne montre ici que la déclaration puisque l'implémentation est déjà connue. En cas de doute, n'hésitez pas à vous référer à l'archive indiquée en fin de paragraphe.

 
Sélectionnez
class CameraEventHandler : public EventHandler
{
public:
    CameraEventHandler(MyCamera *targetCamera);
	
    virtual bool keyPressEvent(QKeyEvent *e);
    virtual bool mouseMoveEvent(QMouseEvent *e);
    virtual bool mousePressEvent(QMouseEvent *e);
    virtual bool mouseReleaseEvent(QMouseEvent *e);
    virtual bool wheelEvent(QWheelEvent *e);
	
private:
    static const Ogre::Real turboModifier;
    static const QPoint invalidMousePoint;

private:
    QPoint oldPos;
    MyCamera *camera;
};

Nous allons finir avec les modifications à apporter à la classe MainWindow. Nous allons activer/désactiver le déplacement de la caméra par le biais d'un menu. Il nous faut donc créer une nouvelle action et l'ajouter au menu Divers. Lorsque l'état de cette action est basculé, ce sera le (nouveau) slot moveCamModeToggled(bool) qui sera exécuté. Selon l'état de l'action nous allons activer ou désactiver le déplacement de la caméra, afficher ou masquer le dock contenant notre widget modifiant des coordonnées et connecter les changements de coordonnées entre la caméra et le widget Coordinate3DModifier. Voici les méthodes qui ont changées:

 
Sélectionnez
    MainWindow()
    :ogreWidget(0)
    {
        ogreWidget = new OgreWidget;
        camPosModifier = new Coordinate3DModifier;
        
        createActionMenus();
        createDockWidget();
        
        setCentralWidget(ogreWidget);
    }
    
[...]

    void createActionMenus()
    {
        QAction *changeColorAct = new QAction("Changer la couleur de fond", this);
        connect(changeColorAct, SIGNAL(triggered()), this, SLOT(chooseBgColor()));
        
        QAction *moveCamModeAct = new QAction("Deplacement de la camera", this);
        moveCamModeAct->setCheckable(true);
        moveCamModeAct->setChecked(false);
        connect(moveCamModeAct, SIGNAL(toggled(bool)), this, SLOT(moveCamModeToggled(bool)));
        
        QAction *closeAct = new QAction("Quitter", this);
        connect(closeAct, SIGNAL(triggered()), this, SLOT(close()));
        
        QMenu *menu = menuBar()->addMenu("Divers");
        menu->addAction(changeColorAct);
        menu->addAction(moveCamModeAct);
        menu->addAction(closeAct);
    }
    
[...]

    
private slots:
[...]
    
    void moveCamModeToggled(bool on)
    {
        if(on)
        {
            MyCamera *cam = ogreWidget->getCamera();
            CameraEventHandler *camEventHandler = new CameraEventHandler(cam);
            ogreWidget->setEventHandler(camEventHandler);

            coordModifier->setNewCoordinate(cam->getPosition());
            coordModifierDock->setVisible(true);

            connect(coordModifier, SIGNAL(coordinateChanged(const Ogre::Vector3&)),
                    cam, SLOT(setPosition(const Ogre::Vector3&)));
            connect(cam, SIGNAL(positionChanged(const Ogre::Vector3&)),
                    coordModifier, SLOT(setNewCoordinate(const Ogre::Vector3&)));
        }
        else
        {
            ogreWidget->setEventHandler(0);
			
            coordModifierDock->setVisible(false);
            coordModifier->disconnect();
        }
    }

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

I-2-3. Implémentation du déplacement de l'objet sélectionné

Le processus à suivre suit de très près celui utilisé pour la caméra ; je plongerais donc un peu moins dans les détails.
A l'image de la caméra pour laquelle nous avons créé un wrapper, nous allons en créer un pour la classe Ogre::SceneNode. Il s'agit simplement d'une version "allégée" du wrapper de la caméra dont voici la déclaration:

 
Sélectionnez
class MySceneNode : public QObject
{
Q_OBJECT

public:
    MySceneNode(Ogre::SceneNode &node, QObject *parent = 0);
    
    Ogre::SceneNode* getOgreSceneNode() const;
    const Ogre::Vector3& getPosition() const;

public slots:
    void setPosition(const Ogre::Vector3 &pos);
    
signals:
    void positionChanged(const Ogre::Vector3 &pos);

private:
    Ogre::SceneNode *ogreSceneNode;
};

L'implémentation de cette classe est particulièrement triviale (il s'agit simplement de transférer les appels au SceneNode wrappé).
C'est cette classe qui va être utilisée pour stocker l'objet sélectionné. Nous allons commencer par mettre à jour le code de OgreWidget::mouseDoubleClickEvent() afin d'utiliser cette nouvelle classe. Voici le snippet concerné:

 
Sélectionnez
        if(queryResultIterator != queryResult.end())
        {
            if(queryResultIterator->movable)
            {
                Ogre::SceneNode *node = queryResultIterator->movable->getParentSceneNode();
                node->showBoundingBox(true);
                delete selectedNode;
                selectedNode = new MySceneNode(*node, this);
            }
        }
        else
        {
            if (selectedNode)
            {
                selectedNode->getOgreSceneNode()->showBoundingBox(false);
                delete selectedNode;
                selectedNode = 0;
            }
        }

Maintenant, il nous faut une dernière modification à OgreWidget afin de permettre à l'interface de déplacer l'objet. Nous ajoutons une méthode (OgreWidget::getSelectedSceneNode()) nous permettant d'obtenir l'objet sélectionné.
Passons maintenant à l'event handler à qui l'on délègue la gestion des événements pour déplacer le noeud concerné. Le comportement est semblable à l'event handler, à 3 exceptions près:

  • il n'y a pas de modificateur "turbo", et le déplacement se fait par incrément d'une unité,
  • le déplacement à la souris se fait sur le plan XZ, contrairement à la caméra où la translation se fait en XY,
  • les événements de la molette sont ignorés (SelectedNodeEventHandler::wheelEvent() renvoie false).

Il ne nous reste donc plus qu'à créer une entrée dans le menu afin de connecter tout ça. Cette action affichera le widget Coordinate3DModifier, mais cette fois, il affichera et permettra de mettre à jour la position du noeud sélectionné. Cette entrée de menu est mutuellement exclusive avec celle permettant de déplacer la caméra.
Pour implémenter cette exclusivité, nous allons utiliser un QActionGroup. Cette classe nous simplifiera la vie en nous permettant de ne pas nous soucier de désactiver l'event handler que nous aurions pu activer précédemment, ni de décocher l'entrée du menu correspondante. Voici le code qui fait ceci:

 
Sélectionnez
    QActionGroup *actionGroup = new QActionGroup(this);
    actionGroup->addAction(moveCamModeAct);
    actionGroup->addAction(moveSelNodeModeAct);

Oui oui, c'est tout :)
moveSelNodeModeAct est une action qui est créée de la même façon que moveCamNodeAct. Seul son texte ainsi que son slot change. Slot dont voici d'ailleurs le code:

 
Sélectionnez
void MainWindow::moveSelectedNodeModeToggled(bool on)
{
    if(on)
    {
        MySceneNode *selNode = ogreWidget->getSelectedSceneNode();
        if (!selNode)
        {
            moveSelNodeModeAct->setChecked(false);
            
            // On récurse afin de nettoyer correctement les signaux/slots et event handlers
            moveSelectedNodeModeToggled(false);
            return;
        }
        SelectedNodeEventHandler *selNodeEventHandler = new SelectedNodeEventHandler(selNode);
        ogreWidget->setEventHandler(selNodeEventHandler);

        coordModifier->disconnect();
        coordModifier->setNewCoordinate(selNode->getPosition());
        coordModifierDock->setVisible(true);
        coordModifierDock->setWindowTitle("Selected node position");

        connect(coordModifier, SIGNAL(coordinateChanged(const Ogre::Vector3&)),
                selNode, SLOT(setPosition(const Ogre::Vector3&)));
        connect(selNode, SIGNAL(positionChanged(const Ogre::Vector3&)),
                coordModifier, SLOT(setNewCoordinate(const Ogre::Vector3&)));
        connect(selNode, SIGNAL(positionChanged(const Ogre::Vector3&)),
                ogreWidget, SLOT(update()));
    }
    else
    {
        ogreWidget->setEventHandler(0);

        coordModifierDock->setVisible(false);
        coordModifier->disconnect();
    }
}

Nous commençons bien sûr par nous assurer qu'un objet est bel et bien sélectionné; dans le cas contraire nous décochons le menu pour signifier que la demande n'est pas acceptée. En l'état, vous pouvez commencer à tester, mais il nous reste un dernier problème à résoudre.
En effet, si vous sélectionnez un objet, que vous entrez en mode de déplacement d'objet, et que vous le désélectionnez, tout événement que nous traitons entraînera un crash puisque le sceneNode piloté est détruit par OgreWidget. Nous allons donc ajouter un signal à cette classe afin de notifier d'un changement de sélection. Voici le signal ajouté:

 
Sélectionnez
signals:
    void selectionChanged(MySceneNode *newSelectedNode);

Nous passons en paramètre le nouveau noeud par commodité bien qu'il n'y en ait pas d'utilité dans le cas présent. Il nous faut maintenant écrire un slot dans MainWindow afin de mettre à jour l'interface selon le besoin:

 
Sélectionnez
void MainWindow::selectionChanged(MySceneNode * /*newSelectedNode*/)
{
    if (moveSelNodeModeAct->isChecked())
        moveSelectedNodeModeToggled(true);
}

Ce slot se contente de mettre à jour l'event handler selon la sélection si nous sommes dans le mode de déplacement d'objet. Maintenant, nous émettons ce signal à tout changement de sélection (une petite modification de OgreWidget::mouseDoubleClickEvent() est nécessaire). Enfin, après avoir connecté le signal fourni par OgreWidget à MainWindow::selectionChanged(MySceneNode *), il ne nous reste plus qu'à compiler et exécuter notre petite application afin de constater que nous pouvons tour à tour modifier la position de la caméra et de l'objet sélectionné.

Le code illustrant ce paragraphe est disponible (miroir http).


précédentsommairesuivant

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 ni 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.