Version en ligne

Tutoriel : 3D temps réel avec Irrlicht

Table des matières

3D temps réel avec Irrlicht
Introduction
Ce qu'est un moteur 3D
Irrlicht
Mise en place
Installation
Configurer un projet
Premier projet
Structures de données
Arbre binaire
Octree
Graphe de scène
Scene nodes et caméras
Ce qu'est un scene node
Types de données propres à Irrlicht
Les caméras
Création d'une scène
Modèles 3D
Ce qu'est un modèle 3D
Fichiers et format 3D
Animation d'un modèle
Chargement et utilisation
Réalisation de la scène
Materials
Ce qu'est un material
Utilisation
Brouillard
Override material
Textures
Le texture mapping en une leçon
Les textures, parlons en !
De la bonne manière de texturer un modèle
Eclairage
Lumière ambiante
Voyons par nous-même
Lumière diffuse
Lumière émissive
Systèmes de particules
Comment ça marche ?
Le système global
Les émetteurs
Les modificateurs
Gestion des périphériques
Les événements à la source
Le mouvement en action
Caméra personnalisée
Des caméras et des animators
Fonctionnement des caméras
Réalisation d'une caméra trackball
Implémentation de la caméra
Introduction à la GUI 1/2
L'organisation interne
Afficher un texte à l'écran
Les polices de caractère
Le curseur
Un titre accrocheur
Introduction à la GUI 2/2
Les boutons
Les zones de texte
Les menus déroulants
Les boîtes de dialogue
Style global
Les images
Récupérer une image
Les variables made in Irrlicht
Afficher une image
La transparence
Dans les entrailles de la bête
Les namespaces
Du polymorphisme et de la GUI
Du polymorphisme et du scene manager
Un codé pour un rendu
Rendu des scene nodes
Au début il y avait le code source
Préparation
Pré-rendu
Rendu
Et le rendu fut
Creation de scene nodes
Déclaration
Implémentation
Du haut de cette pyramide, 14 chapitres vous contemplent
Introduction aux shaders
Théorie générale
Création de la classe contenant le shader
Enregistrement auprès du moteur
Références
Général
Irrlicht & moteurs 3D
Algorithmes & programmation 3D
Textures
Mathématiques
Formats de fichier 3D
Aide mémoire
Lexique
Formats de fichier 3D
Recompiler Irrlicht
Sous Windows
Sous GNU/Linux
Créer ses modèles avec Blender
Modelisation
Texturing
Import

3D temps réel avec Irrlicht

Image utilisateur

Vous aimeriez programmer des applications 3D mais les APIs bas niveau vous font peur ? Vous avez toujours été nul en math et quand on vous parle de matrices vous fuyez en courant ?

Autant de raisons de se lancer dans l'apprentissage d'Irrlicht, un moteur 3D libre, gratuit et performant. Il vous permettra de débuter dans la programmation d'applications 3D temps réel sans trop grosse prise de tête puisqu'il va s'occuper à votre place du fenêtrage, des mathématiques, de la prise en charge des ressources externes, etc...

Image utilisateurImage utilisateurImage utilisateurImage utilisateurQuelques screenshots d'applications réalisées avec Irrlicht.

Introduction

Ce qu'est un moteur 3D

Commençons comme il est d'usage par un chapitre d'introduction qui va se contenter de présenter les tenants et aboutissants du sujet de ce tutoriel. J'ai nommé les moteurs 3D, et plus particulièrement Irrlicht.

Si vous savez déjà de quoi il s'agit ou que vous n'avez pas besoin de le savoir, lisez-le quand même. Juste pour être sûr... ;)

Ce qu'est un moteur 3D

Introduction Irrlicht

Ce qu'est un moteur 3D

Question plus simple qu'il n'y parait. Pour faire court, un moteur 3D est une bibliothèque logicielle qui va s'occuper de tout ce qui concerne l'affichage à l'écran, les calculs dans l'espace, la lecture des fichiers externes, etc... dans une application 3D. D'ailleurs on parle aussi souvent de moteur graphique, l'aspect 3D n'est pas forcément le cœur. Ça dépend de quelle manière on voit les choses.

Maintenant que c'est dit, il reste quand même quelques points à éclaircir. Particulièrement pour les personnes qui ne sont pas très familières avec le monde de la programmation. Je rappelle ceci-dit qu'il faut avoir un certain niveau en C++ pour suivre ce tutoriel dans de bonnes conditions.

Intéressons nous de plus près au concept de bibliothèque logicielle (library en anglais). Il s'agit comme son nom l'indique d'une collection de fonctions logicielles regroupées dans un ou plusieurs fichiers. Le programmeur qui utilise une bibliothèque peut donc faire appel à ces fonctions dans les programmes qu'il crée. Par exemple avec Irrlicht, il suffit de faire appel à quelques fonctions pour créer une fenêtre et l'afficher à l'écran. Puis à quelques autres pour charger un personnage en 3D et l'afficher dans cette fenêtre, etc...

En programmation le niveau désigne la proximité avec la machine. Plus un langage ou une bibliothèque est bas niveau et plus elle est proche du fonctionnement de la machine. Irrlicht, ou n'importe quel moteur 3D en général, est très haut niveau. Hormis les programmeurs bidouillant l'intérieur des moteurs, la plupart ne savent pas comment ça marche à l'intérieur et se contentent d'utiliser l'interface de programmation fournie telle quel.

Cette interface justement porte un nom qui va revenir très régulièrement dans ce tutoriel : API, pour Application Programming Interface. Comme son nom l'indique une API est l'interface par laquelle le programmeur va "communiquer" avec la bibliothèque. Elle est constituée de toutes les fonctions "publiques" librement accessibles.

Il existe des APIs très bas niveau qui accèdent directement ou presque aux éléments hardware, aux éléments physiques de la machine. En les utilisant comme interface on peut donc accéder indirectement à ces éléments hardware. Ces manipulations sont particulièrement délicates dans le cas d'un moteur portable (i.e. qui fonctionne sur les principaux systèmes d'exploitation), car il faut alors adapter certaines parties du code spécifiques à chaque OS.

Le fenêtrage et la gestion de périphériques est spécifique à chaque OS par exemple. Sous Windows il faut utiliser la WinAPI alors que sous Linux on utilise généralement X Window.

Voici un petit graphique récapitulatif de tout ce que nous venons de voir par niveaux :

Moteur 3D

APIs bas niveau : OpenGL, Direct3D, X, etc...

