Tutoriel Irrlicht 20

Gestion de lumières

| Colin MacDonald | Alexandre Laurent |

Traducteur

Neckara

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

Écrit par Colin MacDonald, ce tutoriel explique comment utiliser le gestionnaire de lumière d'Irrlicht. Il permet l'utilisation de plus de sources de lumière dynamiques que le matériel actuel ne peut supporter. Les applications plus poussées du gestionnaire de lumière, comme les fonctions de rappel (callback) par nœud de scène, ne seront pas évoquées ici pour garder l'exemple simple.

Commentez

- Titre : Tutoriel Irrlicht 20

- Auteurs : Colin MacDonald, Alexandre Laurent

- Traducteur : Neckara

- Parution : 21 juin 2013

- Mise à jour : 21 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. Gestionnaire de lumières

- III. Rendu

- IV. Conclusion

- V. Remerciements

I - Prérequis

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

using namespace irr;
using namespace core;

#if defined(_MSC_VER)
#pragma comment(lib, "Irrlicht.lib")
#endif // MSC_VER

II - Gestionnaire de lumières

Normalement, vous êtes limité à huit lumières dynamiques par scène : c'est une limite du matériel. Si vous souhaitez utiliser plus de lumières dans votre scène, vous pouvez enregistrer un gestionnaire de scène optionnel qui vous permettra d'éclairer et d'éteindre les lumières à des moments précis pendant le rendu. Vous êtes toujours limité à huit lumières mais cette fois, par nœud de scène.

Ceci est totalement optionnel : si vous ne voulez pas enregistrer de gestionnaire de lumières, alors un système par défaut basé sur les distances sera utilisé pour déterminer les lumières à envoyer au matériel selon leur distance à la caméra active.

NO_MANAGEMENT désactive le gestionnaire de lumière et montre le comportement par défaut d'Irrlicht au niveau des lumières. Les huit lumières les plus proches de la caméra seront allumées tandis que les autres seront éteintes, cela produit un air funky mais un affichage des lumières incohérent.

LIGHTS_NEAREST_NODE met en place un nombre limité de lumières par nœud de scène mesh. Il trouve les trois lumières les plus proches du nœud à dessiner et les allume en éteignant toutes les autres lumières. Cela fonctionne, mais comme il opère pour chaque lumière et chaque nœud, l'algorithme ne donne pas de bons résultats avec beaucoup de lumières. Le clignotement que vous pouvez voir dans la démonstration est due au changement des positions relatives des lumières par rapport au cube (une démonstration délibérée des limitations de cette technique).

LIGHTS_IN_ZONE est une technique basée sur une « zone » pour allumer les lumières. Chaque nœud de scène vide est considéré comme étant le parent d'une zone. Quand les nœuds sont dessinés, ils éteignent toutes les lumières puis ils cherchent leur « zone » parente et allument toutes les lumières contenues dans cette zone, c'est-à-dire tous ses descendants dans le graphe de scène. Ceci produit un éclairage « local » authentique pour chaque cube dans cet exemple. Vous pouvez utiliser une technique similaire pour allumer localement tous les objets (par exemple) d'une pièce, sans avoir de la lumière provenant des autres pièces.

Ce gestionnaire de lumières est aussi un récepteur d'événements, ceci pour des raisons de simplification de l'exemple, ce n'est ni nécessaire ni recommandé dans une application réelle.

