Aller au menu - Aller au contenu

Icône Des caméras et des animators

Avatar
Mise à jour : 23/03/2011
Difficulté : Facile Facile Creative Commons BY-SA
1 621 visites depuis 7 jours, dont 36 sur ce chapitre classé 80/786
Comme son nom l'indique, ce chapitre va être dédié aux caméras et aux animators. Nous n'en avons pas parlé jusqu'à maintenant mais ils sont en fait essentiels à tout un tas de choses. Les caméras FPS que nous ajoutons par exemple ne sont que des caméras ordinaires, c'est l'animator qui leur est associé qui les rend mobiles.

Ainsi la deuxième partie de ce chapitre va être dédiée à une petite application pratique avec la création d'une caméra trackball. Chose avec laquelle vous devriez être familier si vous avez suivi le tutoriel de Kayl sur OpenGL.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Fonctionnement des caméras

Comme vous le savez sans doute si vous avez lu attentivement le chapitre sur le rendu des scene nodes, n'importe quel scene node peut se faire associer un ou plusieurs animators. Ceux-ci servent comme leur nom l'indique à "animer" le scene node. Dans notre cas précis, à animer une caméra. Il n'existe qu'un seul type de caméra dans Irrlicht : CCameraSceneNode, et elle est statique. En revanche il existe 2 animators qui leurs sont spécifiques :
  • ISceneNodeAnimatorCameraFPS
  • ISceneNodeAnimatorCameraMaya

Pour vérifier tout cela, jetons un œil au contenu de la fonction ISceneManager::addCameraSceneNodeFPS :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ICameraSceneNode* CSceneManager::addCameraSceneNodeFPS(ISceneNode* parent,
	f32 rotateSpeed, f32 moveSpeed, s32 id,
	SKeyMap* keyMapArray, s32 keyMapSize, bool noVerticalMovement, f32 jumpSpeed, bool invertMouseY)
{
	if (!parent)
		parent = this;

	ICameraSceneNode* node = new CCameraSceneNode(parent, this, id);
	ISceneNodeAnimator* anm = new CSceneNodeAnimatorCameraFPS(CursorControl,
			rotateSpeed, moveSpeed, jumpSpeed,
			keyMapArray, keyMapSize, noVerticalMovement, invertMouseY);

	// Bind the node's rotation to its target. This is consistent with 1.4.2 and below.
	node->bindTargetAndRotation(true);

	node->addAnimator(anm);
	setActiveCamera(node);

	anm->drop();
	node->drop();

	return node;
}

Ligne 8, une caméra (statique donc) est créée et ligne 9, un animator FPS. Ligne 16 l'animator est ajouté à la caméra et ligne 17 celle-ci est définie comme caméra active. Et c'est tout. A partir de là, l'animator va prendre en charge tous les déplacements de la caméra. Lors d'un appui sur une touche ou d'un mouvement de la souris, c'est lui qui va traiter l'event, lui qui va calculer la nouvelle position de la caméra, et lui qui va la déplacer.

On peut tout de suite en déduire que l'animator accède d'une manière ou d'une autre aux events. En fait, la classe ISceneNodeAnimator descend directement de IEventReceiver, comme le montre le schéma en haut de la page de documentation.




Cheminement de l'information



Comme nous l'avons vu lors du chapitre sur les events, il existe plusieurs gestionnaires d'events qui se passent l'event capté les uns après les autres. Cet ordre est le suivant :

nomdescription
1 UserReceiver Définit par l'utilisateur
2 GUIEnvironment L'environnement de gui
3 inputReceiver Le scene manager



Celui qui nous intéresse est donc le numéro 3, le scene manager, qui gère les caméras. Le scene manager ne se sert de l'event que pour d'une chose : le passer à la caméra active. Comme nous pouvons le constater dans le code de cette fonction :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool CSceneManager::postEventFromUser(const SEvent& event)
{
	bool ret = false;
	ICameraSceneNode* cam = getActiveCamera();
	if (cam)
		ret = cam->OnEvent(event);

	_IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX;
	return ret;
}


Et la caméra active ne se sert de cet event que pour d'une chose elle aussi : le passer à ses animators :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
bool CCameraSceneNode::OnEvent(const SEvent& event)
{
    if (!InputReceiverEnabled)
	return false;

    // send events to event receiving animators

    core::list<ISceneNodeAnimator*>::Iterator ait = Animators.begin();
	
    for (; ait != Animators.end(); ++ait)
	if ((*ait)->isEventReceiverEnabled() && (*ait)->OnEvent(event))
            return true;

    // if nobody processed the event, return false
    return false;
}