OS (système d'exploitation)

Hardware (matériel)


Introduction Irrlicht

Irrlicht

Ce qu'est un moteur 3D Mise en place

Irrlicht

Rentrons un peu plus dans le vif du sujet avec une présentation du moteur qui nous intéresse : Irrlicht. Nous n'allons pas tout de suite voir comment l'utiliser mais plutôt commencer par le décrire. Allez hop, trois petits screens au passage pour vous faire envie ^^ (d'autres sont disponibles dans la galerie sur le site officiel [21-1]) :

Image utilisateurImage utilisateurImage utilisateur(Cliquer pour agrandir)

Un peu de culture
Image utilisateur

Le projet Irrlicht débute en 2002 en Autriche. C'est Nikolaus Gebhardt (en photo à droite), programmeur de métier, qui eut l'idée de se lancer dans la réalisation d'un moteur 3D. Depuis le moteur en est à la version 1.7 et de très nombreuses améliorations ont été apportées, dont on peut voir la liste sur le site officiel [21-1].

L'équipe s'est également agrandie puisqu'il y a actuellement heu... trop de membres pour que je les compte. ^^ Mais la liste en est également visible sur le site. Et pour finir une anecdote à ressortir pour briller en société : irrlicht signifie "feu follet" en allemand.

Un peu de technique

Maintenant que nous savons un peu mieux ce qu'est un moteur 3D, une question doit sûrement vous brûler les lèvres :

Pourquoi existe-t-il plusieurs moteurs 3D s'ils font tous la même chose ? Et pourquoi choisir Irrlicht plutôt qu'un autre ?

Tout d'abord il faut bien se rendre compte que tous les moteurs 3D ne font pas la même chose. Ils ont la même fonction, mais certains sont plus performants ou complets que d'autres. Cela doit d'ailleurs vous paraître une évidence si vous jouez régulièrement à des jeux vidéos actuels. Quant au choix d'Irrlicht plutôt qu'un autre moteur, voici quelques arguments intéressants :

Irrlicht et Ogre 3D

La plupart du temps, quand quelqu'un a envie de coder une application 3D en C++ sans réinventer la roue, il en arrive souvent à ce dilemme cornélien : Irrlicht ou Ogre 3D[21-4]. Pour avoir utilisé les deux en profondeur, la principale différence se situe dans l'opposition puissance/simplicité. Irrlicht est de loin plus simple qu'Ogre, mais en contrepartie Ogre est de loin plus puissant qu'Irrlicht.
Par exemple Irrlicht est relativement monolithique, le moteur reste complet peu importe ce qui sera utilisé au final dans l'application, ce qui permet une très grande simplicité dans la mise en place d'un projet. D'un autre coté Ogre est beaucoup plus modulaire, utilisant un système de plugins qui permet de n'utiliser que ce dont on a besoin, mais qui est du coup plus complexe à mettre en place.
Si vous avez un doute sur le moteur que vous voulez utiliser ou que vous êtes juste curieux, le meilleur moyen de se faire une idée est de tester les deux. Il existe justement un tutoriel concernant Ogre 3D sur ce site même [21-5], n’hésitez pas à y jeter un œil. Contrairement à ce que laissent parfois penser les échanges sur les forums, utiliser un moteur ne condamne pas à rejeter les autres, il n'y a pas de dogme à adopter. Prenez le temps nécessaire pour "sentir" celui qui vous plait le plus ou qui correspond le mieux à vos besoins avant de faire un choix (qui ne sera pas définitif de toute manière). ;)


Ce qu'est un moteur 3D Mise en place

Mise en place

Irrlicht Installation

Ce chapitre a pour objectif d'installer Irrlicht et de vérifier qu'il fonctionne. Théoriquement ce n'est pas bien compliqué. Toutefois si vous rencontrez un problème que vous n'arrivez pas à résoudre après avoir bien relu les différentes étapes, vous pouvez le poster sur les forums pour avoir de l'aide.

Installation

Mise en place Configurer un projet

Installation

Comme la plupart des grosses bibliothèques logicielle, Irrlicht est contenu dans un SDK, un Software Development Kit. Il s'agit en clair d'un dossier qui va contenir tout ce dont on a besoin pour développer des applications avec cette bibliothèque. Première étape donc, télécharger la dernière version du SDK à cette adresse : http://irrlicht.sourceforge.net/downloads.html.

Une fois que vous l'avez récupéré et décompressé, jetons un œil à ce qu'il contient :

Pour l'installation proprement dite, seuls 3 dossiers nous intéressent :

Dossier bin

Comme nous l'avons vu plus haut, celui-ci contient les bibliothèques dynamiques. Les fameuses dll sous Windows. Pour les utilisateurs de cet OS justement, il vous suffit de choisir le dossier correspondant à votre configuration et de copier le fichier .dll quelque part. Vous devrez le mettre dans le même dossier que vos fichiers exécutables (.exe) pour que ceux-ci fonctionnent.

Pour les utilisateurs de GNU/Linux c'est encore plus simple, il n'y a pas de bibliothèque dynamique. Ce qui signifie que tout sera compilé en statique dans le programme final. Rien à récupérer dans ce dossier donc.

Dossier include

Comme vous pouvez le constater, le dossier include ne contient pas de sous-dossiers. Les headers sont les mêmes pour tout le monde. Remarquez au passage le fichier irrlicht.h, c'est lui que nous allons inclure dans tous nos programmes. Si vous l'ouvrez vous pourrez voir qu'au début il inclut tous les autres headers du dossier. Ce qui signifie qu'il suffit de rajouter une ligne pour pouvoir utiliser tout le moteur :

#include <irr/irrlicht.h>

Il vous faut maintenant copier l'intégralité du dossier dans le dossier include de votre compilateur. Sous GNU/Linux il s'agit généralement du dossier /usr/include. Pour éviter d'en "mettre partout", il est préférable de créer un dossier spécifique nommé irr par exemple (d'où le irr/irrlicht.h de l'inclusion).

Dossier lib

Dernière étape avec le dossier lib qui contient les bibliothèques statiques. Les utilisateurs de Windows connaissent le chemin, il suffit de choisir le dossier correspondant à sa configuration puis d'en copier le contenu dans le dossier lib de son compilateur.

Les utilisateurs de GNU/Linux en revanche vont devoir faire quelques efforts. En effet si vous regardez dans le sous-dossier Linux, vous verrez que celui-ci est vide. Et qui dit pas de bibliothèque statique dit pas de chocolat compilation possible. Il va donc vous falloir recompiler le moteur pour obtenir le précieux fichier.

Pas de panique, vous trouverez en annexe un petit chapitre expliquant la marche à suivre. Il peut être utile par ailleurs de recompiler le moteur s'il vous prends un jour l'envie de modifier quelque chose dans le code source par exemple. Auquel cas la bibliothèque statique ne sera plus valable.

Une fois que vous avez votre fichier (qui devrait s'appeler libIrrlicht.a), il suffit de le copier dans le dossier /usr/lib/. Et voilà ! Irrlicht est maintenant installé. Ce n'était pas si dur, si ? ;)


Mise en place Configurer un projet

Configurer un projet

Installation Premier projet

Configurer un projet

Avec IDE

Commencez donc par ouvrir votre IDE favori et créez-y un nouveau projet. Même si l'IDE possède des modèles spécifiques à Irrlicht, il serait préférable de partir sur un projet vierge de base. Ensuite, quel que soit le code présent dans le fichier main.cpp, il est bon de le remplacer par le code suivant pour tester que l'environnement fonctionne sans Irrlicht :

#include <iostream>
 
int main(void) {
  std::cout << "Hello world!" << std::endl;
  return 0;
}

Rien de nouveau j'espère, ce code affiche simplement "Hello world!" dans la console. Au cas où un problème survient à ce niveau, l'installation de votre IDE est en cause. Partons du principe que tout fonctionne et ajoutons la ligne qui va inclure tous les headers nécessaires à l'utilisation d'Irrlicht :

#include <irr/irrlicht.h>

Vient enfin l'étape la plus complexe : le "linkage". L'opération consiste à "linker" (lier) la bibliothèque statique d'Irrlicht au compilateur. Il faut donc indiquer au linker le fichier .a ou .lib que nous avons mis tout à l'heure dans le sous-dossier lib du compilateur via l'IDE.

Sous code::blocks par exemple, la manipulation à effectuer est Project->Build options->Linker Settings->add. Il faut ensuite choisir le fichier .a ou .lib. Il est toutefois possible que ce fichier seul ne suffise pas. Bien souvent il faut également indiquer les bibliothèques statiques d'OpenGL, GLU ou encore Xfree86. Si le compilateur vous renvoie un message d'erreur concernant ce problème, le nom de la bibliothèque manquante est souvent indiqué plus ou moins clairement.

Sans IDE

Sans IDE c'est encore plus simple. Utilisez votre éditeur favori pour les codes sources, puis utilisez g++ pour compiler. Si vous avez correctement suivi la partie sur l'installation et que votre configuration est bien faite par ailleurs, la commande suivante devrait faire l'affaire :

g++ source.cpp -lIrrlicht -lGL -o executable

source.cpp est bien sûr le nom du fichier source et executable le nom du binaire. Selon votre plateforme il faudra peut être linker plus de bibliothèque que ces 2 là. L'idéal est de se concocter un petit makefile[10-2] pour pouvoir gérer facilement les options de compilation et les projets à multiples fichiers sources. Faîtes vous plaisir. ;)


Installation Premier projet

Premier projet

Configurer un projet Structures de données

Premier projet

Bien, c'est maintenant qu'on rentre dans le vif du sujet. Sans plus attendre la première ligne de code :

irr::IrrlichtDevice *device = irr::createDevice (
    irr::video::EDT_OPENGL,
    irr::core::dimension2d<irr::u32>(800,600),
    32,
    false,
    true,
    false,
    0);

Ouch ! Oui je sais ça fait peur, mais pas de panique on va tout reprendre point par point. La première des choses est encore de savoir à quoi sert cette ligne. En fait elle va créer un périphérique de sortie, aussi appelé contexte d'affichage, ou plus communément... fenêtre. Qui sera un pointeur de type irr::IrrlichtDevice, et que nous appellerons simplement device. Cette ligne permet donc de créer la fenêtre dans laquelle votre application va se dérouler (et bien plus encore mais on verra ça plus tard ;) ).

Cela a dû vous sauter aux yeux, le namespace irr:: revient sans arrêt. La raison en est simple : dans Irrlicht tout est contenu dans le namespace irr. Personnellement je préfère le retaper à chaque fois mais si cela vous est vraiment insupportable vous pouvez toujours ajouter la ligne suivante au début de votre code :

using namespace irr;

Ce qui est impressionnant dans cette fonction c'est le nombre de paramètres qu'elle prend, 7 en tout. Examinons sa déclaration plus en détail :

irr::createDevice (deviceType, windowSize, bits, fullscreen, stencilbuffer, vsync, receiver)

Reprenons notre première ligne, on s'aperçoit qu'on a créé une fenêtre utilisant OpenGL de 800 x 600 pixels avec 32 bits par pixel ayant un stencil buffer. Il est intéressant de noter que tous les paramètres de cette fonction possèdent des valeurs par défaut (comme indiqué dans la documentation [21-2]).

Les éléments de base

Maintenant que nous avons le device (qui est le véritable nom de la "fenêtre"), nous allons instancier la classe qui communique avec les API 3D bas niveau (OpenGL dans notre cas), j'ai nommé le video driver. C'est lui qui va gérer le rendu par exemple :

irr::video::IVideoDriver* driver = device->getVideoDriver();

Continuons en créant le graphe de scène, aussi appelé scene manager, que nous aborderons plus en détails dans le prochain chapitre. C'est lui qui va nous permettre de gérer tout ce qui est contenu dans la scène :

irr::scene::ISceneManager *sceneManager = device->getSceneManager ();

Pour finir mettons une caméra fixe. Je passe sur les paramètres mais nous reviendrons dessus dans le chapitre suivant le prochain :

sceneManager->addCameraSceneNode (0,
    irr::core::vector3df (0,0,0),
    irr::core::vector3df (5,0,0));
Le rendu

Il ne reste plus maintenant qu'à afficher le rendu. Pour ce faire nous utilisons une boucle qui sera parcourue de manière infinie tant qu'on décide de ne pas fermer l'application :

while(device->run()) {
    driver->beginScene (true, true,
        irr::video::SColor(255,255,255,255));
    sceneManager->drawAll ();
    driver->endScene ();
}

Quelques explications s'imposent. La condition utilisée pour la boucle est le résultat de la méthode run du device. Celle-ci renvoie false quand Irrlicht détecte un signal d’arrêt et true dans tous les autres cas. Ce qui veut dire que cette boucle tournera indéfiniment jusqu'à ce qu'on mette fin au programme.

Il est aussi intéressant de noter que cette boucle ne gère absolument pas le temps. Ce qui signifie qu'une fois lancée, l'application utilisera toute la puissance disponible de votre processeur. En soi ça n'a rien de gênant mais ça peut surprendre.

Décortiquons maintenant cette ligne :

driver->beginScene (true, true, irr::video::SColor(255,255,255,255));

Cette fonction permet d'effectuer un rendu. Elle ne le fait pas elle-même mais est indispensable. En gros elle prépare le terrain pour le nouveau rendu. Voyons un peu son prototype :

IVideoDriver::beginScene (backBuffer, zBuffer, color, videoData, sourceRect)

Maintenant les deux dernières lignes :

sceneManager->drawAll ();
driver->endScene ();

La méthode drawAll du sceneManager va s'occuper du rendu de tout ce qui est géré par le scene manager. Donc de tout ce qui se trouve dans la scène. Et enfin la méthode endScene du driver va afficher ledit rendu à l'écran. Sans elle rien ne s'afficherait.

Pour terminer l'application proprement il nous reste à mettre une dernière ligne, hors de la boucle. Elle permet de détruire le device et donc de libérer la mémoire :

device->drop ();

Voici un récapitulatif du code que vous devriez avoir:

#include <irr/irrlicht.h>
 
int main(void) {

    irr::IrrlichtDevice *device = irr::createDevice( // creation device
        irr::video::EDT_OPENGL,                      // l'API est OpenGL
        irr::core::dimension2d<irr::u32>(800,600),   // taille de la fenetre 800x600
        32, false, true, false, 0);
 
    irr::video::IVideoDriver* driver =
        device->getVideoDriver();                    // creation driver
    irr::scene::ISceneManager *sceneManager =
        device->getSceneManager ();                  // creation scene manager
 
    sceneManager->addCameraSceneNode (0,             // ajout camera fixe
        irr::core::vector3df (0,0,0),
        irr::core::vector3df (5,0,0));

    while (device->run()) {                          // la boucle de rendu
        driver->beginScene(                          // demarre le rendu
            true,                                    // clear back-buffer
            true,                                    // clear z-buffer
            irr::video::SColor(255,255,255,255));    // fond blanc
        sceneManager->drawAll ();                    // calcule le rendu
        driver->endScene ();                         // affiche le rendu
    }

    device->drop ();                                 // liberation de la memoire
    return 0;
}

Un coup de compilateur et que voyez-vous apparaître sous vos yeux ébahis ? Une fenêtre de 800 par 600 toute blanche. Rassurez-vous c'est normal.. Ce code peut être considéré comme le code minimal d'une application Irrlicht, il ne fait rien de particulier et sert de base pour le reste.

Dans le chapitre suivant le prochain nous ajouterons quelques lignes pour ajouter un cube et commencer à faire des choses un peu plus palpitantes. ^^


Configurer un projet Structures de données

Structures de données

Premier projet Arbre binaire

Voici un chapitre entièrement théorique dédié aux structures de données, et plus particulièrement aux arbres. D'une manière générale ceux-ci sont très importants en programmation, mais en ce qui nous concerne ils sont absolument indispensable puisque c'est grâce à un arbre nommé graphe de scène qu'Irrlicht organise tout le contenu d'une scène 3D.

Arbre binaire

Structures de données Octree

Arbre binaire

Pour ceux qui ne le savent pas déjà, une structure de données est juste une technique de programmation permettant d'organiser des données d'une certaine manière dans le but d'y accéder plus facilement et / ou plus rapidement.

Un tableau par exemple est une structure de données. En effet dans un tableau (je parle d'un tableau en langage C++ bien sûr) les données sont rangées de façon contiguë, ce qui permet de les retrouver plus facilement. Il est bien plus simple de gérer un tableau de 20 cases que de gérer 20 variables indépendantes.

Il existe beaucoup de structures de données différentes en C++ dont la plupart n'existent pas nativement. La bibliothèque tierce Boost par exemple contient énormément de conteneurs permettant d'organiser des données de manière intéressante [33-2].

Mais pour l'instant donc, intéressons nous aux arbres binaires. La première chose à savoir est qu'ils portent bien leur nom dans la mesure où il s'agit d'une structure de donnée arborescente. C'est à dire qui peut être représentée avec des feuilles, des nœuds et une racine. Voici à quoi ressemble un arbre binaire graphiquement :

Image utilisateur

Et maintenant un peu de vocabulaire. Le rond rose est la racine de l'arbre. Un arbre n'a toujours qu'une seule et unique racine. Tous les ronds violets sont appelés nœuds. Ils dépendent d'un nœud (ou d'une racine) au dessus d'eux mais ont également 1 ou 2 fils. Quand un élément de l'arbre possède un ou deux éléments inférieurs, ceux-ci sont appelés fils. Et comme il y a déjà assez de mots à retenir comme ça, on les appelle chacun gauche et droit.

Vous l'aurez deviné, dans l'autre sens on appelle l'élément père. Le rond violet le plus à gauche est donc le père du rond vert le plus à gauche, qui est son fils gauche. Et les ronds verts sont les feuilles bien sûr. Elles portent bien leur nom puisque ce sont ce sont les extrémités de l'arbre. Ce qui implique qu'elles n'aient pas de fils (sinon ce serait des nœuds ;) ).

La distance entre la racine et la dernière feuille (le nombre de niveaux) est appelée la hauteur de l'arbre. Ici nous avons un arbre de hauteur 4. Et la distance entre la racine et un nœud est appelée profondeur.

Voilà vous connaissez maintenant l'essentiel sur les arbres binaires. Il existe de nombreuses autres choses à savoir sur le sujet [33-1] mais ce n'est pas particulièrement important pour ce tutoriel. Le principal est que vous ayez compris le concept et les principaux termes qui s'y rapportent.


Structures de données Octree

Octree

Arbre binaire Graphe de scène

Octree

Un octree est également une structure de données arborescente. La plupart du temps elle est utilisée pour partitionner l'espace. C'est à dire diviser un espace en morceaux plus petits, pour réduire les temps de calculs notamment.

Si nous regardons de plus près le mot OCTREE, on s’aperçoit qu'il est composé des deux parties suivantes :

Un octree est donc tout simplement un arbre dont les noeuds ont 8 fils. De manière graphique ça donne le résultat suivant :

Image utilisateur

Je suppose que vous comprendrez pourquoi je n'ai pas représenté le second niveau... L'avantage d'avoir 8 fils par nœuds devient évident quand on pense à la manière dont est stocké un espace en informatique. En clair, il s'agit systématiquement d'un parallélépipède rectangle, qu'il est facile de diviser en 8 parts égales. Comme sur l'images suivante :

Image utilisateur

Si vous avez bien suivi vous devriez même être capable de me donner la hauteur de l'arbre. Niveau 1 en effet, l'espace de la scène est divisé une fois par 8. Maintenant que l'espace est partitionné en 8 le moteur 3D va pouvoir réduire les temps de calcul.

Imaginez par exemple que la caméra soit placée dans la scène de telle manière que 5/8 de l'espace total lui soit cachés. Au moment du rendu, un petit test sur l'octree et hop, on oublie ces 5/8. On ne calcule le rendu que pour les 3/8 visibles, ceux qui apparaîtront à l'écran. On peut également s'en servir pour faire de la détection de collision d'autres choses encore.

Rien n'empêche ensuite d'augmenter le niveau niveau de l'arbre. On peut avoir un octree de taille très élevée, le principe ne change pas. Voici ce que donne un octree d'une hauteur de 2 (je n'ai redivisé que le cube ayant le plus de faces visibles... :-° ) :

Image utilisateur

Il y a quand même une petite remarque à faire sur la hauteur que peut avoir un octree. En effet, diviser l'espace c'est bien, mais il arrive un moment où ça devient contre-productif. Pour reprendre l'exemple de la caméra, si on fait un octree d'une hauteur de 4, on aura un espace divisé en 4096 (84). Imaginons que la caméra n'ait dans son champ que 2500 des 4096 morceaux.

Si nous augmentons le niveau de l'octree cela nous donne 32768 morceaux. Il est possible que les quelques morceaux que l'on ne calculerais pas avec une hauteur de 5 vis à vis d'une hauteur de 4 soient négligeables. Tout dépend de la taille de votre espace et de ce qu'il y a dedans. Il est possible aussi que le temps qu'on passe à parcourir l'octree soit supérieur au temps qu'on aurait gagné en le faisant plus petit.

Quoi qu'il en soit tout cela ne nous concerne pas vraiment pour l'instant puisqu'une fois encore, Irrlicht va gérer tout ça en interne et nous n'aurons pas besoin de nous en occuper. :)


Arbre binaire Graphe de scène

Graphe de scène

Octree Scene nodes et caméras

Graphe de scène

Abordons maintenant ce qui nous intéresse le plus avec l'outil qui organise absolument tout ce qui est mis dans une scène Irrlicht : le graphe de scène (que j'appellerai aussi et surtout scene manager). Quand on a compris le principe des arbres, le scene manager est d'une simplicité et d'une efficacité effarante. ^^

La première chose à savoir est qu'il s'agit là aussi d'une structure de données arborescente. Mais alors que les arbres binaires ne pouvaient avoir que deux fils par noeuds et les octrees 8, le scène manager n'a pas de nombre limite. Regardons un exemple :

Image utilisateur

Alors, vous comprenez déjà un peu mieux ? Le scene manager sert à organiser tout ce qui compose la scène de manière hiérarchique. Et pas seulement ce qui apparaîtra à l'écran comme vous pouvez le voir avec le node "camera". La racine de l'arbre englobe toute la scène, puis chaque nœud correspond à un élément précis.

Un autre élément très important concernant le scene manager est le fait que les fils héritent des propriétés spatiales de leur père. Et nous nous servirons principalement de cela pour faire des modifications sur les positions et orientations des nœuds. En effet à chaque nœud est associée une matrice qui représente ses transformations par rapport à la matrice de son père. Et à chaque fois que la matrice d'un noeud change, tous ses fils subissent donc automatiquement les transformations.

Ce qui signifie que si on effectue une rotation de 90° sur le nœud "salle", les nœuds "armoire", "objet" et "perso" pivoteront eux aussi de 90°. En revanche si on effectue une rotation de 90° sur le nœud "armoire", seul le nœud "objet" pivotera aussi de 90°. Et c'est là qu'on s'aperçoit qu'il ne faut pas faire n'importe quoi dans son scene manager. Si vous faites en sorte qu'une salle dépende d'un objet qui s'y trouve plutôt que l'inverse, il risque de vous arriver des bricoles... :D


Octree Scene nodes et caméras

Scene nodes et caméras

Graphe de scène Ce qu'est un scene node

Dans ce chapitre nous allons voir les derniers éléments manquant avant de pouvoir réaliser notre première véritable scène 3D. Humble certes, mais véritable quand même. :D

Lisez le attentivement dans la mesure où la plupart de ce qui s'y trouve est fondamental et nous servira pour toute la suite du tutoriel dans la compréhension de ce qui se passe.

Ce qu'est un scene node

Scene nodes et caméras Types de données propres à Irrlicht

Ce qu'est un scene node

Comme nous l'avons vu lors du précédent chapitre, le scene manager va gérer le contenu de la scène. Pour ce faire il stocke chaque élément dans un nœud (node) et organise ceux-ci de manière arborescente. Un scene node (littéralement "nœud de scène") est donc un élément du scene manager qui va contenir un objet de la scène.

Il existe beaucoup de types de scene node différents. Une caméra sera par exemple stockée dans un node de type ICameraSceneNode alors qu'une lumière sera stockée dans un node de type ILightSceneNode. Tous les scene nodes d'Irrlicht sont des dérivés d'une classe de base nommée ISceneNode, ce qui permet au scene manager de les gérer "simplement".

Ce qu'il faut retenir ici pour l'instant est que chaque élément de la scène est contenu dans un node et que c'est celui-ci qu'on manipulera la plupart du temps. Le node agit comme interface pour communiquer avec l'élément qu'il contient.

Organisation d'une scène

De par la nature arborescente du scene manager, chaque node doit posséder un node père et peut posséder un nombre potentiellement infini de node fils. Irrlicht met donc automatiquement en place un node servant de racine dans chaque scène. Celui-ci se nomme le root scene node, c'est le premier, tout en haut de la structure.

Lors de la création d'un scene node, le scene manager requiert un pointeur vers le père pour organiser les choses. Si on ne précise rien le node créé est automatiquement rattaché au root scene node. Certains types de node restent quasi-systématiquement au niveau de la racine. Les caméras par exemple ne dépendent que très rarement de quoi que soit.

Identification des nodes

Pour ajouter un node au scene manager, il faut systématiquement passer par une méthode de celui-ci, qui renverra un pointeur vers le node si tout se passe bien. La convention de nommage étant bien faite, le prototype d'une méthode d'ajout de node ressemblera toujours à ça :

I_X_SceneNode* ISceneManager::add_X_SceneNode (
    ISceneNode* parent, // pointeur vers le noeud pere
    s32 id)             // numero d'identification du noeud

Ces deux arguments sont systématiquement présents, quel que soit le node à ajouter. Si la valeur 0 est passée pour parent, le node est automatiquement rattaché au root scene node. Nous reviendrons plus loin sur le numéro d'ID. Dans ce prototype les _X_ représentent le type de scene node. Par exemple pour une caméra statique cela donne la déclaration que nous avons croisé dans le deuxième chapitre :

ICameraSceneNode* ISceneManager::addCameraSceneNode (
    ISceneNode* parent,        // pointeur vers le noeud pere
    core::vector3df& position, // position de la camera
    core::vector3df& lookat,   // point de mire de la camera
    s32 id,                    // numero d'identification du noeud
    bool makeActive)           // true = rend la camera active

Cela nous laisse donc deux grandes façons de retrouver et d'accéder aux scènes nodes :

Il existe bien d'autres méthodes pour récupérer un accès à un ou plusieurs nodes du scene manager, mais elles sont pour la plupart imprécises et difficiles à mettre en œuvre. Le plus simple reste généralement de récupérer le pointeur donné lors de la création et de le stocker quelque part où il est simple d'y accéder. En faire un attribut de classe par exemple.


Scene nodes et caméras Types de données propres à Irrlicht

Types de données propres à Irrlicht

Ce qu'est un scene node Les caméras

Types de données propres à Irrlicht

Irrlicht déclare bien entendu énormément de classes, de structure, d'énumérations, etc... qui lui sont propres. Mais il redéfinit aussi une bonne partie des types de données natifs du langage C++ (int, long, etc...). De plus, il propose d'autres types basiques qui sont indispensables à l'utilisation du moteur. Regardons-y de plus près :

Types natifs

Pour des raisons de compatibilité maximale, Irrlicht encapsule les types natifs du C++. Ainsi quelle que soit la plateforme sur laquelle on compile et les APIs bas niveau utilisées, le code source sera compatible sans devoir changer quoi que ce soit. Voici une liste des types de variables de base les plus courants utilisés par Irrlicht :

Couleur

Je rappelle brièvement pour ceux qui ne le savent pas déjà que la couleur est la plupart du temps définie en informatique par la notation RGB[31-4]. C'est à dire que n'importe quelle couleur est définie par la combinaison des trois couleurs de base : rouge, vert et bleu. Plus une quatrième composante alpha correspondant à la transparence.

Irrlicht propose donc une classe permettant d'exprimer une couleur dans une seule variable au lieu de 4. Il s'agit de irr::video::SColor[doc], que nous avons déjà croisé dans la boucle de rendu. Cette classe stocke les composantes de manière ARGB, c'est à dire que la transparence se trouve avant le rouge, puis le vert, puis le bleu. Le code suivant permet de créer une variable contenant une couleur :

irr::video::SColor color(0, 255, 255, 255);

Ici nous avons une variable color contenant un blanc totalement transparent. Pour avoir un bleu pur totalement opaque, il faut donner les valeurs suivantes :

irr::video::SColor color(255, 0, 0, 255);
Vecteurs 2D
vecteur_2D

Un vecteur représente une direction, un déplacement [52-1]. Graphiquement un vecteur 2D ressemble à ce qui est visible sur le graphique de droite. Cet exemple montre un vecteur ayant pour valeurs 1 en X (l'axe des abscisses) et 1 en Y (l'axe des ordonnées).

La classe définie par Irrlicht pour contenir les deux valeurs d'un vecteur en deux dimensions est irr::core::vector2d<T>[doc]. Vous aurez reconnu le template qui nous permet d'utiliser quasiment n'importe quel type de base vu plus haut pour les valeurs. Pour créer le vecteur du graphique de droite par exemple, il suffit d'utiliser le code suivant :

irr::core::vector2d<irr::u32> vec(1, 1);

Et pour l'avoir en nombres flottants, le code suivant :

irr::core::vector2d<irr::f32> vec(1.0f, 1.0f);

Étant donné qu'un vecteur est défini par deux valeurs, une sur chaque axe, la classe vector2d est parfaite pour stocker des coordonnées. Les opérations entre vecteurs et coordonnées sont ainsi simplifiées, et l'utilisation de la classe est sensiblement la même quel qu'en soit la nture du contenu. À chaque fois qu'on a besoin de spécifier la position d'un node par exemple, on utilise un vecteur (3D).

Vecteurs 3D

Le principe est exactement le même que pour les vecteurs 2D sauf qu'on rajoute une dimension, donc un axe, donc une valeur. Visuellement le vecteur (1, 1, 1) donne ce qui est visible sur les graphiques ci-dessous :

vecteur_3D_1vecteur_3D_2vecteur_3D_3La classe en question est irr::core::vector3d<T>[doc]. Elle fonctionne de la même manière que vector2d. Ce qui veut dire que le code suivant permet d'obtenir le vecteur (1, 1, 1) :

irr::core::vector3d<irr::u32> vec(1, 1, 1);

La plupart du temps, Irrlicht requiert des valeurs en nombre flottant concernant les vecteurs. Cela permet plus de précision, notamment pour indiquer des coordonnées. On utilise les vecteurs 3d en flottant tellement souvent qu'un typedef a été mis en place pour éviter d'avoir à taper vector3d<irr::f32>. Il s'agit de vector3df. Pour indiquer le vecteur ci-dessus en nombre flottant, il suffit donc de faire :

irr::core::vector3df vec(1.0f, 1.0f, 1.0f);

Ce qu'est un scene node Les caméras

Les caméras

Types de données propres à Irrlicht Création d'une scène

Les caméras

Il ne nous manque plus grand chose avant de passer à la création de la scène proprement dite, si ce n'est l'indispensable objet : la caméra. Il en existe nativement trois types dans Irrlicht. Passons-les en revue sans plus attendre :

La caméra statique

C'est la caméra de base et comme son nom l'indique, il n'y a rien de prévu pour qu'elle bouge. Nous verrons plus tard lors du chapitre dédié aux caméras et leurs animators que tout est possible et que par un miracle de la technologie moderne une caméra statique peut devenir mobile. Mais d'ici là on va considérer qu'elle ne peut pas bouger. ;)

La caméra Maya

Elle est appelée ainsi car elle reprend le même système que la caméra utilisée dans le logiciel de modélisation 3D Maya. A savoir : l'orientation avec le clic gauche et les déplacements avec le droit.

La caméra FPS

Voici le type de caméra qui nous intéresse pour cet exemple. Son utilisation est très intuitive et est reprise par beaucoup de jeux vidéo. À savoir l'orientation via la souris et les déplacements via des touches du clavier. Comme les meshs, les caméras sont gérées par le scene manager et sont stockées dans un node. Voici la fonction en question :

ISceneManager::addCameraSceneNodeFPS (
 ISceneNode * parent,       // le noeud parent de la caméra
 f32 rotateSpeed,           // la vitesse de rotation de la caméra
 f32 moveSpeed,             // la vitesse de déplacement
 s32 id,                    // numéro d'identification du noeud
 SKeyMap * keyMapArray,     // une map permettant de re-affecter les touches
 s32 keyMapSize,            // taille de la keyMap
 bool noVerticalMovement,   // autorise ou non les mouvements sur l'axe vertical
 f32 jumpSpeed,             // vitesse de déplacement lors d'un saut
 bool invertMouse,          // inverse ou non la rotation de la caméra
 bool makeActive)           // indique si la caméra doit être active ou non

Le seul argument qui devrait vous surprendre est la keymap. Une keymap est comme son nom l'indique une "carte" permettant d'assigner des touches à certaines actions. Par défaut la caméra propose les commandes suivantes :

Action

Touche

Description

EKA_MOVE_FORWARD

KEY_UP

flèche du haut pour déplacement en avant

EKA_MOVE_BACKWARD

KEY_DOWN

flèche du bas pour déplacement en arrière

EKA_STRAFE_LEFT

KEY_LEFT

flèche de gauche pour déplacement à gauche

EKA_STRAFE_RIGHT

KEY_RIGHT

flèche de droite pour déplacement à droite

EKA_JUMP_UP

KEY_KEY_J

touche j pour un saut

Utiliser les flèches directionnelles comme touches permettant de se déplacer n'est pas très agréable. En utilisant les paramètres 5 et 6 on peut ré-assigner les commandes aux touches qui nous intéressent. Pour associer une action à une touche il faut utiliser une structure nommée SKeyMap qui comporte 2 éléments :

Supposons qu'on utilise une bonne vieille configuration w pour avancer, s pour reculer, a pour aller à gauche, d pour aller à droite et barre espace pour sauter. J'ai un clavier qwerty ceci dit, utilisez ce qui vous convient le mieux. Le code de la keymap correspondante sera le suivant :

irr::SKeyMap keyMap[5];
keyMap[0].Action = irr::EKA_MOVE_FORWARD;   // avancer
keyMap[0].KeyCode = irr::KEY_KEY_W;
keyMap[1].Action = irr::EKA_MOVE_BACKWARD;  // reculer
keyMap[1].KeyCode = irr::KEY_KEY_S;
keyMap[2].Action = irr::EKA_STRAFE_LEFT;    // a gauche
keyMap[2].KeyCode = irr::KEY_KEY_A;
keyMap[3].Action = irr::EKA_STRAFE_RIGHT;   // a droite
keyMap[3].KeyCode = irr::KEY_KEY_D;
keyMap[4].Action = irr::EKA_JUMP_UP;        // saut
keyMap[4].KeyCode = irr::KEY_SPACE;

Rien de compliqué si vous avez compris ce qui précède. On crée un tableau pour contenir les instances de SKeyMap. Il ne reste plus qu'à le passer en argument lors de la création de la caméra :

sceneManager->addCameraSceneNodeFPS (0, 100.0f, 0.1f, -1, keyMap, 5);

Il est important de préciser la taille exacte de la keymap, autrement il y a de fortes chances qu'elle ne fonctionne pas correctement. En revanche il est inutile de préciser les derniers arguments si leur valeur par défaut correspond à ce qui nous arrange. Ce qui est le cas en l’occurrence.


Types de données propres à Irrlicht Création d'une scène

Création d'une scène

Les caméras Modèles 3D

Création d'une scène

Maintenant que nous avons tout ce qu'il faut pour réaliser une scène, il ne reste plus qu'à trouver quoi mettre dedans. On va commencer petit avec un cube de test que nous allons ajouter via le scene manager. La méthode [doc] est la suivante :

IMeshSceneNode* ISceneManager::addCubeSceneNode (
    f32 size,                  // longueur d'une arrete
    ISceneNode* parent,        //
    s32 id,                    // 
    core::vector3df& position, // position du cube
    core::vector3df& rotation, // orientation du cube
    core::vector3df& scale))   // echelle du cube