class CMyLightManager : public scene::ILightManager, public IEventReceiver
{
typedef enum
{
NO_MANAGEMENT,
LIGHTS_NEAREST_NODE,
LIGHTS_IN_ZONE
}
LightManagementMode;

LightManagementMode Mode;
LightManagementMode RequestedMode;

// Ces données représentent les informations sur l'état qui intéressent ce gestionnaire de lumière.
scene::ISceneManager * SceneManager;
core::array<scene::ISceneNode*> * SceneLightList;
scene::E_SCENE_NODE_RENDER_PASS CurrentRenderPass;
scene::ISceneNode * CurrentSceneNode;

public:
CMyLightManager(scene::ISceneManager* sceneManager)
: Mode(NO_MANAGEMENT), RequestedMode(NO_MANAGEMENT),
SceneManager(sceneManager), SceneLightList(0),
CurrentRenderPass(scene::ESNRP_NONE), CurrentSceneNode(0)
{ }

// L'interface d'entrée d'événement, qui change juste la stratégie du gestionnaire de lumière.
bool OnEvent(const SEvent & event)
{
bool handled = false;

if (event.EventType == irr::EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
{
handled = true;
switch(event.KeyInput.Key)
{
case irr::KEY_KEY_1:
RequestedMode = NO_MANAGEMENT;
break;
case irr::KEY_KEY_2:
RequestedMode = LIGHTS_NEAREST_NODE;
break;
case irr::KEY_KEY_3:
RequestedMode = LIGHTS_IN_ZONE;
break;
default:
handled = false;
break;
}

if(NO_MANAGEMENT == RequestedMode)
SceneManager->setLightManager(0); // Montre que c'est sûr d'enregistrer le gestionnaire de lumière.
else
SceneManager->setLightManager(this);
}

return handled;
}

// Ceci est appelé avant que le premier nœud de scène ne soit dessiné.
virtual void OnPreRender(core::array<scene::ISceneNode*> & lightList)
{
// Met à jour le mode ; le changer ici permet de s'assurer qu'on reste cohérent tout au long du rendu
Mode = RequestedMode;

// Stocke la liste de lumières. Je suis libre de changer cette liste jusqu'à la fin de OnPostRender().
SceneLightList = &lightList;
}

// Appelé après que le dernier nœud de scène a été dessiné
virtual void OnPostRender()
{
// Comme le gestionnaire de lumière a pu être éteint dans le gestionnaire d'événements, nous allumerons toutes les lumières
// pour nous assurer qu'elles sont dans un état cohérent. Normalement, vous n'aurez pas à le faire quand vous utilisez un gestionnaire de lumière,
// puisque vous continuez à faire vous-même la gestion des lumières.
for (u32 i = 0; i < SceneLightList->size(); i++)
(*SceneLightList)[i]->setVisible(true);
}

virtual void OnRenderPassPreRender(scene::E_SCENE_NODE_RENDER_PASS renderPass)
{
// Je n'ai rien à faire ici sauf me souvenir dans quelle passe de dessin je suis.
CurrentRenderPass = renderPass;
}

virtual void OnRenderPassPostRender(scene::E_SCENE_NODE_RENDER_PASS renderPass)
{
// Je veux seulement que les nœuds solides soient allumés, donc après la passe solide, j'éteins toutes les lumières.
if (scene::ESNRP_SOLID == renderPass)
{
for (u32 i = 0; i < SceneLightList->size(); ++i)
(*SceneLightList)[i]->setVisible(false);
}
}

// Ceci est appelé avant qu'un nœud de scène spécifique ne soit dessiné.
virtual void OnNodePreRender(scene::ISceneNode* node)
{
CurrentSceneNode = node;

// Ce gestionnaire de lumière ne prend en compte que les objets solides, mais vous êtes libre de manipuler les lumières pendant n'importe quelle phase,
//selon vos besoins.
if (scene::ESNRP_SOLID != CurrentRenderPass)
return;

// En fait dans cet exemple, je veux seulement prendre en compte l'éclairage pour les nœuds de scène cubes.
// Vous voudrez probablement vous occuper de l'éclairage pour (au moins) les nœuds de scènes de mesh animés ou non de la même manière.
if (node->getType() != scene::ESNT_CUBE)
return;

if (LIGHTS_NEAREST_NODE == Mode)
{
// Ceci est une implémentation naïve qui donne une priorité à chaque lumière de la scène selon sa proximité au nœud en train d'être dessiné.
// Cela produit quelques clignotements quand l'orbite de la lumière est plus proche d'un cube que sa 'zone' de lumière.
const vector3df nodePosition = node->getAbsolutePosition();

// Trie la liste de lumières par priorité basée sur leur distance avec le nœud qui est en train d'être dessiné.
array<LightDistanceElement> sortingArray;
sortingArray.reallocate(SceneLightList->size());

u32 i;
for(i = 0; i < SceneLightList->size(); ++i)
{
scene::ISceneNode* lightNode = (*SceneLightList)[i];
const f64 distance = lightNode->getAbsolutePosition().getDistanceFromSQ(nodePosition);
sortingArray.push_back(LightDistanceElement(lightNode, distance));
}

sortingArray.sort();

// La liste est maintenant triée par distance avec le nœud.
// Allume les trois lumières les plus proches et éteint les autres.
for(i = 0; i < sortingArray.size(); ++i)
sortingArray[i].node->setVisible(i < 3);
}
else if(LIGHTS_IN_ZONE == Mode)
{
// Des nœuds de scènes vides sont utilisés pour représenter des 'zones'.  Pour chaque mesh solide qui
// est en train d'être dessiné, éteint toutes les lumières puis cherche sa 'zone' parente et allume
// toutes les lumières qui sont trouvées sous ce nœud dans le graphe de scène.
// Ceci est un algorithme général dont le but est de ne nécessiter aucune connaissance
// spéciale sur la manière dont ce graphe de scène particulier est organisé.
for (u32 i = 0; i < SceneLightList->size(); ++i)
{
if ((*SceneLightList)[i]->getType() != scene::ESNT_LIGHT)
continue;
scene::ILightSceneNode* lightNode = static_cast<scene::ILightSceneNode*>((*SceneLightList)[i]);
video::SLight & lightData = lightNode->getLightData();

if (video::ELT_DIRECTIONAL != lightData.Type)
lightNode->setVisible(false);
}

scene::ISceneNode * parentZone = findZone(node);
if (parentZone)
turnOnZoneLights(parentZone);
}
}

// Appelé après que le nœud de scène spécifié est dessiné.
virtual void OnNodePostRender(scene::ISceneNode* node)
{
// Je n'ai besoin de rien faire sur les lumières après les dessins individuels de nœuds.
}

private:

// Trouve le nœud de scène vide qui est le parent du nœud de scène spécifié
scene::ISceneNode * findZone(scene::ISceneNode * node)
{
if (!node)
return 0;

if (node->getType() == scene::ESNT_EMPTY)
return node;

return findZone(node->getParent());
}

// Allume toutes les lumières qui sont fils (directement ou indirectement) du nœud de scène spécifié.
void turnOnZoneLights(scene::ISceneNode * node)
{
core::list<scene::ISceneNode*> const & children = node->getChildren();
for (core::list<scene::ISceneNode*>::ConstIterator child = children.begin();
child != children.end(); ++child)
{
if ((*child)->getType() == scene::ESNT_LIGHT)
(*child)->setVisible(true);
else // Suppose que les lumières ne possèdent pas d'enfant qui soient des lumières
turnOnZoneLights(*child);
}
}

// Une classe utile pour aider à trier les nœuds de scènes avec un ordre de distance
class LightDistanceElement
{
public:
LightDistanceElement() {};

LightDistanceElement(scene::ISceneNode* n, f64 d)
: node(n), distance(d) { }

scene::ISceneNode* node;
f64 distance;

// Les éléments ayant une plus petite distance sont au début du tableau.
bool operator < (const LightDistanceElement& other) const
{
return (distance < other.distance);
}
};
};

III - Rendu

int main(int argumentCount, char * argumentValues[])
{
// Demande un pilote à l'utilisateur
video::E_DRIVER_TYPE driverType=driverChoiceConsole();
if (driverType==video::EDT_COUNT)
return 1;

IrrlichtDevice *device = createDevice(driverType,
dimension2d<u32>(640, 480), 32);

if(!device)
return -1;

f32 const lightRadius = 60.f; // Assez pour atteindre le côté éloigné de chaque 'zone'.

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

gui::IGUISkin* skin = guienv->getSkin();
if (skin)
{
skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255, 255, 255, 255));
gui::IGUIFont* font = guienv->getFont("../../media/fontlucida.png");
if(font)
skin->setFont(font);
}