La ligne 3 sert à vérifier que la caméra est bien sensée capter les events, puis le début de la ligne 11 vérifie que l'animator aussi est bien sensé y avoir accès. Si tout est bon alors l'animator reçoit enfin l'event qu'il peut traiter comme bon lui semble. Typiquement, il stocke les changements captés, et s'en sert un peu plus tard pour calculer la nouvelle position de la caméra.

Nous avons également vu lors du chapitre sur le rendu des scene nodes, que la méthode OnAnimate était appelée pour chacun d'entre eux. Cette fonction s'appelle à son tour dans tous les animators de ce scene node. Dans le cas d'une caméra, elle permet du coup à l'animator de calculer la nouvelle position de celle-ci et de la mettre à jour.

Réalisation d'une caméra trackball

Cette caméra s'inspire totalement de celle réalisée par Kayl dans ce chapitre. Je vous invite donc à en lire au moins la première partie avant de continuer.

Néanmoins, il y a quelques subtiles différences au niveau de l'implémentation entre la sienne et celle-ci. En effet même s'il nous est possible d'accéder directement aux matrices de l'API 3D via Irrlicht, c'est totalement inutile puisque celui-ci les reset à chaque début de rendu. Il va donc nous falloir modifier les coordonnées absolues de la caméra plutôt que d'appliquer une rotation à la matrice monde.

Avant se lancer dans des schémas et des équations qui paraitront obscures à certains, voici 2 chapitres qui vous permettront d'aborder la suite plus sereinement :


Comme dit plus haut, ce n'est pas "le monde" qui va pivoter mais la caméra qui va tourner autour de l'origine de celui-ci. Partant du principe que la distance entre la caméra et l'origine du repère ne change pas, quelque soit la position possible de la caméra, elle se situera sur une sphère ayant pour centre l'origine du repère et pour rayon la distance entre l'origine du repère et la caméra.