Pour achever le tout il nous manque deux lignes de code qui vont faire office de touche finale. La première sert à rendre le curseur invisible, et la deuxième à afficher le cube en mode filaire (nous verrons dans le prochain chapitre ce que cela signifie) :

device->getCursorControl ()-> setVisible (false); // curseur invisible
cube->setMaterialFlag(irr::video::EMF_WIREFRAME, true);

Le code complet final donne donc :

#include <irr/irrlicht.h>


int main(void) {
 
    irr::IrrlichtDevice* device = irr::createDevice(  // creation du device
        irr::video::EDT_OPENGL,                       // API = OpenGL
        irr::core::dimension2d<irr::u32>(200,200),    // taille fenetre 640x480p
        32);                                          // 32 bits par pixel

    irr::video::IVideoDriver* driver =                // video driver
        device->getVideoDriver ();
    irr::scene::ISceneManager* sceneManager =         // scene manager
        device->getSceneManager ();
 
    device->getCursorControl ()-> setVisible (false); // curseur invisible
 
    
    /* CUBE */

    irr::scene::IMeshSceneNode* cube =         // pointeur vers le node
        sceneManager->addCubeSceneNode(        // la creation du cube
            10.0f,                             // cote de 10 unites
            0,                                 // parent = racine
            -1,                                // pas d'ID
            irr::core::vector3df(              // le vecteur de position
                0.0f,                          // origine en X
                0.0f,                          // origine en Y
                20.0f));                       // +20 unites en Z
    
    cube->setMaterialFlag(irr::video::EMF_WIREFRAME, true);
    

    /* CAMERA */
    
    irr::SKeyMap keyMap[5];                    // re-assigne les commandes
    keyMap[0].Action = irr::EKA_MOVE_FORWARD;  // avancer
    keyMap[0].KeyCode = irr::KEY_KEY_W;        // w
    keyMap[1].Action = irr::EKA_MOVE_BACKWARD; // reculer
    keyMap[1].KeyCode = irr::KEY_KEY_S;        // s
    keyMap[2].Action = irr::EKA_STRAFE_LEFT;   // a gauche
    keyMap[2].KeyCode = irr::KEY_KEY_A;        // a
    keyMap[3].Action = irr::EKA_STRAFE_RIGHT;  // a droite
    keyMap[3].KeyCode = irr::KEY_KEY_D;        // d
    keyMap[4].Action = irr::EKA_JUMP_UP;       // saut
    keyMap[4].KeyCode = irr::KEY_SPACE;        // barre espace

    sceneManager->addCameraSceneNodeFPS(       // ajout de la camera FPS
        0,                                     // pas de noeud parent
        100.0f,                                // vitesse de rotation
        0.1f,                                  // vitesse de deplacement
        -1,                                    // pas de numero d'ID
        keyMap,                                // on change la keymap
        5);                                    // avec une taille de 5
    
    
    /* RENDU */
    
    irr::video::SColor color(                  // contient la couleur blanc
        255,                                   // composante A alpha (transparence)
        255,                                   // composante R rouge
        255,                                   // composante G verte
        255);                                  // composante B bleue
    
    while (device->run()) {                    // la boucle de rendu
        driver->beginScene(true, true, color); // demarre le rendu
        sceneManager->drawAll ();              // calcule le rendu
        driver->endScene ();                   // affiche le rendu
    }
 
    device->drop ();                           // libere la memoire
    return 0;
}

Pour ceux qui n'admirent pas le résultat directement sur leur écran, voici un aperçu de ce que ça donne :

cube_1cube_2cube_3cube_4


Les caméras Modèles 3D

Modèles 3D

Création d'une scène Ce qu'est un modèle 3D

Maintenant que nous avons vu toutes les bases nécessaires à la création d'une scène, il est temps d'y ajouter quelque chose d'indispensable : les modèles 3D.

Ce chapitre comporte une grosse partie théorique dans la mesure où Irrlicht gère de son coté énormément de chose et qu'il serait dommage de s'en servir sans comprendre au moins les bases de ce qui se passe.

Ce qu'est un modèle 3D

Modèles 3D Fichiers et format 3D

Ce qu'est un modèle 3D

Comment représenter un objet dans un espace en 3 dimensions quand on part de rien ? La réponse courte est : en le numérisant. Puisqu'il est bien sûr impossible de reproduire la structure physique réelle de l'objet, c'est à dire tous les atomes qui le composent, on se contente de reproduire sa surface avec plus ou moins de détails.

Pour ce faire on crée pleins de petites surfaces plates qui seront stockées dans un fichier, puis exploité par le moteur pour être rendues à l'écran. Une fois assemblées comme il faut, ces petites surfaces (qu'on appelle polygones) auront la même apparence que l'objet reproduit. Enfin plus ou moins... Et bien entendu, plus le nombre de polygones est élevé et plus le modèle 3D est fin, donc ressemblant à l'objet réel.

Pour créer l'ensemble de ces surfaces, on définit un certain nombre de points qu'on assemble ensuite d'une certaine manière. Prenons par exemple les graphiques suivants (réalisés grâce à gnuplot [11-1]) :

points_3D
cube_static

Le graphique de gauche montre un espace en 3 dimensions dans lequel sont indiqués 8 points correspondant aux sommets d'un cube (même si la perspective n'est pas parfaitement isométrique). En reliant ces points d'une certaine manière on obtient effectivement un cube comme le montre le graphique de droite. L'ensemble des points et lignes forme ce qu'on appelle un maillage (mesh en anglais).

Ainsi pour créer un mesh, il suffit de placer suffisamment de sommets (vertex en anglais au singulier et vertices au pluriel) aux bons endroits puis de les relier de la bonne façon pour pouvoir reproduire à peu près tout ce qu'on veut. Il y a une petite contrainte tout de même, pour des raisons de performances les meshs utilisés dans les applications 3D temps réel sont toujours faits avec des triangles uniquement. Ce qui donne pour notre cube :

cube_final_anim

Notez que le nombre de sommets n'a pas changé, on les a juste relié de façon différente. Pour représenter un cube il faut donc au minimum 8 sommets et 12 triangles.


Modèles 3D Fichiers et format 3D

Fichiers et format 3D

Ce qu'est un modèle 3D Animation d'un modèle

Fichiers et format 3D

Maintenant que nous savons comment représenter un mesh dans l'espace, il reste le problème du stockage. En effet il faut bien que les coordonnées des vertices et la façon de les relier soit stockés quelque part pour que le moteur puisse y accéder. Il est possible de l'indiquer directement dans le code source de l'application mais c'est loin d'être pratique. On utilise donc des fichiers pour cela et il en existe pléthore de types différents. La liste complète des formats supportés par Irrlicht est disponible sur le site officiel. En vrac parmi les plus connus :

Il y a plusieurs choses qui les différencient les uns des autres. Comme nous l'avons vu plus haut, un modèle 3D ce n'est pas juste un mesh, il y a aussi la texture par exemple qui est l'image qui recouvrira la surface. Par exemple, voici un même mesh successivement en mode filaire (wireframe en anglais), solide (solid) mais sans texture, puis enfin avec texture (le tout sans éclairage) :

sydney_wireframesydney_solidsydney_texture

Certains fichiers vont contenir les textures alors que d'autres non. Certains vont contenir des animations pour les modèles. Certains fichiers sont binaires, d'autres textes. Et aussi chaque format de fichier possède sa propre syntaxe et organisation. Prenons le format obj [63-1] par exemple, qui est l'un des plus simples qui soit. La description d'une surface carrée donnera le fichier suivant, dont une représentation graphique est visible à droite :

surface_carree
# Les lignes commençant par # sont des commentaires

# Les lignes commençant par v donnent
# les coordonnées des sommets
v 0.0 0.0 0.0  # v1
v 1.0 0.0 0.0  # v2
v 1.0 0.0 1.0  # v3
v 0.0 0.0 1.0  # v4

# Les lignes commençant par f indiquent quels
# sommets relier pour former les triangles
f 1 2 3
f 3 4 1

Ce qu'est un modèle 3D Animation d'un modèle

Animation d'un modèle

Fichiers et format 3D Chargement et utilisation

Animation d'un modèle

Intéressons-nous maintenant à l'animation des modèles 3D. Il existe 2 grandes façons d'animer un modèle :

Animation par key-frames

C'est historiquement la plus ancienne. Elle consiste à afficher le mesh dans un certain état à un moment t puis à l'afficher dans un état différent au moment t+1. En réalité il ne s'agit plus du même mesh puisque tous les sommets sont redéfinis, mais puisque les coordonnées des sommets qui changent de position varient très peu d'une frame à l'autre, on a l'illusion d'un mouvement (plus ou moins) fluide.

Une animation est donc composée de plusieurs frames. Une frame correspondant à un moment précis, un mesh précis. La plupart du temps (en théorie tout le temps mais sait-on jamais...), le nombre de rendus effectués par seconde est largement supérieur au nombre de frames du modèle disponible pour une période donnée. Pour éviter les saccades il faut alors interpoler les coordonnées des vertices entre chaque frame, ce qui donne parfois une désagréable sensation de flottement de la surface du mesh.

C'est à cause de cette interpolation qu'on appelle cette technique key-frames. Au regard du nombre de rendus effectués par seconde, le modèle ne possède que quelques frames clefs.

L'animation squelettale