guienv->addStaticText(L"1 - No light management", core::rect<s32>(10,10,200,30));
guienv->addStaticText(L"2 - Closest 3 lights", core::rect<s32>(10,30,200,50));
guienv->addStaticText(L"3 - Lights in zone", core::rect<s32>(10,50,200,70));

Ajoute plusieurs « zones ». Vous pouvez utiliser cette technique, par exemple, pour éclairer des pièces individuellement.

for(f32 zoneX = -100.f; zoneX <= 100.f; zoneX += 50.f)
for(f32 zoneY = -60.f; zoneY <= 60.f; zoneY += 60.f)
{
//Commence avec un nœud de scène vide qui nous utiliserons pour représenter une zone.
scene::ISceneNode * zoneRoot = smgr->addEmptySceneNode();
zoneRoot->setPosition(vector3df(zoneX, zoneY, 0));

// Chaque zone contient un cube tournant sur lui-même.
scene::IMeshSceneNode * node = smgr->addCubeSceneNode(15, zoneRoot);
scene::ISceneNodeAnimator * rotation = smgr->createRotationAnimator(vector3df(0.25f, 0.5f, 0.75f));
node->addAnimator(rotation);
rotation->drop();

// Chaque cube a trois lumières qui lui sont attachées. Les lumières sont attachées à un panneau donc
// nous pouvons voir où elles sont. Le panneau est attaché au cube ainsi les
// lumières sont, comme le cube, des descendantes indirectes du même nœud de scène vide.
scene::IBillboardSceneNode * billboard = smgr->addBillboardSceneNode(node);
billboard->setPosition(vector3df(0, -14, 30));
billboard->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR );
billboard->setMaterialTexture(0, driver->getTexture("../../media/particle.bmp"));
billboard->setMaterialFlag(video::EMF_LIGHTING, false);
smgr->addLightSceneNode(billboard, vector3df(0, 0, 0), video::SColorf(1, 0, 0), lightRadius);

