Tutoriel Irrlicht 16

Support des cartes Quake 3 et de leurs effets

| Nikolaus Gebhart | Laurent Alexandre |

Traducteur

Neckara

Ce tutoriel est une traduction de l'anglais des tutoriels officiels d'Irrlicht.

Il montre comment charger une carte Quake 3 dans le moteur, créer un nœud de scène pour optimiser la vitesse de rendu et comment créer une caméra contrôlée par l'utilisateur.

Commentez

- Titre : Tutoriel Irrlicht 16

- Auteurs : Nikolaus Gebhart, Laurent Alexandre

- Traducteur : Neckara

- Parution : 7 juin 2013

- Mise à jour : 7 juin 2013

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Neckara. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Prérequis

- II. Chargement du niveau

- III. Recherche d'une position de départ

- IV. Conclusion

- V. Remerciements

I - Prérequis

Commençons comme dans l'exemple Hello World : nous incluons les fichiers d'en-têtes d'Irrlicht et un fichier supplémentaire pour pouvoir demander à l'utilisateur un type de pilote en utilisant la console.

#include <irrlicht.h>
#include "driverChoice.h"

Définissons quel niveau de Quake 3 nous devons charger.

#define IRRLICHT_QUAKE3_ARENA
//#define ORIGINAL_QUAKE3_ARENA
//#define CUSTOM_QUAKE3_ARENA
//#define SHOW_SHADER_NAME

#ifdef ORIGINAL_QUAKE3_ARENA
#define QUAKE3_STORAGE_FORMAT   addFolderFileArchive
#define QUAKE3_STORAGE_1        "/baseq3/"
#ifdef CUSTOM_QUAKE3_ARENA
#define QUAKE3_STORAGE_2    "/cf/"
#define QUAKE3_MAP_NAME     "maps/cf.bsp"
#else
#define QUAKE3_MAP_NAME         "maps/q3dm8.bsp"
#endif
#endif

#ifdef IRRLICHT_QUAKE3_ARENA
#define QUAKE3_STORAGE_FORMAT   addFileArchive
#define QUAKE3_STORAGE_1    "../../media/map-20kdm2.pk3"
#define QUAKE3_MAP_NAME         "maps/20kdm2.bsp"
#endif

using namespace irr;
using namespace scene;

Encore une fois, pour être capable d'utiliser le fichier Irrlicht.DLL, nous devons le lier avec le Irrlicht.lib. Nous pouvons mettre cette option dans les configurations du projet, mais pour le faire plus simplement, nous utilisons un commentaire pragma lib :

#ifdef _MSC_VER
#pragma comment(lib, "Irrlicht.lib")
#endif

Une classe pour produire une série de captures d'écran.

class CScreenShotFactory : public IEventReceiver
{
public:

CScreenShotFactory( IrrlichtDevice *device, const c8 * templateName, ISceneNode* node )
: Device(device), Number(0), FilenameTemplate(templateName), Node(node)
{
FilenameTemplate.replace ( '/', '_' );
FilenameTemplate.replace ( '\\', '_' );
}

bool OnEvent(const SEvent& event)
{
// Vérifie si l'utilisateur presse la touche F9.
if ((event.EventType == EET_KEY_INPUT_EVENT) &&
event.KeyInput.PressedDown)
{
if (event.KeyInput.Key == KEY_F9)
{
video::IImage* image = Device->getVideoDriver()->createScreenShot();
if (image)
{
c8 buf[256];
snprintf(buf, 256, "%s_shot%04d.jpg",
FilenameTemplate.c_str(),
++Number);
Device->getVideoDriver()->writeImageToFile(image, buf, 85 );
image->drop();
}
}
else
if (event.KeyInput.Key == KEY_F8)
{
if (Node->isDebugDataVisible())
Node->setDebugDataVisible(scene::EDS_OFF);
else
Node->setDebugDataVisible(scene::EDS_BBOX_ALL);
}
}
return false;
}

private:
IrrlichtDevice *Device;
u32 Number;
core::stringc FilenameTemplate;
ISceneNode* Node;
};

II - Chargement du niveau

OK, commençons.