squelette_blenderQui porte bien son nom puisque le principe est de créer un squelette autour duquel va s'articuler le mesh. Cette technique est bien plus évoluée que l'animation par key-frames et donne de bien meilleurs résultats. Hélas, on n'a jamais rien sans rien et elle est aussi beaucoup plus difficile à mettre en oeuvre...

Le screen de droite montre un mesh et son squelette sous le logiciel Blender [11-3]. Le modèle a été créé par pfunked et se trouve sous licence CC-By-Sa : http://opengameart.org/content/walk-cycles.

La plupart des jeux vidéos récents implémentent des modèles animés par squelettes. Ce type d'animation possède des avantages non négligeables dont les plus évidents sont de gagner beaucoup de place et de proposer une gestion plus réaliste des animations. On gagne de la place car pour rendre une animation il "suffit" de stocker les coordonnés des articulations du squelette au fil du temps au lieu de stocker les coordonnées de tous les vertices du mesh.

De par leur nature, il est impossible de modifier les animations par key-frame à la volée. Comment recalculer les nouvelles coordonnées des sommets ? L'animation squelettale permet de répondre à ce problème, et par conséquent d'ouvrir la voie à une gestion physique de la déformation des meshs en temps réel. Imaginez par exemple un personnage qui réagit en direct de manière cohérente à un évènement physique, c'est possible. Comme le prouve la vidéo suivante illustrant une technologie notamment utilisée dans les jeux vidéo GTA 4 et Red dead redemption [12-1] :

http://www.youtube.com/v/87qdmuOesRs


Fichiers et format 3D Chargement et utilisation

Chargement et utilisation

Animation d'un modèle Réalisation de la scène

Chargement et utilisation

Image utilisateur

Le modèle dont nous allons nous servir s'appelle Sydney (visible à droite). Il s'agit d'un fichier .md2 [62-x], un type créé pour le jeu Quake II et animé par key-frames. Vous pourrez le trouver dans le dossier media du SDK. La méthode permettant de charger un modèle est la suivante :

IAnimatedMesh* ISceneManager::getMesh (filename)

On spécifie le nom du fichier contenant le modèle via l'argument filename, et celui-ci est chargé en mémoire et accessible via le pointeur de type IAnimatedMesh renvoyé par la méthode. Mais, un mesh n'est pas un node... Pour le moment nous avons simplement chargé le modèle, nous ne l'avons pas encore ajouté à la scène. Pour ce faire il faut utiliser la méthode suivante :

IAnimatedMeshSceneNode* ISceneManager::addAnimatedMeshSceneNode (
    IAnimatedMesh * mesh,              // pointeur vers le mesh
    ISceneNode * parent,               // noeud parent de celui-ci
    s32 id,                            // numero d'ID du noeud
    const core::vector3df & position,  // position du mesh
    const core::vector3df & rotation,  // orientation du mesh
    const core::vector3df & scale,     // echelle du mesh
    bool alsoAddIfMeshPointerZero      // true = ajoute le noeud meme si le
)                                      // pointeur vers le mesh est mauvais

Combinons les deux fonctions que nous venons de voir pour créer le node contenant sydney :

irr::scene::IAnimatedMeshSceneNode *sydney =    // cree un node nomme sydney
    sceneManager->addAnimatedMeshSceneNode (    // via le scene manager
        sceneManager->getMesh ("sydney.md2"));  // contenant le mesh "sydney.md2"

Remarquez que nous avons laissé toutes les valeurs par défaut pour la fonction addAnimatedMeshSceneNode sauf en ce qui concerne le mesh. A ce propos justement, il est important de noter que la chaîne de caractère spécifiant le nom du fichier spécifie également son emplacement. Pour que le code ci-dessus fonctionne, il faut que le fichier "sydney.md2" se trouve dans le même répertoire que l’exécutable. Dans le cas contraire il faut indiquer le chemin vers le fichier en même temps que son nom.

Ajustements

Et maintenant occupons-nous de l'animation. Comme nous l'avons vu plus haut, les fichiers .md2 sont animés par key-frames. Il est donc possible d'animer le modèle en spécifiant les numéros des frames à jouer. Mais il est également possible pour les modèles MD2 spécifiquement de préciser un nom d'animation standard, ce que nous allons faire.

sydney->setMD2Animation(irr::scene::EMAT_STAND);

Et voilà. Nous jouerons l'animation STAND. Une liste de toutes les animations standards possibles est disponible dans la documentation. Nous n'allons pas tout de suite expliquer les deux lignes suivantes en détail, mais sachez simplement pour le moment qu'elles servent respectivement à ne pas prendre en compte l'éclairage et à appliquer une texture au modèle :

sydney->setMaterialFlag(irr::video::EMF_LIGHTING, false);
sydney->setMaterialTexture( 0, driver->getTexture("sydney.bmp") );

Animation d'un modèle Réalisation de la scène

Réalisation de la scène

Chargement et utilisation Materials

Réalisation de la scène

Bon et bien nous avons tout ce qu'il nous faut. A vos claviers ! Voici le code complet pour ceux qui ont raté un épisode :

#include <irr/irrlicht.h>


int main(void) {
 
  irr::IrrlichtDevice* device = irr::createDevice(    // creation du device
    irr::video::EDT_OPENGL,                           // API = OpenGL
    irr::core::dimension2d<irr::u32>(640,480),        // taille fenetre 640x480p
    32);                                              // 32 bits par pixel
  irr::video::IVideoDriver* driver =                  // creation du driver video
    device->getVideoDriver ();    
  irr::scene::ISceneManager* sceneManager =           // creation du scene manager
    device->getSceneManager ();
  device->getCursorControl ()-> setVisible (false);   // rend le curseur invisible
 
  
  /* MODELE */
    
  irr::scene::IAnimatedMeshSceneNode *sydney =        // cree un scene node nomme sydney
    sceneManager->addAnimatedMeshSceneNode (          // via le scene manager
      sceneManager->getMesh ("sydney.md2"));          // en chargeant le mesh "sydney.md2"
      
  sydney->setMaterialFlag(
    irr::video::EMF_LIGHTING, false);
  sydney->setMaterialTexture(
    0, driver->getTexture("sydney.bmp"));
 
  sydney->setMD2Animation(irr::scene::EMAT_STAND);    // joue l'animation STAND en boucle
  

  /* CAMERA */
    
  irr::SKeyMap keyMap[5];                             // re-assigne les commandes
  keyMap[0].Action = irr::EKA_MOVE_FORWARD;           // avancer
  keyMap[0].KeyCode = irr::KEY_KEY_W;                 // w
  keyMap[1].Action = irr::EKA_MOVE_BACKWARD;          // reculer
  keyMap[1].KeyCode = irr::KEY_KEY_S;                 // s
  keyMap[2].Action = irr::EKA_STRAFE_LEFT;            // a gauche
  keyMap[2].KeyCode = irr::KEY_KEY_A;                 // a
  keyMap[3].Action = irr::EKA_STRAFE_RIGHT;           // a droite
  keyMap[3].KeyCode = irr::KEY_KEY_D;                 // d
  keyMap[4].Action = irr::EKA_JUMP_UP;                // saut
  keyMap[4].KeyCode = irr::KEY_SPACE;                 // barre espace

  sceneManager->addCameraSceneNodeFPS(                // ajout de la camera FPS
    0, 100.0f, 0.1f, -1, keyMap, 5);


    /* RENDU */
    
  while (device->run()) {                             // la boucle de rendu
    driver->beginScene(true, true,                    // demarre le rendu
      irr::video::SColor(0,255,255,255));
    sceneManager->drawAll ();                         // calcule le rendu
    driver->endScene ();                              // affiche le rendu
  }
 
  device->drop ();                                    // libere la memoire
  return 0;
}

Et voilà. Vous remarquerez sans doute en y regardant de près que le modèle "ondule" légèrement en permanence. C'est un défaut inhérent aux modèles MD2 du au système d'interpolation qu'il est malheureusement impossible de corriger.


Chargement et utilisation Materials

Materials

Réalisation de la scène Ce qu'est un material

Dans ce chapitre nous allons nous attarder sur une propriété fondamentale des modèles 3D, le material. C'est lui qui va déterminer leur apparence et nous permettre de réaliser des rendus de bonne qualité.

Ce qu'est un material

Materials Utilisation

Ce qu'est un material

Un material (matériau en français) porte bien son nom puisqu'il s'agit des propriétés de la matière en laquelle est fait un modèle 3D. Reprenons le problème dans l'ordre : nous avons vu dans le précédent chapitre comment représenter une surface en 3 dimensions, obtenant ainsi un mesh pouvant représenter n'importe quelle forme. Il nous faut maintenant trouver un moyen de recouvrir cette surface pour donner l'illusion que le mesh est "réellement" constitué de matière et qu'il ressemble ainsi à la chose modélisée.

L'étape la plus évidente est de recouvrir le mesh par une image appelée texture (comme nous l'avons vu lors du précédent chapitre). Mais au-delà de ça il existe beaucoup de techniques permettant de simuler des matières plus ou moins complexes. Une simple transparence couplée à une texture de saleté par exemple peut suffire à donner l'illusion parfaite d'une vitre à l'entretien peu consciencieux. Ou encore un matériau sans texture uniquement basé sur la réflexion peut faire un excellent miroir.

Beaucoup de materials basent leur fonctionnement sur la lumière et son comportement vis à vis du mesh. Par exemple un miroir réfléchi totalement la lumière, une vitre la laisse plus ou moins passer, en l'absorbant sur certaines parties d'une texture on peut donner l'illusion d'un relief, etc... D'une manière globale, et c'est parfois un aspect négligé, la lumière et sa gestion par le moteur 3D est un élément fondamental dans la qualité et le réalisme d'un rendu [12-x].

Etant donné que nous n'avons pas encore abordé en détails la question de l'éclairage et des textures, nous nous limiterons dans ce chapitre aux bases de la gestion des materials sous Irrlicht. Ci-dessous quelques représentations possibles d'un mesh (celui du précédent chapitre) :

pointcloudwireframesolidtexturetexture_1parralax_mapping

De gauche à droite, le mesh est affiché avec les méthodes suivantes :


Materials Utilisation

Utilisation

Ce qu'est un material Brouillard

Utilisation

Un material est défini par un ensemble de paramètres regroupés dans une structure : irrr::video::SMaterial[doc]. Tous les scene nodes possèdent une instance de SMaterial, bien que certains ne s'en servent pas. Une caméra par exemple n'a évidemment pas besoin d'un material. Il y a deux grands types de modification qu'on peut faire sur un material dans Irrlicht, le reste n'est que réglage. Modifier le type du material, et modifier les flags :

Dans ce chapitre nous n'effectuerons des modifications que sur les flags, le type étant un peu plus complexe à comprendre et à manipuler. Néanmoins le principe reste le même. Pour effectuer nos tests nous allons nous servir d'un autre modèle présent dans le dossier "media" du SDK : earth.x. Il s'agit simplement d'une sphère parfaite, que nous allons visualiser avec les représentations suivantes :

material_solid
material_wireferame
material_pointcloud

Voici le code minimal de l'application. On y voit la sphere en mode solid (qui est le mode par défaut, visible à gauche au dessus). Ce code utilise une camera fixe mais vous pouvez tout à fait utiliser une caméra FPS :

#include <irr/irrlicht.h>


int main(void) {
 
  irr::IrrlichtDevice* device = irr::createDevice(irr::video::EDT_OPENGL,
    irr::core::dimension2d<irr::u32>(640,480), 32);
  irr::video::IVideoDriver* driver = device->getVideoDriver();
  irr::scene::ISceneManager* sceneManager = device->getSceneManager();
 
  
  /* MODELE */
    
  irr::scene::IAnimatedMeshSceneNode *sphere =   // cree un scene node nomme sphere
    sceneManager->addAnimatedMeshSceneNode (     // via le scene manager
      sceneManager->getMesh ("earth.x"));        // en chargeant le mesh "earth.x" 
  

  /* CAMERA */
    
  sceneManager->addCameraSceneNode(0,            // cree une camera fixe
    irr::core::vector3df(0.0f, 0.0f, -2.0f),     // position de la cam
    irr::core::vector3df(0.0f, 0.0f, 0.0f));     // cible de la cam


    /* RENDU */
    
  while (device->run()) {                        // la boucle de rendu
    driver->beginScene(true, true,               // demarre le rendu
      irr::video::SColor(0,255,255,255));        // couleur blanche
    sceneManager->drawAll ();                    // calcule le rendu
    driver->endScene ();                         // affiche le rendu
  }
 
  device->drop ();                               // libere la memoire
  return 0;
}

La fonction permettant de modifier un flag du material est : setMaterialFlag[doc]. Elle s'appelle à partir d'un scene node et prend 2 paramètres :

Eclairage

Pour commencer voyons la propriété qui permet d'activer ou de désactiver la sensibilité à l'éclairage. Celle-ci se nomme EMF_LIGHTING. En ajoutant la ligne suivante, le mesh sera insensible à l'éclairage. Ce qui signifie que même lorsqu'il n'y aura aucune lumière dans la scène, il paraîtra comme en plein jour :

sphere->setMaterialFlag(irr::video::EMF_LIGHTING, false);

Cependant pour qu'il soit visible il faut bien entendu qu'il soit recouvert d'une texture. Dans notre cas précis il suffit de copier le fichier "earth.jpg", depuis le dossier media du SDK, à coté du binaire exécutable. La texture sera automatiquement appliquée lors du chargement du modèle.

Wireframe / pointcloud

Une fois qu'on a saisi le principe le reste devient un jeu d'enfant. Les lignes suivantes permettent respectivement d'activer la représentation filaire (wireframe) et "par point" (pointcloud) :

sphere->setMaterialFlag(irr::video::EMF_WIREFRAME, true);
sphere->setMaterialFlag(irr::video::EMF_POINTCLOUD, true);

La seule chose dont il faut se méfier est de ne pas activer les deux en même temps, sinon vous verrez tout de même le mesh en filaire. D'une manière générale faites attention dans les associations de materials à ne pas faire des choses contradictoires.


Ce qu'est un material Brouillard

Brouillard

Utilisation Override material

Brouillard

Voyons maintenant un material assez sympathique : le brouillard. Pour l'activer pas de problème, vous devriez commencer à connaitre :

sphere->setMaterialFlag(irr::video::EMF_FOG_ENABLE, true);

Avec une camera mobile, il est possible de voir la sphère disparaître ou apparaître progressivement dans un brouillard blanc. Enfin progressivement... par défaut les réglages ne sont pas optimaux. D'ailleurs pour mieux voir la différence nous allons réutiliser Sydney à la place de la sphère. Voici le code complet :

#include <irr/irrlicht.h>


int main(void) {
 
  irr::IrrlichtDevice* device = irr::createDevice(    // creation du device
    irr::video::EDT_OPENGL,                           // API = OpenGL
    irr::core::dimension2d<irr::u32>(640,480),        // taille fenetre 640x480p
    32);                                              // 32 bits par pixel
  irr::video::IVideoDriver* driver =                  // creation du driver video
    device->getVideoDriver ();    
  irr::scene::ISceneManager* sceneManager =           // creation du scene manager
    device->getSceneManager ();
  device->getCursorControl ()-> setVisible (false);   // rend le curseur invisible
 
  
  /* MODELE */
    
  irr::scene::IAnimatedMeshSceneNode *sydney =        // cree un scene node nomme sydney
    sceneManager->addAnimatedMeshSceneNode (          // via le scene manager
      sceneManager->getMesh ("sydney.md2"));          // en chargeant le mesh "sydney.md2"
 
  sydney->setMD2Animation(irr::scene::EMAT_STAND);    // joue l'animation STAND en boucle
      
  sydney->setMaterialFlag(                            // modifie un material flag
    irr::video::EMF_FOG_ENABLE, true);                // active le brouillard
  

  /* CAMERA */
    
  irr::SKeyMap keyMap[5];                             // re-assigne les commandes
  keyMap[0].Action = irr::EKA_MOVE_FORWARD;           // avancer
  keyMap[0].KeyCode = irr::KEY_KEY_W;                 // w
  keyMap[1].Action = irr::EKA_MOVE_BACKWARD;          // reculer
  keyMap[1].KeyCode = irr::KEY_KEY_S;                 // s
  keyMap[2].Action = irr::EKA_STRAFE_LEFT;            // a gauche
  keyMap[2].KeyCode = irr::KEY_KEY_A;                 // a
  keyMap[3].Action = irr::EKA_STRAFE_RIGHT;           // a droite
  keyMap[3].KeyCode = irr::KEY_KEY_D;                 // d
  keyMap[4].Action = irr::EKA_JUMP_UP;                // saut
  keyMap[4].KeyCode = irr::KEY_SPACE;                 // barre espace

  sceneManager->addCameraSceneNodeFPS(                // ajout de la camera FPS
    0, 100.0f, 0.1f, -1, keyMap, 5);


    /* RENDU */
    
  while (device->run()) {                             // la boucle de rendu
    driver->beginScene(true, true,                    // demarre le rendu
      irr::video::SColor(0,255,255,255));
    sceneManager->drawAll ();                         // calcule le rendu
    driver->endScene ();                              // affiche le rendu
  }
 
  device->drop ();                                    // libere la memoire
  return 0;
}