billboard = smgr->addBillboardSceneNode(node);
billboard->setPosition(vector3df(-21, -14, -21));
billboard->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR );
billboard->setMaterialTexture(0, driver->getTexture("../../media/particle.bmp"));
billboard->setMaterialFlag(video::EMF_LIGHTING, false);
smgr->addLightSceneNode(billboard, vector3df(0, 0, 0), video::SColorf(0, 1, 0), lightRadius);

billboard = smgr->addBillboardSceneNode(node);
billboard->setPosition(vector3df(21, -14, -21));
billboard->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR );
billboard->setMaterialTexture(0, driver->getTexture("../../media/particle.bmp"));
billboard->setMaterialFlag(video::EMF_LIGHTING, false);
smgr->addLightSceneNode(billboard, vector3df(0, 0, 0), video::SColorf(0, 0, 1), lightRadius);

// Chaque cube a aussi un petit cube plus petit tournant autour pour montrer que les cubes sont allumés par les lumières de leur 'zone' pas seulement par les lumières qui sont leurs enfants directs.
node = smgr->addCubeSceneNode(5, node);
node->setPosition(vector3df(0, 21, 0));
}

smgr->addCameraSceneNode(0, vector3df(0,0,-130), vector3df(0,0,0));

CMyLightManager * myLightManager = new CMyLightManager(smgr);
smgr->setLightManager(0); // Ceci est le comportement par défaut : nous ne géreront pas les lumières tant qu'on ne nous dira pas de le faire
device->setEventReceiver(myLightManager);

int lastFps = -1;

while(device->run())
{
driver->beginScene(true, true, video::SColor(255,100,101,140));
smgr->drawAll();
guienv->drawAll();
driver->endScene();

int fps = driver->getFPS();
if(fps != lastFps)
{
lastFps = fps;
core::stringw str = L"Managed Lights [";
str += driver->getName();
str += "] FPS:";
str += fps;
device->setWindowCaption(str.c_str());
}
}

myLightManager->drop(); // Jette ma référence implicite
device->drop();
return 0;
}

IV - Conclusion

Vous pouvez désormais utiliser le gestionnaire de lumières avec Irrlicht vous permettant ainsi d'augmenter le nombre de sources de lumière dans votre scène.

Dans le prochain tutoriel, Explorateur Quake 3, nous verrons comment charger différentes cartes Quake 3.

V - Remerciements

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

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