int IRRCALLCONV main(int argc, char* argv[])
{

Comme dans l'exemple Hello World, nous créons un IrrlichtDevice avec createDevice(). Maintenant, la différence est que nous demandons à l'utilisateur de choisir quel pilote d'accélération matérielle utiliser. Le moteur logiciel sera trop lent pour dessiner une grande carte Quake 3, mais juste pour le fun, nous rendons cette décision possible aussi.

// Demande à l'utilisateur un pilote.
video::E_DRIVER_TYPE driverType=driverChoiceConsole();
if (driverType==video::EDT_COUNT)
return 1;

// Crée le moteur et quitte si sa création échoue.
const core::dimension2du videoDim(800,600);

IrrlichtDevice *device = createDevice(driverType, videoDim, 32, false );

if (device == 0)
return 1; // Ne peut pas créer le pilote sélectionné.

const char* mapname=0;
if (argc>2)
mapname = argv[2];
else
mapname = QUAKE3_MAP_NAME;

Obtenons un pointeur sur le pilote vidéo et le gestionnaire de scène ainsi nous n'aurons plus à écrire tout le temps device->getVideoDriver() et device->getSceneManager().

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* gui = device->getGUIEnvironment();

device->getFileSystem()->addFileArchive("../../media/");

Pour afficher la carte Quake 3, nous devons d'abord la charger. Les niveaux de Quake 3 sont contenus dans des fichiers .pk3, qui ne sont rien d'autre que des fichiers .zip. Donc nous ajoutons les fichiers .pk3 dans notre FileSystem. Après les avoir ajoutés, nous sommes capables de lire les fichiers de ces archives comme s'ils étaient directement stockés sur le disque dur.

if (argc>2)
device->getFileSystem()->QUAKE3_STORAGE_FORMAT(argv[1]);
else
device->getFileSystem()->QUAKE3_STORAGE_FORMAT(QUAKE3_STORAGE_1);
#ifdef QUAKE3_STORAGE_2
device->getFileSystem()->QUAKE3_STORAGE_FORMAT(QUAKE3_STORAGE_2);
#endif

// Contrôle de Z-Writing de Quake3 Shader.
smgr->getParameters()->setAttribute(scene::ALLOW_ZWRITE_ON_TRANSPARENT, true);

Maintenant nous pouvons charger le mesh en appelant getMesh(). Nous obtenons alors le pointeur sur un IAnimatedMesh. Comme vous le savez, les cartes Quake 3 ne sont pas vraiment animées, il y a seulement un très grand morceau de géométrie statique avec quelques matériels attachés. Comme le mesh IAnimatedMesh n'a qu'une seule image, nous obtenons donc la « première image » de « l'animation » qui est notre carte Quake et créons un nœud de l'octree(1) pour la scène en utilisant addOctreeSceneNode(). L'octree optimise un peu la scène en essayant de dessiner seulement la géométrie visible. Une alternative à l'octree pourrait être un IMeshSceneNode qui dessinera toujours entièrement la géométrie du mesh sans optimisation. Essayons-le, utilisez addMeshSceneNode() à la place d'addOctreeSceneNode() et comparez-les primitives(2) dessinées par le pilote vidéo. (Il y a une méthode getPrimitiveCountDrawn() dans la classe IVideoDriver.) Notez que cette optimisation avec l'octree est utile seulement quand on dessine des grands meshs constitués de nombreuses géométries.

scene::IQ3LevelMesh* const mesh =
(scene::IQ3LevelMesh*) smgr->getMesh(mapname);

Ajoutons le mesh de géométrie à la scène (polygones et chemins). Les meshs de géométrie sont optimisés pour être affichés plus rapidement.

scene::ISceneNode* node = 0;
if (mesh)
{
scene::IMesh * const geometry = mesh->getMesh(quake3::E_Q3_MESH_GEOMETRY);
node = smgr->addOctreeSceneNode(geometry, 0, -1, 4096);
}

// Crée un receveur d'événements pour faire des captures d'écran.
CScreenShotFactory screenshotFactory(device, mapname, node);
device->setEventReceiver(&screenshotFactory);

Maintenant, créons un nœud de scène pour chaque Shader. Les objets sont stockés dans le mesh de Quake scene::E_Q3_MESH_ITEMS et l'identifiant du Shader est stocké dans le MaterialParameters souvent noir ressemblant à des crânes et de la lave mobile… ou des tubes verts clignottants ?

if ( mesh )
{
// Le mesh supplémentaire peut être assez grand et non optimisé.
const scene::IMesh * const additional_mesh = mesh->getMesh(quake3::E_Q3_MESH_ITEMS);

#ifdef SHOW_SHADER_NAME
gui::IGUIFont *font = device->getGUIEnvironment()->getFont("../../media/fontlucida.png");
u32 count = 0;
#endif

for ( u32 i = 0; i!= additional_mesh->getMeshBufferCount(); ++i )
{
const IMeshBuffer* meshBuffer = additional_mesh->getMeshBuffer(i);
const video::SMaterial& material = meshBuffer->getMaterial();

// Le ShaderIndex est stocké dans les paramètres du matériel.
const s32 shaderIndex = (s32) material.MaterialTypeParam2;

// Le tampon du mesh peut être dessiné sans support supplémentaire, ou sans shader.
const quake3::IShader *shader = mesh->getShader(shaderIndex);
if (0 == shader)
{
continue;
}

// Nous pouvons afficher le shader vers la console dans son état original
// ou l'afficher d'une plus belle manière grâce à son analyse
// ... Cette ligne est commentée, car la console sera remplie...
// quake3::dumpShader ( Shader );

node = smgr->addQuake3SceneNode(meshBuffer, shader);

#ifdef SHOW_SHADER_NAME
count += 1;
core::stringw name( node->getName() );
node = smgr->addBillboardTextSceneNode(
font, name.c_str(), node,
core::dimension2d<f32>(80.0f, 8.0f),
core::vector3df(0, 10, 0));
#endif
}
}

Maintenant nous avons juste besoin d'une caméra pour regarder le niveau de Quake 3. Nous voulons créer une caméra contrôlée par l'utilisateur. Il y a plusieurs caméras disponibles dans le moteur Irrlicht. Par exemple, la « MayaCamera » peut être contrôlée comme une caméra Maya : rotation en appuyant sur le bouton droit de la souris, zoom avec deux boutons appuyés, translation en appuyant le bouton gauche. Ceci peut être créé avec addCameraSceneNodeMaya(). Mais pour cet exemple, nous voulons créer une caméra qui se comporte comme celles des jeux de tir à la première personne (FPS(3)).

scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS();

III - Recherche d'une position de départ

Nous avons besoin d'une bonne position de départ dans le niveau. Nous pouvons demander au chargeur Quake 3 toutes les entrées avec class_name ayant pour valeur "info_player_deathmatch" nous choisissons un point de départ au hasard.

if ( mesh )
{
quake3::tQ3EntityList &entityList = mesh->getEntityList();

quake3::IEntity search;
search.name = "info_player_deathmatch";

s32 index = entityList.binary_search(search);
if (index >= 0)
{
s32 notEndList;
do
{
const quake3::SVarGroup *group = entityList[index].getGroup(1);

u32 parsepos = 0;
const core::vector3df pos =
quake3::getAsVector3df(group->get("origin"), parsepos);

parsepos = 0;
const f32 angle = quake3::getAsFloat(group->get("angle"), parsepos);

core::vector3df target(0.f, 0.f, 1.f);
target.rotateXZBy(angle);

camera->setPosition(pos);
camera->setTarget(pos + target);

++index;
notEndList = ( index < (s32) entityList.size () && entityList[index].name == search.name && (device->getTimer()->getRealTime() >> 3 ) & 1 );
notEndList = index == 2;
} while ( notEndList );
}
}

Le curseur de la souris n'a pas besoin d'être visible, nous la rendons donc invisible.

device->getCursorControl()->setVisible(false);

// On charge le logo du moteur.
gui->addImage(driver->getTexture("irrlichtlogo2.png"),
core::position2d<s32>(10, 10));

// On montre le logo du moteur.
const core::position2di pos(videoDim.Width - 128, videoDim.Height - 64);

switch ( driverType )
{
case video::EDT_BURNINGSVIDEO:
gui->addImage(driver->getTexture("burninglogo.png"), pos);
break;
case video::EDT_OPENGL:
gui->addImage(driver->getTexture("opengllogo.png"), pos);
break;
case video::EDT_DIRECT3D8:
case video::EDT_DIRECT3D9:
gui->addImage(driver->getTexture("directxlogo.png"), pos);
break;
}

Nous avons tout fait, donc dessinons le tout. Nous écrivons aussi le nombre d'images par seconde ainsi que les primitives dans le titre de la fenêtre. La ligne 'if (device->isWindowActive())' est optionnelle, mais évite au moteur de capturer le curseur de la souris après un changement de tâche lorsque d'autres programmes sont actifs.

int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, video::SColor(255,20,20,40));
smgr->drawAll();
gui->drawAll();
driver->endScene();