La fonction permettant de paramétrer le brouillard est setFog [doc]. Elle fait partie du driver vidéo puisque le brouillard est commun à toute la scène. Cette fonction possède beaucoup d'arguments permettant de paramétrer le brouillard assez finement. Je vous laisse regarder dans la documentation pour savoir à quoi chacun correspond. Pour notre exemple les valeurs suivantes donnent un résultat intéressant, que vous pouvez voir sur les screenshots qui suivent :

driver->setFog(
  irr::video::SColor(0, 255, 255, 255),
  irr::video::EFT_FOG_LINEAR,
  20.0f,
  100.0f,
  0.01f,
  true,
  true);

fog1fog2fog3fog4fog5


Utilisation Override material

Override material

Brouillard Textures

Override material

Il existe un material appelé OverrideMaterial qui permet comme son nom l'indique d'"outrepasser" les autres. C'est à dire qu'au moment de calculer le rendu de la scène, ce sera ce material qui sera pris en compte, et non le material du node. L'OverrideMaterial peut par exemple être utile lorsqu'on met des tas de choses dans sa scène mais pas d'éclairage. Plutôt que de modifier les materials des nodes un à un, on peut tout simplement modifier l'OverrideMaterial.

Pour être tout à fait exact, l'OverrideMaterial n'est pas un material comme les autres, c'est une structure qui contient un material. L'intérêt est que cette structure est paramétrable et nous permet de ne choisir que certaines propriétés à outrepasser. Les attributs remarquables de la structure sont :

Ce dernier attribut dépasse le cadre de ce chapitre, mais le sujet est abordé dans celui consacré au rendu des scene nodes.

Pour commencer nous allons simplement modifier la représentation des meshs en activant le mode filaire, tout en laissant les autres propriétés à la charge du material de chaque node. On accède à l'OverrideMaterial par le driver. Il suffit ensuite de modifier ce qui nous intéresse :

#include <irr/irrlicht.h>


int main(void) {
 
  irr::IrrlichtDevice* device = irr::createDevice(irr::video::EDT_OPENGL,
    irr::core::dimension2d<irr::u32>(640,480), 32);
  irr::video::IVideoDriver* driver = device->getVideoDriver();
  irr::scene::ISceneManager* sceneManager = device->getSceneManager();
 
  
  /* MODELE */
    
  irr::scene::IAnimatedMeshSceneNode* sphere =        // cree un scene node nomme sphere
    sceneManager->addAnimatedMeshSceneNode (          // via le scene manager
      sceneManager->getMesh ("earth.x"),              // en chargeant le mesh "earth.x"
      0, -1,                                          // pas de parent, pas d'ID
      irr::core::vector3df(0.0f, 0.0f, 25.0f),        // position de la sphere
      irr::core::vector3df(0.0f, 0.0f, 0.0f),         // rotation
      irr::core::vector3df(15.0f, 15.0f, 15.0f));     // echelle
    
  irr::scene::IAnimatedMeshSceneNode* sydney =        // cree un scene node nomme sydney
    sceneManager->addAnimatedMeshSceneNode (          // via le scene manager
      sceneManager->getMesh ("sydney.md2"));          // en chargeant le mesh "sydney.md2"
    
    
  /* OVERRIDE MATERIAL */

  driver->getOverrideMaterial().EnableFlags =         // indique que le flag EMF_WIREFRAME
    irr::video::EMF_WIREFRAME;                        // va etre outrepasse
  driver->getOverrideMaterial().Material.setFlag(     // active le flag EMF_WIREFRAME
    irr::video::EMF_WIREFRAME, true);                 // de l'override material
  driver->getOverrideMaterial().EnablePasses =        // indique le type de node affectes
    irr::scene::ESNRP_SOLID;                          // par l'override material

  
  /* CAMERA */
    
  sceneManager->addCameraSceneNode(0,                 // cree une camera fixe
    irr::core::vector3df(40.0f, 10.0f, 15.0f),        // position de la cam
    irr::core::vector3df(0.0f, 9.0f, 15.0f));         // cible de la cam


    /* RENDU */
    
  while (device->run()) {                        // la boucle de rendu
    driver->beginScene(true, true,               // demarre le rendu
      irr::video::SColor(0,255,255,255));        // couleur blanche
    sceneManager->drawAll ();                    // calcule le rendu
    driver->endScene ();                         // affiche le rendu
  }
 
  device->drop ();                               // libere la memoire
  return 0;
}

C'est aussi simple que ça. Le code ci-dessus donne le résultat visible sur le screenshot suivant :

override_material


Brouillard Textures

Textures

Override material Le texture mapping en une leçon

Dans ce chapitre, nous allons faire un petit tour de tout ce qui touche aux textures et à leurs applications. Nous allons notamment passer en revue les différentes manières de texturer un modèle sous Irrlicht.

Le texture mapping en une leçon

Textures Les textures, parlons en !

Le texture mapping en une leçon

Reprenons depuis le début en commençant par expliquer en quoi consiste le texture mapping (aussi appelé texturing). Le but de la manoeuvre est d'appliquer une texture (pour simplifier disons une image pour le moment) sur une surface (pas forcément plane), dans le but d'améliorer le réalisme d'une scène virtuelle. Par exemple, à gauche une scène sans textures, et à droite, la même avec textures :

Image utilisateurImage utilisateur

On peut voir que le mur derrière a vraiment l'apparence d'être en pierre, et si on y fait bien attention, on remarque que les mêmes pierres se répètent sans arrêt. C'est une des idées directrices du texture mapping, il vaut mieux répéter plusieurs fois une petite texture que d'appliquer une seule fois une grande. Tout simplement car une petite texture prendra moins de place en mémoire, ce qui permet un chargement plus rapide de la scène. Ou dans le cas d'un jeu en réseau par exemple, un téléchargement plus rapide du fichier.

Cela pose néanmoins une condition : il faut que la texture soit répétitive, c'est-à-dire que les raccords entre les répétitions ne doivent pas êtres visibles. Par exemple, à gauche une texture non répétitive, à droite une texture répétitive :

Image utilisateurImage utilisateur

Il est évident que si la texture n'est pas répétitive, on perd en quelque sorte tout le réalisme qu'on avait gagné en l'appliquant...

Une texture est composée de texels (mot qui vient de la compression de texture et element). Vous allez sans doute me demander pourquoi elles ne sont pas composées de pixels alors qu'il s'agit à la base d'images matricielles comme les autres...

Pourquoi elles ne sont pas composées de pixels alors qu'il s'agit à la base d'images matricielles comme les autres ?

Car il y a une différence très importante entre les pixels et les texels.Image utilisateur

Pour comprendre il faut se représenter son écran comme une énorme grille de pixels (ce qu'il est en fin de compte, du point de vue du rendu).

Partons du principe que la grille à droite représente l'écran.

Image utilisateurNous avons dit qu'une texture n'est rien d'autre qu'une image matricielle, donc elle aussi une grille, mais de texels (image à gauche).

Le problème va être que la grille de texels de la texture doit d'une manière ou d'une autre pouvoir être affichée dans la grille de pixels de l'écran (c'est le rendu).

Image utilisateurDans le cas merveilleux où chaque texel correspond à un pixel (image à droite), pas de problème, tout concorde.
Seulement ce cas très particulier n'arrive quasiment jamais.

Image utilisateurImaginons par exemple que vous décidiez de vous éloigner, les texels seront alors plus petits que les pixels, et un pixel contiendra plusieurs texels (image à gauche).

Image utilisateurAlors que si vous vous rapprochez, les texels grossiront jusqu'à devenir plus gros que les pixels, et il faudra alors plusieurs pixels pour contenir un seul texel (image à droite).

Se pose alors le problème suivant : comment déterminer la couleur finale du pixel s'il ne coïncide pas exactement avec un texel ? La solution consiste à appliquer un filtre, portant d'ailleurs souvent un nom barbare (filtre anisotrope, filtre bilinéaire, etc...).

Le but de ce chapitre n'étant pas tant d'expliquer les différentes méthodes de filtrage que le texturing, nous allons les laisser de côté pour le moment. Vous pouvez néanmoins tester les différences de rendu entre les différentes techniques en temps réel via cette petite application créée de toute pièce par votre serviteur : télécharger le pack.

Et si vous voulez en savoir plus, vous pouvez toujours vous mettre cet article sous la dent : Comprendre la 3D : le filtrage des textures.


Textures Les textures, parlons en !

Les textures, parlons en !

Le texture mapping en une leçon De la bonne manière de texturer un modèle

Les textures, parlons en !

C'aurait pu être le nom d'une nouvelle campagne de communication gouvernementale, mais il ne s'agit que du titre de cette sous-partie qui a pour but d'expliquer de quoi est faite la classe ITexture, qui sert à contenir les textures sous Irrlicht. Pour commencer, il faut savoir que plusieurs choses caractérisent une texture sous Irrlicht :

La classe ITexture étant abstraite, ce n'est pas elle que nous allons instancier lors de la création d'une texture, mais une classe dérivée de ITexture. Les différences entre les différentes classes enfant de ITexture tiennent au fait qu'Irrlicht peut utiliser plusieurs API. La manipulation d'une texture sous OpenGL étant différente de la manipulation sous direct3D par exemple, les méthodes de ITexture sont redéfinies en fonction de l'API utilisée.

Le driver est exactement soumis aux mêmes contraintes. Etant donné que c'est par lui qu'on charge les textures, celles-ci gardent en mémoire le type du driver qui les a chargées. Enfin, en pratique il est peu utile de savoir tout ça, car il n'y a qu'un seul driver par application, qu'il ne change pas de type en cours d'exécution, et que l'interface ITexture nous permet de manipuler indifféremment des textures chargées à partir de drivers de types différents. :)

Il existe plusieurs formats d'encodage des couleurs possibles pour une texture. Le fait qu'une texture soit encodée dans un format plutôt qu'un autre dépend essentiellement de l'encodage de l'image à l'intérieur du fichier à partir duquel la texture est chargée, mais aussi du texture creation flag du driver (on en a parlé dans le 5ème chapitre de ce tutoriel ;) ).

Si vous n'y connaissez rien en profondeur de codage et que vous ne voyez pas la différence entre celles-ci, jetez donc un oeil à ce tuto : Les pixels avec SDL. La méthode permettant de savoir en quel format est encodée la texture est getColorFormat().

Chaque texture se voit attribuer un nom lors de sa création, qui correspond au nom de fichier à partir duquel elle est chargée. On peut récupérer ce nom à l'aide de la méthode getName(), mais on ne peut pas le modifier.

Lorsqu'une texture est chargée, il est possible qu'elle soit redimensionnée. Dans le cas d'une texture dont les dimensions ne sont pas des multiples de 2 par exemple. Dans ce cas, il peut être utile de connaître les dimensions originales de la texture. La méthode getOriginalSize() sert à ça. Sinon pour connaître la taille actuelle de la texture, il faut se servir de getSize().

Les niveaux de mip map d'une texture permettent d'améliorer l'affichage de celle-ci en réduisant son niveau de détail en fonction de sa distance. L'aspect technique de cette méthode est un peu trop complexe pour être développé ici, nous y reviendrons peut être dans un prochain chapitre. En attendant, vous pouvez toujours constater visuellement la différence grâce à cet article : Le mipmapping.

En règle générale, Irrlicht génère automatiquement des niveaux de mip map pour toutes les textures chargées. Mais il est possible que ces niveaux n'existent pas ou ne soient plus valables après une modification de la texture par exemple. Pour savoir si une texture possède des niveaux de mip map, il faut utiliser la méthode hasMipMaps(), qui renvoie true si la texture en a. Dans le cas contraire, on peut utiliser la méthode regenerateMipMapLevels() pour en générer de nouveaux.


Le texture mapping en une leçon De la bonne manière de texturer un modèle

De la bonne manière de texturer un modèle

Les textures, parlons en ! Eclairage

De la bonne manière de texturer un modèle

Maintenant que nous avons vu ce que sont et à quoi servent les textures, reste à voir comment les appliquer. Mais commençons d'abord par mettre quelques petites choses au point.

Si vous avez déjà fait des applications 3D utilisant OpenGL, vous êtes sûrement passés (à un moment ou un autre) par le mode de déclaration directe des sommets. Celui-ci permet de déclarer plusieurs sommets d'affilée qui définissent au final des primitives de base (triangles, carrés, etc...). Par exemple :

glBegin(GL_QUADS);
    glVertex2d(-0.75,-0.75);
    glVertex2d(-0.75,0.75);
    glVertex2d(0.75,0.75);
    glVertex2d(0.75,-0.75);
glEnd();

L'un des avantages de ce mode est qu'il permet de déclarer les coordonnées de texture en même temps. On associe une coordonnée de texture à chaque sommet déclaré, et au final tout se retrouve là où il faut lors du rendu. Et bien grande nouvelle : vous pouvez oublier tout ça ! En effet vous ne trouverez pas ce mode de déclaration des sommets sous Irrlicht.

Mais pourquoi donc ? Encore un complot pour rendre l'informatique inaccessible et mystérieuse ?

Que nenni. Il y a en fait deux bonnes raisons à ce qu'Irrlicht n'intègre pas cette fonctionnalité :

Nous (re)verrons dans un prochain chapitre en quoi consistent les autres modes de déclaration des sommets (et comment ils sont implémentés sous Irrlicht), mais si vous êtes vraiment pressés, vous pouvez toujours vous faire les dents sur ces deux tutoriels :

Reste une question en suspens :

Comment fait-on alors pour déclarer les sommets et les coordonnées de texture qui vont avec ?

Et bien malheureusement (ou pas) nous n'allons pas voir comment le faire manuellement pour le moment. Parce que ça implique de connaître beaucoup de choses (qui peuvent se résumer à "savoir comment créer un scene node personnalisé"). Nous y reviendrons lors d'un prochain chapitre, mais en attendant... il va falloir vous en passer. Il existe néanmoins d'autres moyens d'appliquer des textures, le seul "souci" est qu'il faut passer par un modèle 3D.

Coordonnées de texture

La plupart (une grande majorité à vrai dire) des formats de fichiers 3D fonctionne sur le principe des coordonnées de texture. L'idée est la suivante : le modèle 3D en lui même est contenu dans un fichier (MD2, OBJ, etc...), et la texture qui le recouvre dans un autre (BMP, PNG, etc...). A chaque sommet du modèle sont associées 2 valeurs représentant des coordonnées en 2 dimensions. Ces coordonnées désignent un point sur la surface de la texture. L'aire délimitée sur la surface de la texture par tous les sommets d'une face du modèle représente exactement le morceau de la texture qui sera appliqué sur cette face.

Si vous jetez un oeil au modèle Sydney qu'on utilise depuis le début de ce tutoriel, vous vous apercevrez que c'est ce système qui est utilisé. Le modèle 3D en lui même est contenu dans un fichier MD2, et la texture qui le recouvre dans un banal fichier BMP. Basiquement, le code permettant d'assigner une texture à un mesh est le suivant :

sceneNode->setMaterialTexture( 0, driver->getTexture("le_nom_de_la_texture") );

Le 0 du premier paramètre indique le niveau de cette texture.
En effet, il est possible d'appliquer plusieurs niveaux de textures sur un mesh.

Tout en un

Il existe des formats de fichiers 3D qui contiennent le modèle et les textures. Là c'est que du bonheur, vous n'avez plus qu'à vous mettre les doigts de pieds en éventail et laisser Irrlicht faire le boulot. ^^ Lorsque vous chargez le fichier dans votre scène, le modèle et les textures sont chargées en même temps.

Le format .bsp utilisé pour les maps de Quake 3 est de ceux-là. Et d'ailleurs si vous avez fait attention, vous aurez remarqué que c'est également le format de la map qu'on a chargée lors du chapitre sur les événements. On ne le voit pas au premier coup d'oeil puisque le fichier est à la base au format .pk3 qui n'est rien de plus qu'un fichier .bsp compressé.

Plaquage répétitif

La dernière solution, qui fait un peu "roue de secours", est de plaquer répétitivement une texture sur le modèle. Pour ce faire il faut passer par le manipulateur de mesh. Il s'agit d'une classe qui, comme son nom l'indique, va nous permettre de... manipuler des meshs. La méthode à appeler est celle-ci : makePlanarTextureMapping (IMesh *mesh, f32 resolution)

Le premier paramètre est un pointeur vers le mesh à texturer, et le deuxième permet de définir le nombre de répétitions de la texture. Si vous avez bonne mémoire, vous devez vous souvenir qu'on a déjà rencontré cette fonction. Dans le chapitre sur l'éclairage, au moment de texturer la pièce. Je vous remets le code intéressant :

// La salle
scene::IAnimatedMesh *room = scenegraph->getMesh("room.3ds");
scene::IMeshSceneNode *Nroom = scenegraph->addMeshSceneNode(room->getMesh(0));
//On desactive la sensibilite a la lumiere
Nroom->setMaterialFlag(video::EMF_LIGHTING, true);
//On definit le nombre de repetition de la texture
scenegraph->getMeshManipulator()->makePlanarTextureMapping(room->getMesh(0), 0.04f);
//On choisit la texture
Nroom->setMaterialTexture( 0, driver->getTexture("rockwall.bmp") );

A dire vrai, la seule difficulté consiste à savoir quelle valeur passer au deuxième paramètre. Vous pouvez toujours tâtonner pour trouver la valeur qui vous plaît le plus. Plus ce chiffre est élevé, et plus la texture sera répétée. Par exemple, sur les images ci-dessous, les valeurs sont (de gauche à droite) 0.0004, 0.004, 0.04 :

