1. Introduction▲
L'animation est une partie importante et quasi inévitable de n'importe quelle application en 3D temps réel. OpenSceneGraph met à disposition plusieurs moyens pour cette dernière. Nous en verrons ici trois différentes :
- AnimationPath qui permet de définir des « clefs » de position et osg interpole la position de l'objet pour chaque frame ;
- Sequence, qui est un groupe particulier qui change d'enfant pour un intervalle donné ;
- un exporter pour 3ds Max : osgexp. En enregistrant son modèle en .osg, l'animation sera lue toute seule.
2. Classes et bibliothèques utilisées▲
2-A. AnimationPath▲
Classes :
- osg::AnimationPath : classe permettant l'animation par « clefs » ;
- osg::MatrixTransform : classe gérant les transformations. C'est un groupe. Tout ce qu'il contient subit la transformation qu'il définit ;
- osg::Geode et osg::ShapeDrawable : ces deux classes permettront de créer un cube simple pour les tests.
Bibliothèques : (enlevez le d pour le mode release)
- osgd.lib ;
- osgViewerd.lib ;
- osgManipulatord.lib ;
- osgUtild.lib.
Includes :
- <osgViewer/Viewer> ;
- <osg/ShapeDrawable> ;
- <osg/animationpath> ;
- <osg/matrixtransform>.
2-B. Sequence▲
Classes :
- osg::Geode et osg::ShapeDrawable : ces deux classes permettront de créer un cube simple pour les tests ;
- osg::Sequence : classe permettant la permutation de ses enfants pour un intervalle donné.
Bibliothèques : (enlevez le d pour le mode release)
- osgd.lib ;
- osgViewerd.lib ;
- osgUtild.lib.
Includes:
- <osgViewer/Viewer> ;
- <osg/Sequence> ;
- <osg/ShapeDrawable>.
2-C. osg Exporter▲
Classe :
- osgDB::readNodeFile : fonction permettant de lire un fichier, et de renvoyer une référence vers la « node » en résultant.
Bibliothèques : (enlevez le d pour le mode release)
- osgd.lib ;
- osgViewerd.lib ;
- osgUtild.lib ;
- osgDBd.lib.
Includes :
- <osgViewer/Viewer> ;
- <osg/ReadFile>.
3. Mise en pratique▲
3-A. Utilisation de AnimationPath▲
Animationpath marche selon le principe de « keyframe » ou « image-clé». Ce principe se base sur le fait qu'entre deux positions d'une transformation linéaire (une rotation d'un certain angle, une translation en ligne droite, une mise à l'échelle) on peut interpoler, soit deviner, les images intermédiaires.
Nous allons faire un programme simple qui fait se déplacer un cube de gauche à droite de la fenêtre et vice versa.
Partons du code complet et expliquons :
#include
<osgViewer/Viewer>
#include
<osg/animationpath>
#include
<osg/matrixtransform>
#include
<osg/ShapeDrawable>
osg::
Node*
creerScene()
{
// Création du cube.
osg::
Geode*
pGeode =
new
osg::
Geode();
pGeode->
addDrawable( new
osg::
ShapeDrawable( new
osg::
Box(osg::
Vec3(0.0
f,0.0
f,0.0
f),2.0
f) ) );
// Ajout du cube dans un groupe MatrixTransform
osg::
MatrixTransform*
pMatTrans =
new
osg::
MatrixTransform;
pMatTrans->
addChild( pGeode );
// Création d'un Animation Path
osg::
ref_ptr<
osg::
AnimationPath>
rPath =
new
osg::
AnimationPath;
// Définition du mode de boucle
rPath->
setLoopMode( osg::AnimationPath::
SWING );
// Création de point de contrôle
osg::AnimationPath::
ControlPoint c0(osg::
Vec3(-
1
,0
,0
));
osg::AnimationPath::
ControlPoint c1(osg::
Vec3( 1
,0
,0
));
rPath->
insert( 0.0
f, c0 );
rPath->
insert( 2.0
f, c1 );
// Mise en place du callback (rappel)
osg::
ref_ptr<
osg::
AnimationPathCallback>
rAniCallback =
new
osg::
AnimationPathCallback( rPath.get() );
pMatTrans->
setUpdateCallback( rAniCallback.get() );
return
pMatTrans;
}
int
main(int
argc, char
*
argv[]) {
// Création du viewer
osgViewer::
Viewer viewer;
viewer.setUpViewInWindow( 32
, 32
, 512
, 512
);
// Création de la scène.
viewer.setSceneData(creerScene());
// lancement du viewer
return
viewer.run();
}
Le main doit maintenant vous être familier (si ce n'est pas le cas, voir le tutoriel sur le premier programme en OpenSceneGraph). Attardons-nous plus sur les différentes parties de la création de la scène.
// Création du cube.
osg::
Geode*
pGeode =
new
osg::
Geode();
pGeode->
addDrawable( new
osg::
ShapeDrawable( new
osg::
Box(osg::
Vec3(0.0
f,0.0
f,0.0
f),2.0
f) ) );
// Ajout du cube dans un groupe MatrixTransform
osg::
MatrixTransform*
pMatTrans =
new
osg::
MatrixTransform;
pMatTrans->
addChild( pGeode );
Ici nous ne créons qu'un cube et nous le mettons dans un Group MatrixTransform. Ce Groupe particulier permet de définir des transformations (sous forme matricielle, mais des prédéfinis existent). Une fois une transformation appliquée à un MatrixTransform, tous ses enfants auront cette transformation appliquée (ne pas perdre de vue la structure en arbre : tout ce qui est parent de quelque chose répercute des attributs sur ses fils).
// Création d'un Animation Path
osg::
ref_ptr<
osg::
AnimationPath>
rPath =
new
osg::
AnimationPath;
// Définition du mode de boucle
rPath->
setLoopMode( osg::AnimationPath::
SWING );
// Création de point de contrôle
osg::AnimationPath::
ControlPoint c0(osg::
Vec3(-
1
,0
,0
));
osg::AnimationPath::
ControlPoint c1(osg::
Vec3( 1
,0
,0
));
rPath->
insert( 0.0
f, c0 );
rPath->
insert( 2.0
f, c1 );
C'est ici que l'AnimationPath est déclaré.
On définit un mode pour ce dernier. Ces modes sont au nombre de trois :
- NO_LOOPING : l'animation sera jouée une seule fois et c'est tout ;
- LOOP : l'animation est jouée en boucle. Une fois arrivée à la dernière position, elle recommence depuis la première, jusqu'à ce qu'on l'arrête ;
- SWING : comme looping, continue de jouer l'animation, sauf qu'au lieu de recommencer à la première position, elle part en sens inverse une fois sur la dernière position.
Enfin on crée des ControlPoint, qui sont les clefs de notre animation. Ici nous n'en avons que deux car le cube se déplace de gauche à droite. Mais rien n'empêche d'en faire d'autres pour que le cube fasse des zigzags ou autres.
Le constructeur de ControlPoint existe en trois versions :
- un seul paramètre, de type Vec3d, qui ne va donc définir qu'une position dans l'espace ;
- deux paramètres, Vec3d pour la position, et un type Quat, un quaternion permettant de définir une rotation ;
- trois paramètres, les deux précédents, plus un paramètre de mise à l'échelle de type Vec3d.
Et pour finir, on insère ces points de contrôle dans notre AnimationPath, par ordre chronologique. Le premier paramètre est le temps écoulé depuis le début de l'animation, et non depuis la position précédente.
// Mise en place du callback (rappel)
osg::
ref_ptr<
osg::
AnimationPathCallback>
rAniCallback =
new
osg::
AnimationPathCallback( rPath.get() );
pMatTrans->
setUpdateCallback( rAniCallback.get() );
return
pMatTrans;
Dernière étape importante, on crée un objet AnimationPathCallback. Cet objet permet de faire un callback de notre AnimationPath. Un callback, ou rappel, et une fonction appelée par une autre, généralement après un temps donné.
Ici on lui passe en paramètre une référence vers notre AnimationPath (le .get() permet juste de retourner un pointeur standard, puisque l'on utilise des ref_ptr). Et on applique ce callback à notre groupe MatrixTransform. Ainsi à chaque mise à jour de l'affichage, le groupe appellera le callback de l'animation, qui lui renverra la transformation pour placer l'objet à la position à laquelle il doit être.
3-B. Utilisation de Sequence▲
La classe osg::Sequence est un groupe spécial qui a un fils « actif » qu'il affiche, et d'autres inactifs. Toutes les x secondes que vous lui avez indiqué, il change son fils actif pour le suivant dans l'ordre dans lequel on les lui a fournis. Cela rend donc l'animation d'un déplacement plus complexe qu'avec l'AnimationPath, mais cela permet de choisir entre plusieurs modèles 3D.
Ici nous n'allons que faire changer un cube en sphère puis en cône.
Le main est le même que précédemment, voici la fonction de création de scène :
// Création du cube.
osg::
Geode*
pCube =
new
osg::
Geode();
pCube->
addDrawable( new
osg::
ShapeDrawable( new
osg::
Box(osg::
Vec3(0.0
f,0.0
f,0.0
f),2.0
f) ) );
// Création de la sphère.
osg::
Geode*
pSphere =
new
osg::
Geode();
pSphere->
addDrawable( new
osg::
ShapeDrawable( new
osg::
Sphere(osg::
Vec3(0.0
f,0.0
f,0.0
f),1.0
f) ) );
// Création du cône.
osg::
Geode*
pCone =
new
osg::
Geode();
pCone->
addDrawable( new
osg::
ShapeDrawable( new
osg::
Cone(osg::
Vec3(0.0
f,0.0
f,0.0
f),1.0
f, 2.0
f) )
);
Nous créons des objets simples : cube, sphère et cône.
osg::
Sequence*
pSequ =
new
osg::
Sequence;
pSequ->
addChild(pCube);
pSequ->
addChild(pSphere, 5.0
f);
pSequ->
addChild(pCone);
pSequ->
setInterval(osg::Sequence::LoopMode::
LOOP, 0
, 2
);
pSequ->
setDuration(1.0
f);
pSequ->
setMode(osg::Sequence::SequenceMode::
START);
return
pSequ;
Voilà enfin osg::Sequence.
Une fois instanciée, nous y ajoutons nos trois formes. Remarquez que le deuxième addChild, celui de la sphère, a un deuxième paramètre. Ce paramètre est le temps associé à ce fils. Ainsi la sphère a un temps assigné de 2 (secondes). Les autres n'en ayant pas de précisé, c'est 1 (seconde) qui est défini par défaut.
Viennent ensuite trois méthodes de osg::Sequence :
- setInterval, qui permet de définir le type (LOOP ou SWING, voir 3.1), ainsi que l'intervalle de fils à prendre en compte. Ici tous sont pris en compte (0, 1 et 2). Si je n'avais mis que 0,1 seuls le cube et la sphère auraient été affichés ;
- SetDuration qui définit le facteur de durée. Ici 1.0 signifie que chaque fils sera affiché le temps qui lui est associé (soit 1 s pour le cube et le cône et 2 s pour la sphère). Si on avait mis 2 ils seraient restés deux fois plus longtemps et 0.5, deux fois moins. Plus généralement un fils restera son temps multiplié par ce paramètre ;
- SetMode qui peut prendre quatre paramètres différents : START, STOP, PAUSE et RESUME. START démarre l'animation du premier fils défini dans setInterval, STOP la coupe .
- PAUSE arrête le défilement, Sequence reste bloqué sur le fils actif, et RESUME recommence l'animation à partir de ce fils.
Bien sûr d'autres méthodes existent. Par exemple celle qui permet de définir si lorsque l'animation est STOP, on laisse affiché à l'écran le dernier fils ou si au contraire, on n'affiche plus rien du tout. Pour de plus amples informations, voir la doc.
À savoir qu'une classe similaire existe : Switch. Sauf que celle-ci ne marche pas avec le temps. C'est à l'utilisateur de définir quel fils est activé, ce qui permet de changer un modèle en fonction de l'action utilisateur par exemple.
3-C. Utilisation d'un modèle en .osg▲
Les fichiers en .osg sont du format natif d'OpenSceneGraph. Les exemples de ce dernier comportent d'ailleurs des modèles en .osg. Ici nous allons utiliser un plugin pour 3ds qui permet d'exporter un modèle en .osg. Ce plugin permet également d'exporter l'animation créée dans 3ds. Attention cependant, le modèle sera animé sans interruption, sur la longueur de temps définie dans 3ds.
3-C-1. Téléchargement et installation du plugin▲
Le plugin pour 3ds Max peut être trouvé à cette adresse :
http://sourceforge.net/projects/osgmaxexp/
Une fois téléchargé, lancez simplement l'installateur. Il détectera la version de 3ds installée sur le PC et installera automatiquement le plugin.
À noter qu'une version Blender, au fonctionnement similaire, existe également.
3-C-2. Exportation d'un modèle▲
Pour exporter un modèle, c'est très simple. Je suppose ici que vous avez des bases en logiciel de modélisation. Créez une forme simple et animez-la. Par exemple, faites faire une rotation à un cube.
Une fois votre modèle créé, allez simplement dans Fichier → Exporter...
Choisissez dans la liste déroulante le type OpenSceneGraph Exporter (*.ive, *.osg), puis donnez un nom au fichier, en le terminant par .osg pour l'enregistrer en .osg, le défaut étant .ive. (Les .ive sont du format binaire OSG. En .ive, les textures sont enregistrées dans le fichier, alors qu'en .osg il faut les placer à côté du fichier. Les .ive ont un chargement plus rapide cependant, ils posent parfois certains problèmes si les bons plugins ne sont pas installés.)
Si votre modèle est complexe et a demandé beaucoup de travail, gardez bien une version en .max ou .3ds, car il est très difficile de faire la conversion inverse (.osg ou .ive vers quelque chose d'autre.)
Une boite de dialogue s'ouvre. Vous pouvez regarder la documentation d'OSG Exporter pour savoir à quoi correspond chaque paramètre, mais ceux par défaut vont très bien la plupart du temps.
Placez ensuite le fichier .osg obtenu à côté de vos fichiers sources. Le programme est le même que les deux fois précédentes, mais cette fois-ci, le contenu de creerScene est :
osg::
Node*
temp =
NULL
;
temp =
osgDB::
readNodeFile("cube.osg"
);
return
temp;
Bien sûr, remplacez cube.osg par le nom de votre fichier.
Vous devriez voir votre animation se jouer dans le viewer.
Même si cette technique est très pratique, elle a cependant quelques limitations : l'exporter ne supporte pas pour l'instant l'animation par déformation. En d'autres termes, vous pouvez agrandir, translater ou faire faire une rotation à votre modèle, mais pas le déformer, ce qui pose des problèmes pour l'animation par « skinning », technique utilisée pour les personnages par exemple.
Un moyen de détourner cette limitation est l'utilisation de l'objet osgSequence dans Max. Il suffit de créer cet objet (dans la page « helper » de 3DSMax) :
De le lier avec votre modèle, et d'exporter ensuite.
Le problème de cette technique est que l'exporter va créer X instances de votre objet avec à chaque fois une déformation différente. En affichant chaque déformation l'une après l'autre, la séquence est recréée et donne l'impression de mouvement. Ce qui peut être pour les mouvements complexes très lourd à charger.
Cependant à l'instant où j'écris ces lignes, la communauté d'OSGExp assure travailler activement à ce support. Le plugin Blender quant à lui semble déjà supporter ce type d'animation.
4. Aller plus loin▲
Voici une petite idée pour, par exemple, utiliser plusieurs animations qui se déclenchent au moment voulu dans une application :
- créez et animez les modèles dans un modeleur 3D (Blender ou 3DS Max) et exportez-les en .osg ou .ive ;
- dans l'application, on utilise ensuite un osg::switch et un osg::timer. Pour chaque animation, on lui associe un temps (connu, puisque l'animation a été créée) et on l'insère dans le switch ;
- lorsque l'action qui doit déclencher l'animation est effectuée, on place le switch sur le fils qui correspond à l'animation voulue et on démarre le timer. À chaque mise à jour de l'écran (frames), on vérifie le temps écoulé depuis le lancement du Timer. Si ce temps est égal ou supérieur à celui de l'animation, on arrête le Timer et on remet le switch sur le fils neutre, idéalement placé en 0.
Et voilà, des animations en fonction du contexte. Bien sûr, ceci n'est qu'un exemple parmi tant d'autres, mais peut permettre d'implémenter des animations simplement.
5. Remerciements▲
Merci à ClaudeLELOUP et jacques_jean pour les corrections.