I. Prérequis▲
Ce tutoriel présume que vous ayez des connaissances de programmation en C++ et que vous êtes capable de configurer et compiler une application Ogre (si vous avez des difficultés à configurer votre application, voir ce guide pour les configurations spécifiques au compilateur). Aucune connaissance sur Ogre n'est nécessaire, en dehors de celles concernant l'installation, pour aborder ce tutoriel.
Bien que je détaille la procédure pour les compilateurs Microsoft Visual Studio 2005 et Microsoft Visual C++ Express 2005 (nommé dans la suite VS), tout le code présenté fonctionne quel que soit le compilateur et l'environnement (Windows ou linux), vous aurez juste à taper le code et à créer les fichiers à la main si vous utilisez un autre environnement de programmation.
II. Introduction▲
Dans ce tutoriel, on va chercher à créer une application Ogre à partir de rien. Bien que cet objectif semble très simple, vous verrez qu'il met en oeuvre beaucoup de choses : vous pouvez regarder la table des matières de cet article pour vous en convaincre.
III. C'est parti !▲
Nous nous contenterons de créer une application minimale qui ne fera rien d'autre que d'afficher une tête d'Ogre !
I-A. L'application ogre▲
Créez un projet Ogre, si vous utilisez l'assistant pensez bien à choisir un projet vide, si ce n'est pas le cas et que vous êtes sous Windows créez bien un projet Windows Application et non un projet Console Application. Créez dans ce projet un fichier nommé Tutoriel2.cpp et tapez pour l'instant le code suivant :
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include
<windows.h>
#endif
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int
main(int
argc, char
*
argv[])
#endif
{
return
0
;
}
Bien, rapidement que fait ce code ? Pas grand chose à vrai dire : il crée une application qui ne fait rien, passionnant ! Les #if, #ifdef ... permettent de vérifier si on fonctionne sous Windows ou non afin de charger l'entête windows.h si nécessaire.
I-B. L'objet root▲
Ajoutez la prise en charge d'Ogre. Pour cela ajoutez, en tête du fichier, la directive :
#include
<Ogre.h>
Créez à présent l'application Ogre proprement dite, ajoutez donc dans la fonction main :
try
{
Ogre::
Root*
root=
new
Ogre::
Root();
delete
root;
}
catch
(Ogre::
Exception e){
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBox(NULL
,e.getFullDescription(),
"Erreur d'initialisation de l'objet Root"
,MB_OK|
MB_ICONERROR|
MB_TASKMODAL);
#else
std::
cerr <<
"Une exception a été levée: "
<<
e.getFullDescription().c_str() <<
std::
endl;
#endif
}
Une application Ogre est susceptible de lever des exceptions, c'est pourquoi les deux instructions sont entourées dans un bloc try-catch. Celui-ci ne fait qu'afficher à l'écran (soit par une boîte de message si vous êtes sous Windows, soit dans la console sinon) la description de l'erreur. Je n'expliquerai pas ici les exceptions, cela l'étant dans tout bon livre de référence sur le C++.
La première ligne crée l'objet indispensable à toute application Ogre : l'objet Root. C'est à partir de cet objet que se construira tout le reste, cet objet est donc indispensable à notre application. La deuxième ligne se contente de nettoyer correctement la mémoire. Si vous compilez et lancez l'application, le résultat sera quelque peu décevant : il ne se passera rien ! Etoffons maintenant cette application.
Dans Ogre, tous les types d'objets définis dans la bibliothèque Ogre sont placés dans un espace de nom (namespace) appelé Ogre. Ainsi tous ces types doivent être précédés de Ogre:: pour pouvoir y faire référence. Cette notation que l'on peut parfois trouver lourde a, entre autre, l'avantage de bien mettre en évidence l'appartenance de ces objets à Ogre. Si toutefois vous souhaitez vous évitez cette "décoration", vous pouvez spécifier en début de fichier, juste après la directive d'inclusion de l'entête Ogre, la directive :
using
namespace
Ogre;
I-C. Création de la fenêtre de rendu▲
Vous devez ajouter à votre programme une chose essentielle qui apparaissait à chaque lancement de votre application : la boîte de dialogue de configuration de l'écran. Ceci va vous permettre d'obtenir un pointeur vers une fenêtre de rendu : RenderWindow. Ajoutez les lignes suivantes entre la création de l'objet Ogre et sa destruction :
if
(!
root->
restoreConfig()){
if
(!
root->
showConfigDialog()){
delete
root;
return
-
1
;
}
}
Ogre::
RenderWindow*
pRenderWindow;
pRenderWindow =
mRoot->
initialise(true
,"Tutoriel 2 App"
);
Que font ces lignes ? Le premier if, vérifie qu'un fichier de configuration d'écran existe (ogre.cfg par défaut; si vous souhaitez modifier ce nom de fichier, utilisez le deuxième paramètre du constructeur de l'objet Root). Si ce fichier existe, les paramètres sont chargés et la fonction renvoie true; si le fichier n'existe pas ou si les paramètres ne sont pas valides, la méthode restoreConfig() renvoie false, dans ce cas, on affiche la boite de dialogue de configuration d'écran avec showConfigDialog(). Si cette fonction renvoie false, l'utilisateur a alors cliqué sur Annuler, dans ce cas on nettoie l'objet root et on ressort de l'application en envoyant le code d'erreur -1. Sinon la fonction a renvoyé true, on crée alors un pointeur vers un objet "fenêtre de rendu", ou RenderWindow que l'on stocke dans pRenderWindow avec la fonction initialise. Cette fonction prend 2 arguments : le premier est booléen et, permet de spécifier si la création de la fenêtre est immédiate ou non; le deuxième est une chaîne permettant de spécifier le titre de la fenêtre. Vous pouvez compiler et lancer l'application : une fenêtre de configuration s'ouvrira selon que vous avez déjà ou non un fichier ogre.cfg (vous pouvez l'effacer pour voir la conséquence sur le lancement de l'application). Un écran noir s'affiche puis l'application se termine.
I-D. Création de la scène▲
Que faire si vous souhaitez à présent afficher quelques objets comme on l'a déjà vu dans le précédent tutoriel ? Il nous faut un gestionnaire de scène, ajoutez donc :
Ogre::
SceneManager*
pSceneManager=
NULL
;
pSceneManager=
root->
createSceneManager(Ogre::
ST_GENERIC,"SceneManager Generique"
);
Vous avez votre gestionnaire de scène, ici, un gestionnaire générique. Il existe d'autres types de gestionnaires, notamment certains qui permettent de gérer les grandes scènes d'extérieur avec terrain, ciel et grands espaces. Je vous renvoie à la documentation si cela vous intéresse car ce n'est pas le but de ce tutoriel. Pour votre petite scène, un générique suffira amplement. Pour le reste du code, vous devriez être assez familier avec, je ne le détaillerai pas :
Ogre::
Entity*
pEntity=
pSceneManager->
createEntity("Ogre Head"
,"OgreHead.mesh"
);
Ogre::
SceneNode*
pNode=
pSceneManager->
getRootSceneNode()->
createChildSceneNode("Node"
);
pNode->
attachObject(pEntity);
Verifier si vous pouvez compiler, mais ne lancez surtout pas votre programe ! Celui-ci se planterait !
I-E. Les ressources▲
Pourquoi planterait-il ? Une exception serait levée car Ogre ne saurait pas où trouver le fichier ogrehead.mesh. Ah ?!? Mais dans le tutoriel 1, cela n'avait pas posé de problème, vous ne vous étiez d'ailleurs même pas poser la question ! Qu'est ce qui a donc changé ? Dans le tutoriel 1, votre application héritait de la classe ExampleApplication qui faisait une grande partie du travail pour vous et, entre autre, celui d'indiquer à Ogre où résidaient les fichiers des objets maillés et des textures grâce au fichier resources.cfg dans lequel vous pouvez jeter une oeil pour voir ce qu'il contient. Il va vous falloir réaliser cette opération à la main, ici.
I-E-1. Code en dur !▲
Il vous faut ajouter le chemin qui permettra de trouver le fichier ogrehead.mesh, puis initialiser les ressources. Ajoutez les deux lignes suivantes juste avant la création de votre scène :
Ogre::ResourceGroupManager::
getSingleton().addResourceLocation("../../media/models"
,"FileSystem"
,"General"
);
Ogre::ResourceGroupManager::
getSingleton().initialiseAllResourceGroups();
La première fonction ajoute le chemin, indique qu'il s'agit un répertoire (FileSystem) et non d'une archive Zip, le dernier paramètre, "Général" ici, permet de définir différents groupes de ressources. Ici vous n'en utiliserez qu'un compte tenu du faible nombre d'entités différentes : 1 ici !
Compilez puis lancez le programme. Celui va se terminer sans rien afficher, cela est parfaitement normal : vous avez créé notre scène, mais vous ne demandez jamais de l'afficher ! Là encore la classe ExampleApplication le faisait pour vous ... Mais avant de s'occuper de cela, examinons ensemble un autre problème plus "vicieux" ...
Allez jeter un coup d'oeil au fichier ogre.log dans le dossier bin du SDK : parcourez-le et cherchez attentivement vers la fin du fichier les lignes suivantes :
Mesh: Loading OgreHead.mesh
Can't assign material ...
Argh ! Qu'est-ce donc ? Les entités dans Ogre sont composées d'un modèle de maillage 3D, ce sont les fichiers .mesh qui peuvent être construits avec 3DS Max, Maya, blender ... avec l'exporteur adéquat. Ces fichiers ne contiennent que la description géométrique de l'objet, c'est-à-dire, l'emplacement des points dans l'espace. Pour réaliser un objet en 3D, la tête d'ogre dans notre cas, il faut ajouter également une ou des textures, qui sont les images à superposer sur le maillage pour donner l'impression d'une matière. Ces matières (ou material) sont contenues dans des fichiers .material qui peuvent être également construits par les logiciels 3DS, Maya ou blender avec l'exporteur correspondant.
Les quatres textures que ne trouvent pas Ogre sont : Ogre/Eyes, Ogre/Skin, Ogre/Earring et Ogre/Tusks. Ces 4 "material" sont, en réalité, contenus dans le fichier Ogre.material qui se situe dans le répertoire media/materials/scripts du SDK. Ce fichier fait lui-même appel à quatre textures qui se trouvent dans media/materials/textures.
Ajoutez donc les lignes suivantes dans l'ajout des ressources juste avant l'initialisation des groupes de ressources :
Ogre::ResourceGroupManager::
getSingleton().addResourceLocation("../../media/materials/scripts"
,"FileSystem"
,"General"
);
Ogre::ResourceGroupManager::
getSingleton().addResourceLocation("../../media/materials/textures"
,"FileSystem"
,"General"
);
Ogre::ResourceGroupManager::
getSingleton().addResourceLocation("../../media/materials/programs"
,"FileSystem"
,"General"
);
Tiens, mais n'a-t-on pas parlé que des deux premières lignes ? Or, il y en une de plus : media/materials/programs. Dans le répertoire scripts, il n'y a pas que notre matière pour l'ogre; il y a d'autres exemples, et parmi ceux-ci, certains font appels à des shaders. Or ceux-ci nécessitent les fichiers se trouvant dans media/materials/programs, d'où la présence de cette troisième ligne.
Recompilez, lancez à nouveau, il ne devrait plus y avoir d'erreur ni apparentes ni dans le fichier Ogre.log. Mais cette programmation est-elle vraiment envisageable ? Dès que l'on souhaitera ajouter un objet ou une texture qui ne sera pas dans les répertoires précédents, ça risque d'être compliqué. Et surtout, on devra recompiler tout le code ! Cette solution fonctionne mais nous pourrions trouver mieux.
I-E-2. Utilisation d'un script de configuration▲
Pourquoi ne pas reprendre le principe utilisé dans ExampleApplication, qui consiste à aller mettre nos différents dossiers et/ou archives dans un fichier appelé par exemple resources.cfg ? Cette méthode sera souple : on pourra modifier les répertoires sans tout recompiler, et on va voir qu'Ogre va beaucoup nous faciliter le travail !
Le fichier resources.cfg contient des entêtes de sections; dans chaque section, on trouvera des lignes contenant le type de ressources, puis le chemin de celle-ci, bref, les paramètres de la fonction addResourcesLocation().
Ouvrez donc votre fichier ressources :
Ogre::
ConfigFile cf;
cf.load("resources.cfg"
);
Créez un itérateur qui vous permettra de parcourir les différentes sections du fichier :
Ogre::ConfigFile::
SectionIterator si=
cf.getSectionIterator();
Maintenant pour chaque section (vous stockerez le nom de la section dans secNom), il vous faut prendre chaque ligne; et pour chaque ligne, prendre le type d'archive, que vous stockerez dans typeArch et son chemin que vous stockerez dans archChemin :
Ogre::
String secNom, typeArch, cheminArch;
while
(si.hasMoreElements()){
secNom =
si.peekNextKey();
Ogre::ConfigFile::
SettingsMultiMap *
settings =
si.getNext();
Ogre::ConfigFile::SettingsMultiMap::
iterator it;
for
(it=
settings->
begin(); it!=
settings->
end();++
it){
typeArch =
it->
first;
cheminArch =
it->
second;
Ogre::ResourceGroupManager::
getSingleton().addResourceLocation(cheminArch,typeArch,secNom);
}
}
Ogre::ResourceGroupManager::
getSingleton().initialiseAllResourceGroups();
Au final, par rapport au six lignes précédentes, ça ne vous fait guère plus de code, si l'on compare aux possibilités de ces lignes. Il faudra, en revanche, prendre soin de placer un fichier resources.cfg (ou un autre nom si vous modifiez le code précédent) dans le répertoire de l'application. Pour tous mes tutoriaux, le fichier par défaut (resources.cfg)sera amplement suffisant.
Mais toujours pas d'Ogre en vue !
I-F. Création de la caméra d'observation▲
Ajoutez donc une camera, c'est par celle-ci que vous pourrez observer votre scène. L'ajout ressemble beaucoup à celui d'une entité, si ce n'est que vous n'aurez pas besoin de noeuds :
Ogre::
Camera*
pCamera=
pSceneManager->
createCamera("Camera"
);
Il faut, bien évidemment, spécifier la position de la caméra, et le point qu'elle observe. Plaçons la en (0,50,200), et faisons la observer le point d'origine (0,0,0). De cette façon, nous n'avons pas à préciser la focale de la caméra, bien que cela puisse être fait également :
pCamera->
setPosition(Ogre::
Vector3(0
,50
,200
));
pCamera->
lookAt(Ogre::
Vector3(0
,0
,0
));
I-G. Création du "Viewport"▲
Il ne faut pas seulement créer la caméra : ce qui est vu par la camera peut n'occuper qu'une partie de l'écran. Il faut qu'on précise à Ogre que nous souhaitons voir la scène par l'intermédiaire de la caméra et que cette vue occupe tout l'écran ! C'est ce que l'on nomme le Viewport. Imaginez un jeu de courses de voitures où deux joueurs s'affrontent; l'écran pourrait être divisé en deux parties : la partie gauche serait une vue consacrée à la caméra suivant la voiture du joueur 1, et à droite, la caméra suivant la voiture du joueur 2. Ici, faisons simple : une caméra, une vue ! Il faut donc ajouter un Viewport, on définira au passage la couleur de fond ici un bleu foncé (0.0,0.0,0.3) en couleur RGB :
Ogre::
Viewport*
pViewport=
pRenderWindow->
addViewport(pCamera);
pViewport->
setBackgroundColour(Ogre::
ColourValue(0.0
,0.0
,0.3
));
// On ajuste le ratio de la camera pour qu'il coïncide avec celui de l'écran
pCamera->
setAspectRatio(Ogre::
Real(pViewport->
getActualWidth())/
Ogre::
Real(pViewport->
getActualHeight()));
I-H. La boucle de rendu▲
Il ne nous reste plus qu'à ajouter la fonction qui indique à Ogre d'afficher à proprement parler la scène, c'est-à-dire d'effectuer le rendu à l'écran :
root->
startRendering();
Ouf! Enfin fini! Vite, vite compilez. Pendant ce temps, réfléchissez : que fait startRendering() ? Cette méthode fait le rendu puis l'affiche, refait un rendu puis l'affiche, refait ... Stop ! Cette fonction s'arrête quand ? JAMAIS ! Enfin presque. Si vous exécutiez votre programme en l'état actuel des choses, le programme ne se terminerait pas. Il va donc falloir travailler encore ! On va commencer par enlever cette instruction de boucle sans fin. On verra dans le prochain tutoriel que l'on peut, sous certaines conditions de programmation, utiliser cette fonction bien utile. Pour l'instant, malheureusement, nous devrons nous en passer.
Il faut que vous rajoutiez ce qu'il faut pour qu'à chaque rendu de votre scène, Ogre vérifie que l'utilisateur ait appuyé ou non sur la touche ECHAP. Si c'est le cas, il faut stopper l'application. Créez l'objet qui vous permettra de lire le clavier :
Ogre::
InputReader*
pInputReader =
Ogre::PlatformManager::
getSingleton().createInputReader();
pInputReader->
initialise(pRenderWindow,true
,false
);
La boucle de rendu doit commencer par afficher le rendu de la scène. Enfin elle lit le clavier et si la touche ECHAP est enfoncée, elle doit sortir de la boucle pour terminer le programme. Ajoutez maintenant le code de la boucle de rendu proprement dite :
while
(1
){
root->
_updateAllRenderTargets();
pInputReader->
capture();
if
(pInputReader->
isKeyDown(Ogre::
KC_ESCAPE))
break
;
}
}
Voilà, c'est fini cette fois ! Vous devriez pouvoir observer la belle tête d'Ogre ! Que de travail juste pour une tête !
V. Conclusion▲
Dans ce tutoriel, vous avez mis les mains dans le cambouis : vous avez fait votre application de A à Z. On notera que dans le premier tutoriel, vous aviez pu vous passer de tout cela, grâce à une classe d'application Tutoriel1App que vous aviez dérivé d'une classe existante : ExampleApplication. N'oubliez pas, en effet, qu'Ogre est avant tout un moteur 3D orienté objet. Il vous faut donc penser à la réutilisation du code. D'où l'intérêt de la classe ExampleApplication.
Pourquoi dans ces conditions avez vous fait tout cela ? Parce que comme beaucoup vous le diront, la classe ExampleApplication, qui est d'une praticité indéniable pour débuter, est écrite de manière très contestable : les fichiers d'entêtes et de codes ne sont absolument pas séparés. La conception de la classe peut aussi être criticable en fonction de vos besoins.
Grâce à ce tutoriel, j'espère que vous pourrez vous servir de cette classe tout en étant critique par rapport à sa construction. Elle est très bien pour se focaliser sur des points précis comme les entités, les noeuds de scène, les animations (que nous verrons plus tard) ... sans se soucier de la gestion de l'application. Elle est en revanche horrible pour servir de base à un véritable Framework d'application Ogre.
Téléchargements▲
Voici les fichiers relatifs à ce tutoriel :