Image utilisateurImage utilisateurImage utilisateur

La "formule" permettant de calculer le nombre de répétitions est la suivante :

longueur de la plus grande face de la bounding box du mesh * le nombre passé au deuxième paramètre = nombre de répétitions de la texture

Si vous ne savez pas ce qu'est une bounding box, pas de panique. On le verra dans un prochain chapitre. En attendant, imaginez-vous que c'est un grand rectangle qui englobe votre mesh. Donc, prenez en gros la longueur de votre mesh pour faire le calcul. ;)


Les textures, parlons en ! Eclairage

Eclairage

De la bonne manière de texturer un modèle Lumière ambiante

L'éclairage est une composante parfois sous-estimée dans la réalisation d'une scène 3D interactive. Elle est pourtant totalement fondamentale dans l'obtention d'un rendu de qualité, d'autant plus si celui-ci vise le réalisme (voir la référence [12-x]).

Dans ce chapitre nous allons aborder les différents types d'éclairages et scene nodes en rapport avec la lumière gérés par Irrlicht.

Lumière ambiante

Eclairage Voyons par nous-même

Lumière ambiante

Commençons par la plus facile. La lumière ambiante est la plus simple qui soit, elle éclaire tout, partout, tout le temps et avec la même intensité. Par définition la lumière ambiante n'a pas de source précise, elle provient de partout à la fois. Ce qui explique qu'elle éclaire tout avec la même intensité et qu'il soit impossible de faire des ombres avec.

Image utilisateur

Le problème avec ce type de lumière est qu'elle ne permet pas à elle seule de faire en sorte qu'on puisse distinguer les détails d'un objet. On ne voit que ses contours (s'il est de couleur unie). L'image de droite en est un exemple. On voit bien (ou plutôt on ne voit rien du tout ^^ ) que la lumière étant la même partout, il est impossible de distinguer les détails d'un objet.

Voyons maintenant le code qui permet de régler la lumière ambiante d'une scène :

sceneManager->setAmbientLight (irr::video::SColorf(1.0,1.0,1.0,0.0));

Vous l'aurez deviné, l'unique paramètre représente la nouvelle valeur de la lumière. Le dernier chiffre de la couleur n'est pas important dans ce cas précis. Il s'agit de la composante alpha, et elle ne changera rien à la couleur de la lumière ni à son intensité.

Par défaut, la lumière ambiante est égale à (0,0,0). Ce qui signifie que si vous ne rajoutez aucune source de lumière, vous n'y verrez rien du tout. En réglant la lumière ambiante à (1.0,1.0,1.0), toute la scène sera éclairée au maximum. Quelques images pour mieux se rendre compte :

Image utilisateurImage utilisateurImage utilisateur


Eclairage Voyons par nous-même

Voyons par nous-même

Lumière ambiante Lumière diffuse

Voyons par nous-même

Moins de parlottes, plus d'action. Passons au code et réalisons une petite scène de test pour que vous puissiez admirer vous-même le résultat. Et comme ça devient malheureusement une habitude, je vais vous donner un code dont certaines parties n'ont pas encore été expliquées dans ce tuto et ne le seront pas avant quelques chapitres.

Pour cette scène vous allez avoir besoin de 2 fichiers présents dans le dossier media du SDK : "room.3ds" qui contient un modèle 3D comme l'indique son extension, et "rockwall.bmp" qui est bien évidement une image et qui va nous servir de texture. Et maintenant le code complet :

#include <IRR/irrlicht.h>
 
int main(void)
{
    irr::IrrlichtDevice *device = irr::createDevice (
        irr::video::EDT_OPENGL,
        irr::core::dimension2d<irr::u32>(800,600),
        32,true,true,false,0);
    irr::video::IVideoDriver* driver = device->getVideoDriver ();
    irr::scene::ISceneManager *sceneManager = device->getSceneManager ();
 
    irr::scene::ICameraSceneNode *camera =                        // creation de la camera
        sceneManager->addCameraSceneNodeFPS (0,80.0f,100.0f);
    camera->setPosition(irr::core::vector3df(10.0, 50.0, -50.0)); // positionnement
 
    irr::scene::IAnimatedMesh *room =                             // chargement du modele
        sceneManager->getMesh("room.3ds"); 
    irr::scene::IMeshSceneNode *Nroom =                           // creation du node
        sceneManager->addMeshSceneNode(room->getMesh(0));
    Nroom->setMaterialFlag(irr::video::EMF_LIGHTING, true);            // sensible a la lumiere
 
    sceneManager->getMeshManipulator()->                          // parametrage de la texture
        makePlanarTextureMapping(room->getMesh(0), 0.004f);
    Nroom->setMaterialTexture(0,                                  // application de la texture
        driver->getTexture("rockwall.bmp") );

    sceneManager->setAmbientLight(                                // ajout lumiere
        irr::video::SColorf(1.0, 1.0, 1.0,0.0));
 
    while (device->run ())                                        // la boucle de rendu
    {
        driver->beginScene (true, true,             
            irr::video::SColor (255,255,255,255));
        sceneManager->drawAll ();
        driver->endScene ();
    }
 
    device->drop ();
    return 0;
}

Et voilà. Vous remarquerez qu'on a modifié un material sur le mesh de la pièce : EMF_LIGHTING. Un petit tour dans la doc nous apprend que celui-ci permet de choisir si un mesh est sensible à la lumière ou pas. Par défaut il est à "true", ce qui signifie qu'il est sensible à la lumière. Si vous ne rajoutez pas d'éclairage, il restera noir comme du charbon. En le mettant à false, le mesh n'aura que faire de l'éclairage de la scène sera visible comme en plein jour.

Dans le code ci-dessus, le mettre à true n'a pas grand intérêt puisque c'est sa valeur par défaut. Vous pouvez néanmoins le mettre à false pour tester le changement. En jouant sur le paramètre de la fonction modifiant la lumière ambiante vous pouvez régler l'éclairage comme on vient de le voir. :)

Maintenant qu'on dispose d'une scène sympa pour faire des tests, on est prêt pour passer à la suite.


Lumière ambiante Lumière diffuse

Lumière diffuse

Voyons par nous-même Lumière émissive

Lumière diffuse

Image utilisateurLes lumières diffuses sont bien plus intéressantes que la lumière ambiante et par conséquent... bien plus complexes. ^^ Il s'agit en fait d'une source de lumière au sens classique du terme, comme une lampe de bureau ou un spot. En pure théorie une lumière est dite diffuse quand elle est réfléchie par les objets qu'elle éclaire.

Pour reprendre l'exemple de tout à l'heure avec une lumière diffuse, regardez l'image à droite. On peut nettement voir que la lumière se réfléchit sur toutes les surfaces de l'objet, là où avec une lumière ambiante on ne voyait... rien.

La fonction de base pour ajouter une lumière diffuse sous Irrlicht est :

sceneManager->addLightSceneNode (0, irr::core::vector3df(0,0,0),
                               irr::video::SColorf(0.4f,0.4f,0.6f,0.0f), 100.0f);

Nous pouvons voir qu'il s'agit d'un noeud particulier qu'on ajoute (comme tous les noeuds) en passant par le scene manager. Pour connaître l'utilité de chaque paramètre, c'est par ici. Le paramètre radius notamment, sert à déterminer le rayon d'éclairage du noeud. Voici deux screens montrant le même point de vue mais avec un radius différent :

Image utilisateurImage utilisateur

On peut déjà rajouter cette ligne de code dans la scène de test que l'on vient de faire pour se rendre compte des changements. Et par exemple combiner la lumière ambiante avec celle-ci ou modifier la couleur passée en paramètre pour voir la différence.

Voyons voir maintenant ce qu'on peut faire pour personnaliser l'éclairage. La première chose à faire est de récupérer le noeud qu'on ajoute. Pour cela il faut modifier quelque peu le code précédent :

irr::scene::ILightSceneNode* lumiere = sceneManager->addLightSceneNode(
    Nroom,
    irr::core::vector3df(100,100,200),
    irr::video::SColorf(0.4f, 0.4f, 0.6f, 0.0f),
    100.0f);

Nous pouvons maintenant accéder directement au noeud créé. Il est temps maintenant d'aller mettre les mains dans le cambouis et de retourner faire un tour dans les méandres de la doc. Voici la page qui nous intéresse : ILightSceneNode. Comme vous pouvez le voir, cette classe possède 2 méthodes :

Vous vous en doutez, c'est cette deuxième méthode que nous allons regarder de plus près. Observez un peu ce qu'elle retourne : une structure de type video::SLight. Et là, sous nos yeux ébahis... des tas, que dis-je ? Des montagnes de variables paramétrables ! :waw: La méthode "classique" pour modifier l'une des composantes de cette structure est la suivante :

lumiere->getLightData().DiffuseColor = irr::video::SColorf(0.6, 1.0, 1.0, 1);

A priori même pas avoir besoin d'explications, si ? On appelle la fonction qui nous renvoie l'instance de la structure SLight associée au noeud, et on peut directement modifier l'une de ses composantes. Je vous laisse le soin d'explorer les possibilités offertes par ces paramètres, mais on va quand même s'attarder sur quelques-uns d'entre eux :

Type

Type représente le type de la lumière (sans blagues). Cette composante peut prendre 2 valeurs qui correspondent aux 2 types de lumières possibles dans Irrlicht:

Par défaut le type est ELT_POINT. Donc la lumière éclairera dans toutes les directions.

SpecularColor

SpecularColor correspond à la couleur des spéculaires. Pour faire très simple, il s'agit des reflets de la lumière. Pour bien voir ce que cela représente regardez plutôt cette page d'un tutoriel pour Blender. Si on se contente de modifier la couleur des spéculaires de cette manière par exemple :

lumiere->getLightData().SpecularColor = irr::video::SColorf(0.6, 0.0, 0.0, 1);

Image utilisateurOn risque d'avoir des surprises, comme sur l'image de droite. La solution se trouve dans un des material du mesh. Et plus précisément dans Shininess qui permet justement de régler la taille des spéculaires.

Comme vous pouvez le voir, un code est gracieusement donné sur la page de la doc pour montrer la manière de s'en servir. Alors ne nous en privons pas.

Nroom->getMaterial(0).Shininess = 20.0f;

Image utilisateurEt voilà. Comme vous l'aurez deviné, 20.0f représente la nouvelle taille des spéculaires.

En cherchant un peu on finit forcément par trouver le réglage qui nous convient. Voilà ce que j'arrive à faire avec ce qu'on vient de voir (image de droite). Bon évidemment des reflets sur de la pierre, ce n'est pas très réaliste. Mais ce qui compte c'est d'avoir compris. :)


Voyons par nous-même Lumière émissive

Lumière émissive

Lumière diffuse Systèmes de particules

Lumière émissive

Pour finir nous allons revenir à un type de lumière moins compliqué à gérer. Une lumière émissive correspond à la lumière émise par un objet. Un peu comme les autocollants fluorescents qu'on trouve... heu... je ne sais pas où d'ailleurs. Bref... toujours est-il qu'ils brillent dans le noir. ;)

Chaque mesh peut avoir sa propre lumière émissive. Il s'agit donc d'un material : EmissiveColor. Pour y accéder on va utiliser la même technique que pour changer la taille des spéculaires :

Nroom->getMaterial(0).EmissiveColor = irr::video::SColor(255,0,255,255);

Image utilisateurOn peut voir sur l'image de droite ce que donne le code au dessus. Et... c'est tout ce qu'il y a à savoir sur les lumières émissives. Je vous avais dit qu'elles seraient simples. ^^


Lumière diffuse Systèmes de particules

Systèmes de particules

Lumière émissive Comment ça marche ?

Les systèmes de particules sont un grand incontournable des scènes 3D. Principalement car ils sont un excellent moyen de représenter des phénomènes naturels courants; tels que la neige, le feu, la pluie, la fumée d'une trainée de roquette et l'explosion qui suit... :-°

Ce n'est malheureusement pas pour autant qu'ils sont simples à manipuler. Mais par chance nous allons voir dans ce chapitre qu'Irrlicht fait quasiment tout le boulot. Tout ce qu'il nous reste à faire est de bien comprendre comment pour arriver à en tirer le meilleur.

Comment ça marche ?

Systèmes de particules Le système global

Comment ça marche ?

Avant de se lancer vaillamment dans l'étude de la documentation, puis du code qui en découle, il est important de revoir nos bases et de bien comprendre comment fonctionne un générateur de particules.

Par souci de simplicité et comme tout le moteur est codé comme ça, tout ce qui concerne un système de particules est regroupé dans la classe IParticleSystemSceneNode. C'est elle qui va regrouper et gérer toutes les composantes du système. Composantes que nous allons d'ailleurs énumérer :

Je n'ai pas mis la classe correspondante à coté de l'émetteur et du modificateur car il en existe plusieurs différentes.

Emetteur :

Son rôle est comme son nom l'indique, d'émettre des particules. C'est lui qui les génère. Et c'est donc lui qui détermine leurs paramètres à ce moment-là. Un système de particule ne peut gérer qu'un seul émetteur à la fois. En revanche, un même émetteur peut être utilisé pour deux systèmes différents.

Tous les émetteurs de particules d'Irrlicht permettent de contrôler les éléments suivants :

La propagation des particules est le seul critère qui varie d'un type d'émetteur à l'autre, comme nous le verrons plus tard en les passant tous en revue.

Modificateur :

Les modificateurs permettent d'agir sur les particules émises par un émetteur. En effet, sans modification celles-ci seraient bien mornes : pas de changement de couleur, de direction, etc... Contrairement à ce qu'on pourrait penser, un modificateur s'associe au scene node concernant le système de particules, pas à l'émetteur. On peut utiliser plusieurs modificateurs sur un seul système, mais on ne peut pas utiliser un seul modificateur sur plusieurs systèmes.


Systèmes de particules Le système global

Le système global

Comment ça marche ? Les émetteurs

Le système global

Attardons-nous maintenant sur les paramètres modifiables de ces trois éléments et sur le code qui va avec. En commençant par le système global.

Nous avons déjà vu que la classe correspondante est IParticleSystemSceneNode

On peut en obtenir une instance à partir du scene manager :

irr::scene::IParticleSystemSceneNode* particleSystem = sceneManager->addParticleSystemSceneNode(false);

Le premier paramètre de la fonction est un booléen permettant de déterminer s'il y a un émetteur par défaut ou non. C'est uniquement utile à des fins de test ou de débogage étant donné qu'on ne peut pas le paramétrer. Il va néanmoins nous être utile tout de suite, pour vérifier que ça marche. :)

En incluant la ligne d'au-dessus dans un code de base :

#include <IRR/irrlicht.h>


int main(void)
{
    //Device, driver et graphe de scene.
    irr::IrrlichtDevice* device = irr::createDevice(irr::video::EDT_OPENGL,
        irr::core::dimension2d<irr::u32>(800,800),32,false,false,false);
    irr::video::IVideoDriver* driver = device->getVideoDriver ();
    irr::scene::ISceneManager *sceneManager = device->getSceneManager ();

     //On rend invisible le curseur.
    device->getCursorControl ()-> setVisible (false);

    //La camera
    irr::SKeyMap keyMap[5];
    keyMap[0].Action = irr::EKA_MOVE_FORWARD;
    keyMap[0].KeyCode = irr::KEY_KEY_Z;
    keyMap[1].Action = irr::EKA_MOVE_BACKWARD;
    keyMap[1].KeyCode = irr::KEY_KEY_S;
    keyMap[2].Action = irr::EKA_STRAFE_LEFT;
    keyMap[2].KeyCode = irr::KEY_KEY_Q;
    keyMap[3].Action = irr::EKA_STRAFE_RIGHT;
    keyMap[3].KeyCode = irr::KEY_KEY_D;
    keyMap[4].Action = irr::EKA_JUMP_UP;
    keyMap[4].KeyCode = irr::KEY_SPACE;
    irr::scene::ICameraSceneNode *camera;
    camera = sceneManager->addCameraSceneNodeFPS (0,100.0f,0.02f, -1, keyMap, 5, false, 0.4);
    camera->setPosition(irr::core::vector3df(0.0f, 0.0f, 0.0f));

    //La chaine pour le titre de la fenetre
    irr::core::vector3df posCam;
    wchar_t titre[100];


    irr::scene::IParticleSystemSceneNode* particleSystem = sceneManager->addParticleSystemSceneNode(true);


    //La boucle de rendu
    while (device->run())
    {
        posCam = camera->getPosition();
        swprintf(titre, 100, L"FPS : %d (mode debug) |  PosCam :  "
        "X : %f Y : %f Z : %f", driver->getFPS(), posCam.X, posCam.Y, posCam.Z);
        device->setWindowCaption(titre);
        //On utilise un fond blanc pour voir les particules
        driver->beginScene(true, true, irr::video::SColor(0,255,255,255));
        sceneManager->drawAll ();
        driver->endScene ();
    }

    device->drop();
    return 0;
}

Image utilisateurEt vous devriez voir apparaître de gros carrés noirs bien moches sur un fond blanc (comme sur le screen à droite).