Ce qui signifie qu'en connaissant le rayon et deux angles indiquant la rotation de la caméra (les coordonées sphériques), il est possible d'en déduire la position de la caméra, en coordonnées absolues (schémas à l'appui). Comme vous l'aurez deviné, les déplacements de la souris vont directement faire varier ces deux angles, ce qui modifiera la position de la camera. Et dans la foulée, nous allons nous servir de la molette pour faire varier le rayon de la sphère. C'est à dire la distance entre la caméra et l'origine du repère.


Le détail de ces calculs est donné à la fin de ce chapitre lors de l'implémentation. Commençons plutôt par voir la déclaration de notre animator :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class CSceneNodeAnimatorCameraTrackball : public ISceneNodeAnimator
{

public :

    CSceneNodeAnimatorCameraTrackball(
        irr::video::IVideoDriver *driver,
        irr::gui::ICursorControl *cursorControl,
        irr::f32 radius,
        irr::f32 moveSpeed,
        irr::f32 rotateSpeed);

    virtual void animateNode(ISceneNode* node, u32 timeMs);
    virtual bool OnEvent(const SEvent& event);
    virtual ISceneNodeAnimator* createClone(ISceneNode* node, ISceneManager* newManager = 0);

    virtual void setMoveSpeed(f32 moveSpeed);
    virtual void setRotateSpeed(f32 rotateSpeed);

    virtual irr::f32 getMoveSpeed() const { return m_moveSpeed; }
    virtual irr::f32 getRotateSpeed() const { return m_rotateSpeed; }
    virtual bool isEventReceiverEnabled() const { return true; }


private :

    irr::video::IVideoDriver *m_driver;             // pointer to video driver
    irr::gui::ICursorControl *m_cursorControl;      // allow operations on cursor
    irr::f32 m_radius;                              // distance from target
    irr::f32 m_moveSpeed;                           // movements's speed to the target
    irr::f32 m_rotateSpeed;                         // speed of fake world rotation
    bool m_isPressed;                               // is the left mouse button clicked
    bool m_wheel;                                   // did the wheel moved
    irr::core::position2d<irr::f32> m_cursorMove;   // contain changes on cursor position
    irr::core::position2d<irr::f32> m_cursorPos;    // contain the cursor position
    irr::core::vector3df m_rotation;                // contain changes on camera position
};


Les trois quarts des fonctions (toutes à part les accesseurs en fait) sont directement là pour redéfinir les classes mères. OnEvent est bien entendue héritée de IEventReceiver et va nous permettre de capter les events pour modifier les deux angles (les coordonnées sphériques, stockées dans m_rotation). animateNode va justement nous permettre de calculer (si besoin) les nouvelles coordonnées absolues à chaque rendu de la caméra.

createClone est une fonction déclarée dans ISceneNodeAnimator et qui permet comme son nom l'indique de cloner l'instance de l'animator. Comme nous l'avons vu plus haut, il est indispensable de redéfinir isEventReceiverEnabled pour indiquer au node auquel est attaché l'animator, que celui-ci doit bien recevoir les events.

Implémentation de la caméra

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
irr::scene::CSceneNodeAnimatorCameraTrackball::CSceneNodeAnimatorCameraTrackball(
    irr::video::IVideoDriver *driver,
    irr::gui::ICursorControl *cursorControl,
    irr::f32 radius,
    irr::f32 moveSpeed,
    irr::f32 rotateSpeed)
    : m_driver(driver),
    m_cursorControl(cursorControl),
    m_radius(radius),
    m_moveSpeed(moveSpeed),
    m_rotateSpeed(rotateSpeed),
    m_isPressed(false),
    m_wheel(false)
{
    m_rotation = irr::core::vector3df(1.0f,1.0f,0.0f);
    m_cursorPos = m_cursorControl->getRelativePosition();
    m_cursorMove = irr::core::position2d<irr::f32>(0,0);
}

Rien de bien affolant pour le constructeur. On se contente de passer tous les arguments aux attributs respectifs, puis on initialise la position du curseur et l'attribut qui contient les changements de cette position. Notez tout de même qu'on place des valeurs arbitraires dans le vecteur m_rotation. Nous reviendrons dessus plus loin.


Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#define MAXANGLE 1.3


bool irr::scene::CSceneNodeAnimatorCameraTrackball::OnEvent(const SEvent& event)
{
    if(event.EventType == EET_MOUSE_INPUT_EVENT) {
        if (event.MouseInput.Event == irr::EMIE_LMOUSE_PRESSED_DOWN) {
            m_isPressed = true;
	    return true;
	}
	else if (event.MouseInput.Event == irr::EMIE_LMOUSE_LEFT_UP) {
            m_isPressed = false;
            return true;
	}
	// change the radius/distance according to wheel moves
	else if (event.MouseInput.Event == irr::EMIE_MOUSE_WHEEL) {
            if ( event.MouseInput.Wheel > 0 ) { m_radius -= 1.0f * m_moveSpeed; }
            else { m_radius += 1.0f * m_moveSpeed; }
            m_wheel = true;
            return true;
	}
	else if (event.MouseInput.Event == EMIE_MOUSE_MOVED) {
            // if the mouse moves without the left button is pressed,
            // we just update the position. Camera doesn't move
            if (m_isPressed == false) {
                m_cursorPos = m_cursorControl->getRelativePosition();
                return true;
            }
            // if the left button is pressed, then we get the change and
            // set the rotation vector. And update the position
            else if (m_isPressed == true) {
                m_cursorMove = m_cursorControl->getRelativePosition() - m_cursorPos;
                m_rotation.X += m_cursorMove.X * m_rotateSpeed;
                m_rotation.Y += m_cursorMove.Y * m_rotateSpeed;
                // check the Y change to prevent vertical looping
                if (m_rotation.Y >= MAXANGLE) { m_rotation.Y = MAXANGLE; }
                else if (m_rotation.Y <= -MAXANGLE) { m_rotation.Y = -MAXANGLE; }
                m_cursorPos = m_cursorControl->getRelativePosition();
                return true;
            }
	}
    }
    return false;   // event not proceeded
}

Voilà une fonction bien plus intéressante. Les lignes 7 à 14 servent juste à paramétrer le booléen m_isPressed qui permet de savoir si le clic gauche de la souris est actuellement enfoncé. Les lignes 15 à 21 permettent de modifier le radius de la sphère, donc la distance entre la caméra et l'origine du repère en fonction des mouvements de la molette de la souris. C'est à ce moment que m_moveSpeed est pris en compte pour modifier la "vitesse de déplacement". Notez au passage que le booléen m_wheel passe à 1 au moindre changement. Nous verrons pourquoi dans la prochaine fonction.

Lignes 25 à 28, on se contente de mettre à jour notre version de la position du curseur si le booléen m_isPressed est faux, pour éviter de prendre en compte un changement non voulu. Et enfin les dernières lignes servent à effectuer les changements en cas de déplacement de la souris alors que le clic gauche est enfoncé. Ces changements sont stockés dans le vecteur 3D m_rotation. Petite subtilité toutefois, nous vérifions lignes 36 et 37 que le changement à appliquer à l'angle vertical n'est pas supérieur à une certaine valeur (basée sur la moitié de PI si vous voulez tout savoir) pour éviter d'éventuels looping verticaux qui mettraient à mal notre manière de calculer les coordonnées absolues de la caméra. Là encore, un coefficient (m_rotateSpeed) est pris en compte et permet de déterminer la "vitesse de rotation" de la caméra grâce à un argument du constructeur.


Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void irr::scene::CSceneNodeAnimatorCameraTrackball::animateNode(ISceneNode* node, u32 timeMs)
{
    // check the node is a camera
    if (!node || node->getType() != ESNT_CAMERA) { return; }
    ICameraSceneNode* camera = static_cast<ICameraSceneNode*>(node);
    irr::scene::ISceneManager * sceneManager = camera->getSceneManager();
    // check the camera is operational
    if(!camera->isInputReceiverEnabled()) { return; }
    if(sceneManager && sceneManager->getActiveCamera() != camera) { return; }

    // if there is change to apply
    if (m_isPressed || m_wheel)
    {
        irr::f32 posZ = m_radius * cos(m_rotation.Y);
        irr::f32 posY = m_radius * sin(m_rotation.Y);

        irr::core::vector3df camPos(irr::core::vector3df(0.0f, posY, posZ));
        camPos.rotateXZBy(-m_rotation.X*40, irr::core::vector3df(0.0f, 0.0f, 0.0f));

        camera->setPosition(camPos);
        m_wheel = false;
    }
}


Et voici la fonction qu'il faut absolument comprendre. Les premières lignes n'ont rien de bien sensationnel, on se contente de récupérer un accès au node qu'on anime, donc à la caméra en ce qui nous concerne, et on vérifie que tout est OK. Si c'est le cas on arrive ligne 12 au moment de vérifier que la position de la caméra a bougé. C'est là qu'on voit l'utilité de m_wheel que nous avons vu plus haut. Dès qu'un mouvement sur la molette est repéré, ce booléen passe à 1, puis à la fin de cette fonction quand nous aurons mis la position de la caméra à jour, il repassera à 0.

Si la condition est vérifiée donc, on commence par récupérer un vecteur indiquant la position de la caméra sur le plan ZY (lignes 14/15/17). Cette étape correspond exactement au calcul du vecteur r rouge grâce à l'angle \phi sur le schéma de Kayl. Au détail près que les axes Z et Y sont inversés, puisque lui définit Z comme étant la verticale alors que c'est l'axe Y qui l'est par défaut dans Irrlicht. C'est lors de cette étape que le rayon de la sphère, donc la distance entre la caméra et l'origine du repère est pris en compte (lignes 14/15).

Une fois qu'on a ce vecteur (qui représente directement des coordonnées absolues puisqu'on part du principe que l'origine du repère est 0,0,0) il ne nous reste plus qu'à effectuer une rotation d'angle \theta (toujours par rapport à ce schéma) autour de l'axe vertical (Y pour nous), et nous obtiendrons les coordonnées absolues définitives de la caméra. C'est la ligne 18 qui s'en charge grâce à une fonction interne d'Irrlicht concernant les vecteurs. On aurait également pu passer par une matrice, mais autant utiliser tout ce qui est à notre disposition. :)



Utilisation



Comme le scene manager ne contient pas de fonction toute faite pour ajouter une caméra avec le bon animator (à moins que vous le modifiiez), nous allons devoir le faire nous même. Cela n'a vraiment rien de sorcier, regardez plutôt le code ci-dessous :

Code : C++
1
2
3
4
5
6
7
irr::scene::ICameraSceneNode *camera = sceneManager->addCameraSceneNode(sceneManager->getRootSceneNode(),
    irr::core::vector3df(40.0f, 100.0f, 40.0f), irr::core::vector3df(0.0f, 0.0f, 0.0f));
irr::scene::ISceneNodeAnimator* animator = new irr::scene::CSceneNodeAnimatorCameraTrackball(
    driver, device->getCursorControl(), 50, 4, 6);

camera->addAnimator(animator);
animator->drop();

Nous commençons par ajouter une caméra banale, donc statique ligne 1. Puis on crée manuellement l'animator ligne 3, avant de l'attacher à la caméra ligne 6.
Voilà, vous êtes maintenant capable de créer n'importe quel type de caméra qui vous plaira.

Néanmoins et pour conclure, si vous avez testé celle que nous venons de réaliser vous avez dû remarquer un petit défaut. En effet lors de la première mise à jour de la position de la caméra, donc lors du premier mouvement de souris, la caméra "saute" d'un endroit à l'autre. Cela est dû au fait que sa position est calculée à partir de la variable m_rotation, qui est initialisée tout à fait arbitrairement dans notre constructeur.

On ne tient donc absolument pas compte de la position de la caméra passée en paramètre lors de sa création. Pour ce faire, il faudrait faire les calculs inverses à ceux que nous avons détaillés, dans le constructeur, pour initialiser les 2 angles de coordonnées sphériques (m_rotation) à partir des coordonnées absolues.

Vous savez ce qu'il vous reste à faire. ;)
Chapitre précédent Sommaire Chapitre suivant

Partager

7 commentaires pour "Des caméras et des animators"
Note moyenne : 3.60 / 4 (122 votes)
Pseudo Commentaire
Hors ligne kami-sama # Posté le 11/01/2011 à 05:18:42
真実はたった一つ
Avatar

Ece-normal que je vois que le tuto a été crée en 2007 alors que j'ai lu les autre tuto irrlicht il y a un peu près un ans et que il n'y étais pas?....
 
Hors ligne OveRdrivR # Posté le 01/02/2011 à 14:47:10
No Sleep Till Brooklyn
Avatar

Avis : Très bon

Une chose que je ne trouve pas clair dans ce tutoriel est pourquoi tu écris "irr::scene" dans cette ligne par exemple :

Code : C++
1
bool irr::scene::CSceneNodeAnimatorCameraTrackball::OnEvent(const SEvent& event)


La classe CSceneNodeAnimatorCameraTrackball est une classe que nous avons crée, elle n'appartient donc pas au namespace irr::scene de irrlicht. Peut être est-ce à cause de l'héritage ? Mais ça me semble bizarre même si je suis loin d'être un expert.

Ensuite, le code ne fonctionne pas chez moi j'obtiens une erreur :

main.cpp
Code : Bash
1
error: cannot allocate an object of abstract type 'CSceneNodeAnimatorCameraTrackball'


Je précise que même avec l'implémentation de Kts, la méthode virtuelle CreateClone(...) me provoque une erreur à la compilation :

Code : Bash
1
error: 'createClone' declared as a 'virtual' field


J'espère que tu pourra m'éclairer sur ces points.
 
Hors ligne Kevin Leonhart # Posté le 01/02/2011 à 15:17:44
Monde de merde
Avatar

Salut,

Le namespace vient sûrement du fait que j'ai du déclarer la classe dans le namespace irr::scene:: et oublié de le préciser dans le code du tuto. Pour ton problème d'instanciation, crée un nouveau thread sur le forum en remettant tout le code.

L'intégralité du code représente d'ailleurs pas mal de lignes, un peu trop pour être affichée tel quel selon moi. C'est la raison pour laquelle je ne détaille que les points importants. L'implémentation de createClone ne devrait pas poser de problèmes en théorie. On peut d'ailleurs s'inspirer des autres animators de camera existants dans le moteur en cas de besoin.

Kami-sama, la date de création représente littéralement la date de création, et non la date de parution. J'indique au passage que je suis en train de ré-écrire le tutoriel progressivement, mais ça prend du temps. Donc je ne sais pas quand la nouvelle version de ce chapitre sera disponible.

tuto_csoundcypherImage utilisateurthe_machinistdanger_daysle_coeur_a_ses_raisonsghost_in_the_shellinternet_is_shitplay_asia
 
Hors ligne OveRdrivR # Posté le 01/02/2011 à 15:32:06
No Sleep Till Brooklyn
Avatar

Avis : Très bon

ça gère si tu mets à jour le tutoriel ;) Merci des réponses du coup il faut que je vire tout ces irr::scene même si mettre des namespaces en trop généralement ne cause pas d'erreurs.

EDIT : mon problème de virtual semble venir du fait que toutes mes méthodes dans ma classe sont virtuelles pures. Je poste ça sur le forum.
 
Hors ligne chichiri # Posté le 22/04/2011 à 17:51:05
Viva Lasagna
Avatar

Avis : Très bon

Très intéressant chapitre, comme tous les autres d'ailleurs, en revanche je ne sais pas si tu l'as fait exprès, mais le fait que tu n'ais pas donné le code complet force à jeter un coup d'oeil aux source. Ce qui n'a que du bon en soi. :)

La violence est le dernier refuge de l'incompétence : ménager votre ordi! :p
Un programme ne marche pas.. il fonctionne. :p
Un problème sous Blender, référez-vous au Image utilisateur

Sessions photoréalistes du SDZ

Sessions speed modelling du SDZ

 

Voir tous les commentaires