int fps = driver->getFPS();
//if (lastFPS != fps)
{
io::IAttributes * const attr = smgr->getParameters();
core::stringw str = L"Q3 [";
str += driver->getName();
str += "] FPS:";
str += fps;
str += " Cull:";
str += attr->getAttributeAsInt("calls");
str += "/";
str += attr->getAttributeAsInt("culled");
str += " Draw: ";
str += attr->getAttributeAsInt("drawn_solid");
str += "/";
str += attr->getAttributeAsInt("drawn_transparent");
str += "/";
str += attr->getAttributeAsInt("drawn_transparent_effect");

device->setWindowCaption(str.c_str());
lastFPS = fps;
}
}

À la fin, nous supprimons le moteur Irrlicht.

device->drop();

return 0;
}

IV - Conclusion

Vous pouvez désormais charger une carte Quake 3 en utilisant Irrlicht.

Dans le prochain tutoriel Hello World pour mobile, nous verrons comment montrer « Hello World » pour les mobiles Windows.

V - Remerciements

Merci à Nikolaus Gebhardt de nous permettre de traduire ce tutoriel.

Merci à LittleWhite pour sa relecture technique ainsi qu'à ClaudeLELOUP pour sa relecture orthographique.

(1)Un octree est un arbre où chaque nœud peut avoir huit enfants. C'est une structure couramment utilisée pour le partitionnement d'un espace 3D en sous-ensembles.

(2)Formes géométriques basiques constituant des géométries plus complexes.

(3)First person shooter.