Mais le IParticleSystemSceneNode permet également de gérer d'autres choses (comme l'indique la page de documentation). Etant donné qu'il est dérivé de ISceneNode, il possède notamment un material. Pour ceux qui auraient oublié de quoi il s'agit; relisez le chapitre correspondant. ;)


Comment ça marche ? Les émetteurs

Les émetteurs

Le système global Les modificateurs

Les émetteurs

Comme je le disais plus haut, il y a plusieurs types d'émetteur. En voici la liste :

J'imagine que les noms sont suffisamment explicites. Néanmoins si vous voulez en savoir plus, la documentation est là pour ça.

Commençons par un facile, le IParticleBoxEmitter. Comme son nom l'indique, il va générer des particules à partir d'une boite. La position de cette boite étant relative à la position du noeud particle system, les coordonnées que nous allons renseigner aussi. 0 correspond donc à la position du noeud particle system.

camera->setPosition(irr::core::vector3df(60.0f, 60.0f, 0.0f));
camera->setTarget(irr::core::vector3df(0.0f, 20.0f, 0.0f));

irr::scene::IParticleSystemSceneNode* particleSystem =
    sceneManager->addParticleSystemSceneNode(false);

irr::scene::IParticleEmitter* emitter = particleSystem->createBoxEmitter(
    irr::core::aabbox3d<irr::f32>(-3,0,-3,3,1,3), // coordonnees de la boite
    irr::core::vector3df(0.0f,0.06f,0.0f),        // direction de diffusion
    80,100,                                       // nb particules emises a la sec min / max
    irr::video::SColor(0,0,0,0),                  // couleur la plus sombre
    irr::video::SColor(0,255,255,255),            // couleur la plus claire
    600, 1000,                                    // duree de vie min / max
    0,                                            // angle max d'ecart / direction prevue
    irr::core::dimension2df(6.0f,6.0f),           // taille minimum
    irr::core::dimension2df(8.0f,8.0f));          // taille maximum

particleSystem->setEmitter(emitter);                               // on attache l'emetteur
emitter->drop();                                                   // plus besoin de ca
particleSystem->setMaterialFlag(irr::video::EMF_LIGHTING, false); // insensible a la lumiere

Image utilisateurSi tant est que vous codez sur un matériel un peu performant, vous devriez voir que ça va vite, très vite. C'est du au fait que nous n'avons pas imposé de limite de framerate. Comme il y a très peu de choses à calculer et à afficher, le nombre de FPS est très élevé. Vous remarquerez aussi qu'on a modifié le material du particle system pour le rendre insensible à la lumière. Ce qui est indispensable pour y voir quelque chose, sachant que la scène ne contient aucune source de lumière.

Poussons l'expérience un peu plus loin et tentons de reproduire un feu (pour être original) à l'aide du material du particle system. Avant tout, nous allons avoir besoin de la texture "fire.bmp", qui se trouve dans le dossier media du SDK. Ensuite, regardez le code qui suit et faites fonctionner vos méninges. :)

camera->setPosition(irr::core::vector3df(60.0f, 60.0f, 0.0f));
camera->setTarget(irr::core::vector3df(0.0f, 40.0f, 0.0f));

irr::scene::IParticleSystemSceneNode* particleSystem =
    sceneManager->addParticleSystemSceneNode(false);

irr::scene::IParticleEmitter* emitter = particleSystem->createBoxEmitter(
    irr::core::aabbox3d<irr::f32>(-6,0,-6,6,1,6), // coordonnees de la boite
    irr::core::vector3df(0.0f,0.06f,0.0f),        // direction de diffusion
    80,100,                                       // nb particules emises a la sec min / max
    irr::video::SColor(0,255,255,255),            // couleur la plus sombre
    irr::video::SColor(0,255,255,255),            // couleur la plus claire
    600, 1200,                                    // duree de vie min / max
    0,                                            // angle max d'ecart / direction prevue
    irr::core::dimension2df(8.0f,8.0f),           // taille minimum
    irr::core::dimension2df(14.0f,14.0f));        // taille maximum

particleSystem->setEmitter(emitter);              // on attache l'emetteur
emitter->drop();                                  // plus besoin de ca

particleSystem->setMaterialFlag(irr::video::EMF_LIGHTING, false);          // insensible a la lumiere
particleSystem->setMaterialFlag(irr::video::EMF_ZWRITE_ENABLE, false);     // desactive zbuffer pour surfaces derriere
particleSystem->setMaterialTexture(0, driver->getTexture("fire.bmp"));     // on colle une texture
particleSystem->setMaterialType(irr::video::EMT_TRANSPARENT_VERTEX_ALPHA); // application transparence

Image utilisateurImage utilisateurImage utilisateurLes seules nouveautés sont les trois dernière lignes qui collent une texture sur chaque particule et activent la transparence. De fait, si vous compilez tout ça vous verrez apparaitre un joli feu (comme à droite), et pas un amas immonde de particules carrées. Faites le test de retirer la transparence pour voir la différence. ;)

L'ajout de texture est fondamental pour reproduire des phénomènes physiques naturels. A vrai dire les générateurs de particules sont (de manière générale) tellement souples et efficaces qu'ils permettent par exemple de générer une pelouse. Il suffit de ne pas re-générer les particules et de leur mettre une texture de brin d'herbe. Les possibilités sont énormes...


Le système global Les modificateurs

Les modificateurs

Les émetteurs Gestion des périphériques

Les modificateurs

Notre feu a donc un aspect plutôt réaliste, mais quelques détails sont néanmoins gênants. Le fait que les particules ne baissent pas "d'intensité" par exemple. En effet, le haut des flammes produit théoriquement moins de lumière que le gros du brasier. Nous allons donc utiliser un modificateur qui change progressivement la couleur des particules. Mais avant cela, passons en revue les différents types de modificateur :

Nous allons donc utiliser un IParticleFadeOutAffector qui va diminuer progressivement la couleur de la texture et la rendre ainsi transparente.

irr::scene::IParticleAffector* affector =    // creation du modificateur
    particleSystem->createFadeOutParticleAffector(
    irr::video::SColor(0,0,0,0),             // la couleur finale
    1200);                                   // temps necessaire a la modification
particleSystem->addAffector(affector);       // ajout du modificateur au particle system
affector->drop();                            // plus besoin de ca

Image utilisateurImage utilisateurImage utilisateurVous pouvez voir sur les screens à droite que les particules disparaissent maintenant progressivement, ce qui donne l'impression que les flammes sont plus fortes en bas du brasier qu'en haut (logique).

Les autres modificateurs sont peut être un peu plus spécialisés, IParticleRotationAffector par exemple propose un effet sympathique mais peu commun dans le monde réel. A l'inverse, certains modificateurs évidents ne sont pas présents dans le moteur. Le vent par exemple ne peut pas être simulé avec ceux listés au dessus. Quoi qu'il en soit ce n'est pas un problème très gênant puisqu'il est bien sûr possible de créer ses propres modificateurs et de les utiliser sans même avoir à recompiler le moteur. Magie de la POO. :magicien:

Exercice :

Quoi de mieux qu'un exercice pour s'assurer qu'on a tout compris ? Je vous propose donc de créer une petite scène représentant une chute de neige. Voici d'ailleurs quelques images de ce que j'ai fait pour vous donner une idée du rendu :

Image utilisateurImage utilisateurImage utilisateur

L'important ici sont les particules de neige. Inutile de faire un sol ou une interface si ça ne vous intéresse pas. Pour la texture des flocons, j'ai juste repris le fichier "fire.bmp" qu'on a déjà utilisé plus haut, auquel j'ai changé les couleurs. Pour comparer votre code avec le mien ou si vous êtes bloqués, vous pouvez

télécharger le pack contenant le code source et les textures de l'application.

La gestion des particules par Irrlicht est suffisante pour créer de jolis effets et produire des scènes relativement réalistes, mais elle est tout de même assez rudimentaire dans l'absolu. Par exemple, il n'y a pas de gestion des particules en 3 dimensions. Alors que c'est incontournable pour reproduire certaines choses.

Aussi, le moteur montre assez rapidement ses limites par rapport au nombre de particules à gérer simultanément. En effet, si vous faites le test de prendre un générateur tout bête et d'augmenter la durée de vie des particules, ça va rapidement ramer.

Mais après tout, tous ces défauts n'en sont pas. Car il est tout à fait possible de modifier cette partie du moteur. Soit en l'améliorant, soit en la recodant totalement. "Il n'y a de limite que celle qu'on s'impose" comme dirait l'autre. ;)


Les émetteurs Gestion des périphériques

Gestion des périphériques

Les modificateurs Les événements à la source

Pour ce premier chapitre des techniques intermédiaires, on attaque fort avec les événements !
Vous allez enfin pouvoir interagir avec vos applications Irrlicht en temps réel. :)

Il s'agit donc d'un chapitre fondamental, et s'il n'est pas placé plus tôt dans ce tutoriel, c'est parce que la gestion des événements sous Irrlicht est loin d'être aisée. Mais pas de panique, on a tout notre temps alors on va y aller par étapes en expliquant bien à chaque fois de quoi il retourne.

Les événements à la source

Gestion des périphériques Le mouvement en action

Les événements à la source

Pour commencer, il convient de savoir exactement de quoi on parle. On appelle "événement" tout appui/relâchement sur une touche, mouvement de la souris, clic sur un bouton, etc... La liste complète des types d'événements (events en anglais) pouvant être captés par Irrlicht est disponible dans la documentation. Notez au passage le mot "captés", voilà une notion importante en ce qui concerne les événements.

Le schéma de la gestion des événements sera le suivant :

Phase 1

Un événement est produit

↓ ↓ ↓

Phase 2

Il est capté par le capteur d'événements

↓ ↓ ↓

Phase 3

Il est traité en fonction de son type

Les deux premières phases sont gérées par Irrlicht, notre boulot va consister à écrire le code décrivant comment traiter tel ou tel type d'événement. Par exemple, imaginons que l'on crée un FPS, on voudra que lorsqu'un clic gauche de la souris est détecté, le personnage tire.

Voyons un peu plus en détail comment cela s'organise dans le code déjà existant du moteur. Le capteur d'event est de la classe IEventReceiver, on peut d'ailleurs voir sur le diagramme UML la liste des classes du moteur susceptibles d'envoyer des événements à un capteur (les caméras et quasiment tous les éléments de la GUI).

Il est indiqué sur la page de documentation que la classe IEventReceiver possède en tout et pour tout 2 méthodes. Dont un destructeur dont nous ne nous préoccuperons pas. Tout va donc se jouer sur OnEvent(SEvent event). Cette méthode est automatiquement appelée dès qu'un événement se produit. C'est la 2ème phase du schéma de tout à l'heure. Par défaut, cette méthode est purement virtuelle, ce qui signifie que si on la laisse en l'état, il ne se passera rien si un événement est capté (enfin de toute façon, on ne pourrait même pas instancier la classe en l'état...).

Tout ce qu'il nous reste à faire est donc de dériver la classe IEventReceiver pour pouvoir implémenter la méthode OnEvent de manière à ce que notre traitement des événements s'applique. C'est la 3ème phase du schéma. J'espère que vous êtes au point sur le C++, on va commencer à faire des choses moins triviales. ^^

Bon, pour le moment on a à peu près tout ce qu'il faut pour gérer "bêtement" les événements, mais comme nous sommes curieux, cela ne nous suffit pas. Alors regardons encore plus en détails dans le code source comment Irrlicht se débrouille pour gérer tout ça. Le point de départ se trouve dans la méthode run() du device. Vous la connaissez forcément, c'est la méthode qu'on appelle dans la boucle de rendu pour savoir s'il faut arrêter l'application ou pas :

//La boucle de rendu
while (device->run())
{
    /* ... */
}

Réfléchissons un instant, comment cette fonction sait-elle si elle doit retourner true pour que l'application continue ou false pour qu'elle s'arrête ? En détectant les événements bien sûr ! Par exemple, un appui simultané sur Alt et F4 ferme l'application. C'est cette fonction qui va se charger de capter tous les événements, et d'appeler la méthode OnEvent() de la classe IEventReceiver.

On peut donc retrouver le code de cette fonction dans le fichier contenant le code de CDevice. Pas de chance, il y a deux versions possibles de ce fichier. L'une d'entre elles contient le code de CDevice dans le cas d'une utilisation sous systèmes de type Unix (Linux par exemple), et l'autre contient le code pour une utilisation sous Windows. La raison de cette séparation est que, avant de renvoyer un événement de type SEvent (donc un type propre à Irrlicht), celui-ci est obligé de passer par une API système.

Au tout début de la chaîne, un événement n'est qu'un signal électrique partant du clavier jusqu'à l'unité centrale pour indiquer que telle ou telle touche a été enfoncée par exemple. La première couche logicielle à intercepter ce signal pour le traiter est l'OS (le système d'exploitation). Et il existe une bibliothèque permettant de "dialoguer" avec l'OS pour lui demander, par exemple, de nous prévenir quand une touche est enfoncée et de nous dire laquelle, etc... Cette bibliothèque s'appelle une API système et elle est bien sûr différente en fonction de l'OS. Irrlicht va donc se servir des ces API systèmes et c'est ce qui explique les 2 versions différentes de CDevice.

Après ces quelques explications un peu théoriques, nous allons examiner un morceau du code de la fonction run() de la version systèmes type Unix de CDevice. Les systèmes de type Unix utilisent l'API X Window System (souvent abrégée X) pour ce qui concerne l'interface graphique et la gestion des événements. Par conséquent, certaines lignes du code qui va suivre sont spécifiques à cette API. J'expliquerais brièvement à quoi elles servent mais le but de la manoeuvre est plutôt de comprendre le fonctionnement général de la fonction. Aussi j'ai donc coupé pas mal de code et ajouté des commentaires :

bool CIrrDeviceLinux::run()
{
 
    /* ... */
 
    //Une instance de SEvent est cree
    irr::SEvent irrevent;
 
    /* ... */
 
    //Sert a recuperer les events
    XNextEvent(display, &event);
 
    //On teste le type d'event
    switch (event.type)
    {
 
        /* ... */
 
        //Dans le cas d'un deplacement de souris
        case MotionNotify:
            irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT;
            irrevent.MouseInput.Event = irr::EMIE_MOUSE_MOVED;
            irrevent.MouseInput.X = event.xbutton.x;
            irrevent.MouseInput.Y = event.xbutton.y;
            postEventFromUser(irrevent);
            break;
 
        /* ... */
 
    }
}

Vous pouvez admirer ligne 7 l'événement qui sera transmis à la méthode OnEvent. Il est créé au début de la fonction et sera initialisé en fonction de ce que X aura détecté. C'est d'ailleurs le rôle de la ligne 12 comme l'indiquent les commentaires.

Une fois que X a récupéré l'event, un switch est fait pour déterminer quelle est sa nature. Normalement ce switch est assez énorme (allez faire un tour dans le code source pour vous en convaincre), mais un seul case suffit à comprendre. Ligne 21 donc, on teste qu'il s'agit d'un déplacement de souris. Si c'est le cas, les 4 lignes suivantes vont servir à paramétrer l'instance de SEvent. C'est à la ligne 26 que les choses se corsent. On peut voir qu'après avoir été paramétrée, l'instance de SEvent est passée en paramètre à la fonction postEventFromUser.

Ne reculant devant rien dans notre quête de savoir, allons voir dans la classe CIrrDeviceStub (la classe mère de CDevice (quelle que soit la version)) l'implémentation de cette méthode :

void CIrrDeviceStub::postEventFromUser(SEvent event)
{
        bool absorbed = false;
 
        if (UserReceiver)
                absorbed = UserReceiver->OnEvent(event);
 
        if (!absorbed && GUIEnvironment)
                absorbed = GUIEnvironment->postEventFromUser(event);
 
        scene::ISceneManager* inputReceiver = InputReceivingSceneManager;
        if (!inputReceiver)
                inputReceiver = SceneManager;
 
        if (!absorbed && inputReceiver)
                absorbed = inputReceiver->postEventFromUser(event);
}

Bon, normalement à ce stade, j'ai perdu la moitié des lecteurs. :p
Qu'à cela ne tienne, nous n'allons pas nous arrêter en si bon chemin !

Le but de cette fonction est de faire parvenir l'événement aux différents capteurs d'événements. Comment ça ? J'ai oublié de préciser qu'il y en a plusieurs ? :-° C'est assez simple en fait : en premier lieu, la fonction envoie l'événement à "UserReceiver" (ligne 6). Le user c'est nous, il s'agit donc d'une instance de notre classe dérivée de IEventReceiver que nous allons associer au device (c'est la phase 3 du schéma du début). Notez au passage que la ligne 5 sert à vérifier que nous avons bien associé un capteur au device.

Il se trouve que la méthode OnEvent renvoie un booléen. Et celui-ci indique si l'événement a été traité ou pas. Eh oui, il est possible que l'événement détecté soit un clic de souris et que seuls les appuis sur les touches du clavier nous intéressent. Dans ce cas là, la fonction OnEvent renvoie false pour indiquer qu'elle n'a pas traité l'événement.

Seulement voilà, un clic de souris ce n'est pas rien quand même, il se peut que ça intéresse quelqu'un d'autre... la GUI par exemple. C'est tout l'intérêt de la ligne 9. On commence par tester ligne 8 que le booléen renvoyé par notre capteur est bien false, et qu'il existe bien un environnement de GUI. Si la condition est remplie, on envoie l'événement à l'environnement GUI, et il se débrouille avec. On pourrait farfouiller là aussi dans le code source pour découvrir des choses intéressantes mais je crois qu'on fera ça dans un autre chapitre sinon vous allez être dégoûtés d'Irrlicht pour de bon. ^^

De même, pour simplifier, nous allons dire que les lignes restantes servent à envoyer l'événement au scene manager puisqu'il gère la (l'éventuelle) caméra. En partant bien sûr du principe que la fonction OnEvent du capteur de la GUI a elle aussi renvoyé un false. Cette simplification est correcte, c'est véritablement ce qu'il se passe. Mais pas seulement...

Histoire de souffler un peu et de se remettre les idées en place, je vous mets la version améliorée du schéma du début :

Phase 1

Un événement est produit

↓ ↓ ↓

Phase 1.5

Il est capté par l'API système

↓ ↓ ↓

Phase 2

Il est récupéré par Irrlicht et passé à la fonction postEventFromUser

↓ ↓ ↓

Phase 2.5

La fonction postEventFromUser passe l'événement (de type SEvent) de gestionnaire en gestionnaire jusqu'à ce qu'il soit traité

↓ ↓ ↓

Phase 3

Un gestionnaire traite l'événement en fonction de son type

Alors ? Qui a dis que c'était compliqué ? ^^


Gestion des périphériques Le mouvement en action

Le mouvement en action

Les événements à la source Caméra personnalisée

Le mouvement en action

Normalement le plus dur est passé, si vous avez compris ce qui précède la pratique ne devrait pas être compliquée outre mesure. La première chose à faire est donc de dériver IEventReceiver pour pouvoir implémenter OnEvent à notre sauce. Voilà ce que donne la déclaration de la classe :

#include <IRR/irrlicht.h>
 
 
class CEventReceiver : public irr::IEventReceiver
{
 
public :
 
    //Capte automatiquement les events.
    virtual bool OnEvent(const irr::SEvent &event);
};

Bon, c'est un début. Maintenant réfléchissons à l'implémentation de OnEvent, qu'est-ce qu'on aimerait faire en rapport avec les événements ? Bouger un personnage par exemple ! (bon ok vous n'avez pas vraiment le choix alors ce sera ça et c'est tout :p ).

L'idée de base est donc que la méthode OnEvent va agir sur la position d'un mesh en fonction de l'événement détecté. Oui mais voilà, encore faut-il pouvoir accéder à ce mesh. Nous allons donc mettre en attribut de la classe un pointeur vers un IAnimatedMeshSceneNode. Il est inutile que le destructeur de notre classe se charge de libérer le modèle pointé puisque c'est le scene manager qui s'en occupe (d'ailleurs il n'y a pas besoin de destructeur du tout).

Voilà comment nous allons nous y prendre pour déplacer le modèle. Si on bougeait le mesh à chaque appui sur la touche, le mouvement serait saccadé, même en laissant la touche enfoncée (surtout en laissant la touche enfoncée). La solution consiste à se servir d'un booléen indiquant si la touche est enfoncée ou pas. A chaque appui sur la touche, on met le booléen à true et à chaque relâchement, il repasse à false. Ainsi, il ne reste plus qu'à tester à chaque frame si le booléen est true ou false, et à agir en conséquence.

Notre déclaration se transforme donc en :

#include <IRR/irrlicht.h>
 
class CEventReceiver : public irr::IEventReceiver
{
 
public :
 
    //Le constructeur.
    CEventReceiver(irr::scene::IAnimatedMeshSceneNode* Nmodele);
    //Capte automatiquement les events.
    virtual bool OnEvent(const irr::SEvent &event);
    //Met a jour la position du mesh.
    void majPosMesh();
 
 
private :
 
    //Le modele qu'on va controler.
    irr::scene::IAnimatedMeshSceneNode* m_Nmodele;
    //Indique si on est deja en mouvement ou pas.
    bool m_isMoving;
};

Voyons maintenant l'implémentation :

#include "CEventReceiver.h"
 
CEventReceiver::CEventReceiver(irr::scene::IAnimatedMeshSceneNode* Nmodele)
{
    //On pointe le mesh passe en parametre.
    m_Nmodele = Nmodele;
    //Par defaut on ne bouge pas
    m_isMoving = false;
}
 
 
bool CEventReceiver::OnEvent(const irr::SEvent &event)
{
    //On verifie que le pointeur est ok
    if(m_Nmodele != 0
    //Qu'il s'agit d'un event concernant un appui/relachement de touche
    && event.EventType == irr::EET_KEY_INPUT_EVENT
    //Qu'il s'agit de la touche z
    && event.KeyInput.Key == irr::KEY_KEY_Z)
    {
        //Si il s'agit d'un appui
        if(event.KeyInput.PressedDown == true)
            m_isMoving = true;
        //Sinon c'est un relachement
        else
            m_isMoving = false;
        //L'event est traite, on retourne true
        return true;
    }
    //Si on arrive la, c'est qu'on a pas traite l'event
    return false;
}
 
 
void CEventReceiver::majPosMesh()
{
    //On verifie que le pointeur vers le mesh est
    //ok et que la touche est enfoncee
    if(m_Nmodele != 0 && m_isMoving == true)
    {
        //On commence par recuperer la position actuelle
        irr::core::vector3df v = m_Nmodele->getPosition();
        //On y ajoute la valeur de deplacement
        v.X += 1.0f;
        //On renvoie la nouvelle position
        m_Nmodele->setPosition(v);
    }
}

Je pense qu'il y a suffisamment de commentaires pour ne pas avoir à détailler. Notez tout de même que le mouvement du mesh va se limiter à une translation sur l'axe X (on commence petit). On pourrait même optimiser ce code en se servant directement de la valeur de event.KeyInput.PressedDown, étant donné que c'est un booléen valant true lors d'un appui et false lors d'un relâchement (vous pouvez dire merci à minirop pour le tuyau ;) ).

bool CEventReceiver::OnEvent(const irr::SEvent &event)
{
    //On verifie que le pointeur est ok
    if(m_Nmodele != 0
    //Qu'il s'agit d'un event concernant un appui/relachement de touche
    && event.EventType == irr::EET_KEY_INPUT_EVENT
    //Qu'il s'agit de la touche z
    && event.KeyInput.Key == irr::KEY_KEY_Z)
    {
        //On assigne la bonne valeur directement
        m_isMoving = event.KeyInput.PressedDown;
        //L'event est traite, on retourne true
        return true;
    }
    //Si on arrive la, c'est qu'on a pas traite l'event
    return false;
}

Ne reste qu'à trouver une application de test pour faire tourner la bête. Allez je suis sympa, je vous fournis le code :

#include "CEventReceiver.h"
 
 
int main(void)
{
 
    //Device, driver et graphe de scene.
    irr::IrrlichtDevice* device = irr::createDevice(irr::video::EDT_OPENGL,
        irr::core::dimension2d<irr::u32>(800,800),32,false,false,false);
    irr::video::IVideoDriver* driver = device->getVideoDriver ();
    irr::scene::ISceneManager *sceneManager = device->getSceneManager ();
 
    //On rend invisible le curseur.
    device->getCursorControl ()-> setVisible (false);
 
    //Sydney
    irr::scene::IAnimatedMeshMD2* modele;
    modele = (irr::scene::IAnimatedMeshMD2*)sceneManager->getMesh("sydney.md2");
    irr::scene::IAnimatedMeshSceneNode* Nmodele =
    sceneManager->addAnimatedMeshSceneNode(modele);
 
    //On modifie les proprietes de Sydney
    Nmodele->setMaterialFlag(irr::video::EMF_LIGHTING, false);
    Nmodele->setFrameLoop(0, 0);
    Nmodele->setMaterialTexture( 0, driver->getTexture("sydney.bmp") );
 
    //La camera
    irr::scene::ICameraSceneNode *camera;
    camera = sceneManager->addCameraSceneNodeFPS (0,100.0f,300.0f);
 
    //On cree le capteur d'event et on l'associe au device.
    CEventReceiver receiver(Nmodele);
    device->setEventReceiver(&receiver);
 
    //La boucle de rendu
    while (device->run())
    {
        driver->beginScene(true, true, irr::video::SColor(0,200,200,200));
        //Met a jour la position du mesh
        receiver.majPosMesh();
        sceneManager->drawAll ();
        driver->endScene ();
    }
 
    device->drop ();
    return 0;
}

Oui je l'avoue, c'est l'adaptation du code du chapitre "votre deuxième scène". :-° Les seules lignes qui changent sont les 32 et 33 qui servent respectivement à créer l'instance de CEventReceiver (notre capteur) et à l'associer au device. Ainsi que la 40 qui met à jour la position du modèle en fonction de l'enfoncement de la touche z.

Nous y voilà. Compilez, lancez et appuyez sur la touche z. Et là, sous vos yeux ébahis, Sydney avance ! o_O Et de manière parfaitement fluide qui plus est.

Image utilisateurImage utilisateurImage utilisateur

Maintenant que vous savez comment ça marche, vous pouvez vous entraîner à ajouter quelques "mouvements". L'idéal étant de combiner les déplacements avec l'animation "courir" du modèle. Mais ça sera pour un autre chapitre. ;)


Les événements à la source Caméra personnalisée

Caméra personnalisée

Le mouvement en action Des caméras et des animators

Caméra personnalisée

Si vous avez bonne mémoire, vous devez vous rappeler que lors du chapitre "votre deuxième scène", j'ai passé sous silence certains paramètres de la fonction de création d'une caméra FPS en disant que nous y reviendrions dans le chapitre consacré aux événements. Et bien nous y sommes, alors voyons voir de quoi il retourne. :)

Je remets le prototype de la fonction en question :

virtual ICameraSceneNode* irr::scene::ISceneManager::addCameraSceneNodeFPS(
    ISceneNode* parent, f32 rotateSpeed, f32 moveSpeed, s32 id,
    SKeyMap* keyMapArray, s32 keyMapSize, bool noVerticalMovement, f32 jumpSpeed);

Les paramètres qui nous intéressent sont le 5ème et le 6ème, qui permettent de modifier les touches par défaut attribuées aux actions de la caméra. Vous pouvez donc voir qu'Irrlicht se sert d'une keyMap pour attribuer des actions aux touches. Une keymap reprend un peu le même principe qu'une std::map de la STL.

Il s'agit en fait d'un tableau d'instances de SKeyMap, et SKeyMap est une structure composée de 2 éléments :

Il existe 5 actions possibles :

Quant aux touches... Jetez un oeil à votre clavier. ;)

Le but de la manoeuvre est d'associer une (ou plusieurs) touche(s) à une action, de sorte que lorsqu'un événement sur cette (ces) touche(s) est capté, l'action soit effectuée. Par défaut pour une caméra FPS, les associations sont les suivantes :

Action

Touche

Description

EKA_MOVE_FORWARD

KEY_UP

flèche du haut pour déplacement en avant

EKA_MOVE_BACKWARD

KEY_DOWN

flèche du bas pour déplacement en arrière

EKA_STRAFE_LEFT

KEY_LEFT

flèche de gauche pour déplacement à gauche

EKA_STRAFE_RIGHT

KEY_RIGHT

flèche de droite pour déplacement à droite

EKA_JUMP_UP

KEY_KEY_J

touche j pour un saut

Les flèches directionnelles ne sont pas ce qu'il y a de plus ergonomique pour se déplacer. Nous allons donc reprendre la bonne vieille configuration : z pour avancer, s pour reculer, q pour aller à gauche, d pour aller à droite et barre espace pour sauter. La keyMap va se construire de la sorte :

irr::SKeyMap keyMap[5];
//avancer
keyMap[0].Action = irr::EKA_MOVE_FORWARD;
keyMap[0].KeyCode = irr::KEY_KEY_Z;
//reculer
keyMap[1].Action = irr::EKA_MOVE_BACKWARD;
keyMap[1].KeyCode = irr::KEY_KEY_S;
//a gauche
keyMap[2].Action = irr::EKA_STRAFE_LEFT;
keyMap[2].KeyCode = irr::KEY_KEY_Q;
//a droite
keyMap[3].Action = irr::EKA_STRAFE_RIGHT;
keyMap[3].KeyCode = irr::KEY_KEY_D;
//saut
keyMap[4].Action = irr::EKA_JUMP_UP;
keyMap[4].KeyCode = irr::KEY_SPACE;

Voilà, rien de compliqué si vous avez compris ce qui précède. Il ne reste plus qu'à la passer à la caméra lors de sa création :

irr::scene::ICameraSceneNode *camera;
camera = sceneManager->addCameraSceneNodeFPS (0, 100.0f, 300.0f, -1, keyMap, 5, false, 0.4);

C'est au cinquième paramètre que l'on passe le tableau, et au sixième qu'on spécifie sa taille. C'est important ! Si vous spécifiez une taille plus petite par exemple, les cases dépassant ce nombre ne seront pas prises en compte et n'effectueront pas les actions quelles devraient.

Il est maintenant temps de tester tout ça dans un petit exemple. Vous aurez besoin pour cela du fichier "map-20kdm2.pk3" que vous pouvez trouver dans le dossier media du SDK. Le code suivant va charger une map de type .bsp dans laquelle nous allons nous déplacer avec nos toutes nouvelles commandes. Le seul souci c'est qu'on n'a pas encore vu comment charger des fichiers .bsp (et encore moins quand ils sont compressés). Il va donc falloir que vous attendiez un prochain chapitre pour comprendre les quelques lignes concernant la map (ou que vous alliez voir dans la doc ;) ).

#include <IRR/irrlicht.h>
 
int main(void)
{
    //Device, driver et graphe de scene
    irr::IrrlichtDevice* device = irr::createDevice(irr::video::EDT_OPENGL,
        irr::core::dimension2d<irr::u32>(800,800),32,false,false,false);
    irr::video::IVideoDriver* driver = device->getVideoDriver ();
    irr::scene::ISceneManager *sceneManager = device->getSceneManager ();
 
    //On rend invisible le curseur
    device->getCursorControl ()-> setVisible (false);
 
 
    //Sert a charger la map
    device->getFileSystem()->addZipFileArchive("map-20kdm2.pk3");
    irr::scene::IAnimatedMesh* mesh = sceneManager->getMesh("20kdm2.bsp");
    irr::scene::ISceneNode* node = 0;
    if (mesh)
        node = sceneManager->addOctTreeSceneNode(mesh->getMesh(0));
    if (node)
        node->setPosition(irr::core::vector3df(-1300,-144,-1249));
    sceneManager->setAmbientLight(irr::video::SColorf(1.0,1.0,1.0,0.0));
 
 
    //On cree la keymap associe a la camera
    irr::SKeyMap keyMap[5];
    //avancer
    keyMap[0].Action = irr::EKA_MOVE_FORWARD;
    keyMap[0].KeyCode = irr::KEY_KEY_Z;
    //reculer
    keyMap[1].Action = irr::EKA_MOVE_BACKWARD;
    keyMap[1].KeyCode = irr::KEY_KEY_S;
    //a gauche
    keyMap[2].Action = irr::EKA_STRAFE_LEFT;
    keyMap[2].KeyCode = irr::KEY_KEY_Q;
    //a droite
    keyMap[3].Action = irr::EKA_STRAFE_RIGHT;
    keyMap[3].KeyCode = irr::KEY_KEY_D;
    //saut
    keyMap[4].Action = irr::EKA_JUMP_UP;
    keyMap[4].KeyCode = irr::KEY_SPACE;
 
    //La camera
    irr::scene::ICameraSceneNode *camera;
    camera = sceneManager->addCameraSceneNodeFPS (0, 100.0f, 0.3f, -1, keyMap, 5, false, 0.4);
 
    //La boucle de rendu
    while (device->run())
    {
        driver->beginScene(true, true, irr::video::SColor(0,200,200,200));
        sceneManager->drawAll ();
        driver->endScene ();
    }
 
    device->drop ();
    return 0;
}

Image utilisateurImage utilisateurImage utilisateur

Pas mal hein ? Il ne manquerait plus qu'une skybox et on s'y croirait vraiment. Bon forcément, vu qu'on n'a pas implémenté de gravité le saut ressemble plutôt à un envol... Mais ça permet d'admirer la map de haut...

"That's all folks". J'espère que le chapitre vous aura plu. J'ai tenté ici de vous faire comprendre un peu de la mécanique sous-jacente d'Irrlicht qu'on ne voit pas en restant dans la doc.

Si vous avez bonne mémoire, vous devez vous souvenir que j'insistais parfois au début du tutoriel pour que vous alliez fouiner dans la doc. Eh bien les temps changent et il est peut-être temps pour vous d'aller maintenant fouiner dans le code source. Dans la plupart des cas ce n'est pas nécessaire à la compréhension de la fonctionnalité, mais parfois (comme ici) ça permet d'avoir accès à des parties non documentées du code, et on y fait souvent des découvertes intéressantes. ;)


Le mouvement en action Des caméras et des animators

Des caméras et des animators

Caméra personnalisée Fonctionnement des caméras

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.

Fonctionnement des caméras

Des caméras et des animators Réalisation d'une caméra trackball

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 :

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

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 :

nom

description

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 :

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 :

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.


Des caméras et des animators Réalisation d'une caméra trackball

Réalisation d'une caméra trackball

Fonctionnement des caméras Implémentation de la caméra

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 :

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.