Latest article: Exporter des données depuis Excel

Les shaders GLSL avec Irrlicht

Un petit tutoriel pour apprendre à utiliser les shaders GLSL avec le moteur Irrlicht 3D

C’est surement le sujet pour lequel je reçois le plus de questions.

Je vais essayer de faire le point sur l’utilisation des shaders GLSL avec le moteur Irrlicht 3D, en donnant un petit exemple.

Tout d’abord je ne saurais que trop vous conseiller de lire le tutoriel officiel à ce sujet : http://irrlicht.sourceforge.net/tut010.html

Maintenant nous allons voir le code minimal pour utiliser les shaders avec Irrlicht, et nous verrons comment créer un shader GLSL.

Utiliser les shaders avec Irrlicht

Dans Irrlicht, les shaders sont des matériaux personnalisés. Ils sont donc identifiés par leur numéro de matériau.

Les assigner à un node revient donc à appeller :

node->setMaterialType ( id_materiau );

Mais encore faut-il pouvoir les créer.

Pour commencer, il faut savoir qu’un shader est un bout de code s’exécutant sur la carte graphique. Son but est de permettre au programmeur de remplacer le pipeline de rendu fixe. A terme, ce pipeline de rendu fixe est voué à la disparition, pour être entièrement remplacé par les shaders (c’est déjà le cas dans DirectX 10).

Le vertex shader est destiné aux traitements sur les sommets, et le pixel shader (ou fragment shader) directement sur les pixels.

Comme les shaders s’exécutent du côté de la carte graphique (le GPU), il faut que le programme hôte (le CPU) lui communique certaines informations utiles, comme la position de l’éclairage, ou quelques paramètres.

Ces paramètres peuvent être dynamiques, il faut donc les envoyer à chaque fois que le shader est appelé. Naturellement l’utilisation d’une callback est toute indiquée.

Voyons maintenant le code :

#include <irrlicht.h>
#include <iostream>
using namespace irr;

#include "MyShaderCallBack.h"

On inclus Irrlicht et un fichier “MyShaderCallBack.h” qui sera définit plus tard, c’est dans cet en-tête que nous implémenteront la callback dont j’ai parlé plus haut.

On initialise ensuite Irrlicht :

int main ( int argc , char** argv )
{

// Initialise Irrlicht
IrrlichtDevice* device = createDevice ( video::EDT_OPENGL , core::dimension2d<s32>(640, 480) );
video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* gui = device->getGUIEnvironment();

Et maintenant on va vérifier que notre carte graphique supporte bien les Shaders Models 1.1 (ce qui est le cas de toutes les cartes graphiques un minimum récentes !) :

// Verifie le support des shaders
bool pixel_shader_support = ( driver->queryFeature ( video::EVDF_PIXEL_SHADER_1_1 ) || driver->queryFeature ( video::EVDF_ARB_FRAGMENT_PROGRAM_1 ) );
bool vertex_shader_support = ( driver->queryFeature ( video::EVDF_VERTEX_SHADER_1_1 ) || driver->queryFeature ( video::EVDF_ARB_VERTEX_PROGRAM_1 ) );
if ( !pixel_shader_support || !vertex_shader_support )
{
std::cerr << "Les shaders 1.1 ne sont pas supportés !" << std::endl;
}

On va maintenant créer le shader :

// Creer le shader
video::IGPUProgrammingServices* gpu = driver->getGPUProgrammingServices();

// L'ID du matériau personnalisé
s32 newMaterialType = 0;

if (gpu)
{

// On instancie notre callback
MyShaderCallBack* shader_callback = new MyShaderCallBack ( device );

// On créer un nouveau matériau à partir du shader
newMaterialType = gpu->addHighLevelShaderMaterialFromFiles(
"vertex_shader.vert", "main", video::EVST_VS_1_1,
"pixel_shader.frag", "main" , video::EPST_PS_1_1,
shader_callback, video::EMT_SOLID);

shader_callback->drop();
}

Le fichier vertex_shader.vert contiendra notre vertex shader, et pixel_shader.frag notre pixel shader.

Vous voyez que j’ai ici instancié un objet “MyShaderCallBack”, nous verrons ce objet plus en détail plus tard. C’est cet objet qui enverra les paramètres et variables aux shaders.

Avec Irrlicht, on a la possibilité de se baser sur un matériau déjà existant pour notre shader, ici j’ai choisis EMT_SOLID.

On va maintenant créer une petite scène :

// Ajoute un cube texturé et lui assigne le shader
scene::ISceneNode* node = smgr->addCubeSceneNode(50);
node->setPosition ( core::vector3df ( 0 , 0 , 0 ) );
node->setMaterialTexture ( 0 , driver->getTexture ( "chess.jpg" ) );
node->setMaterialFlag ( video::EMF_LIGHTING , false );
node->setMaterialType ( (video::E_MATERIAL_TYPE)newMaterialType );

// Fait tourner le cube sur lui même
scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator ( core::vector3df ( 0 , 0.3f , 0 ) );
node->addAnimator ( anim );
anim->drop();

// Ajoute une caméra à la scène
scene::ICameraSceneNode* cam = smgr->addCameraSceneNodeFPS(0, 100.0f, 100.0f);
cam->setPosition(core::vector3df(-100,50,100));
cam->setTarget(core::vector3df(0,0,0));

La fin du code est classique :

// Boucle du jeu
while ( device->run() )
{
if ( device->isWindowActive() )
{
driver->beginScene(true, true, video::SColor(255,128,128,128));
smgr->drawAll();
driver->endScene();
}
}

device->drop();

return 0;
}

Le fichier en entier : IrrlichtShader.cpp

Intéressons-nous maintenant à la callback, qui est définie dans le fichier MyShaderCallBack.h :

class MyShaderCallBack : public video::IShaderConstantSetCallBack

Toutes les callback pour les shaders dans Irrlicht doivent implémenter l’interface IShaderConstantSetCallBack et sa méthode virtuelle OnSetConstants.

Comme nous aurons sans doute besoin d’une référence vers l’objet IrrlichtDevice pour récupérer certaines informations, on va récupérer un pointeur via le constructeur :

private :

IrrlichtDevice* device;

public :

MyShaderCallBack ( IrrlichtDevice* _device )
{
device = _device;
}

Nous enverrons pour commencer des informations de base, telle que les matrices pour les transformations ou la position et la couleur de la lumière (on trichera légèrement, en utilisant la caméra comme source de lumière) :

virtual void OnSetConstants ( video::IMaterialRendererServices* services , s32 userData )
{

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

// Set inverted world matrix
// if we are using highlevel shaders (the user can select this when
// starting the program), we must set the constants by name.
core::matrix4 invWorld = driver->getTransform(video::ETS_WORLD);
invWorld.makeInverse();
services->setVertexShaderConstant("mInvWorld", invWorld.pointer(), 16);

// Set clip matrix
core::matrix4 worldViewProj;
worldViewProj = driver->getTransform(video::ETS_PROJECTION);
worldViewProj *= driver->getTransform(video::ETS_VIEW);
worldViewProj *= driver->getTransform(video::ETS_WORLD);
services->setVertexShaderConstant("mWorldViewProj", worldViewProj.pointer(), 16);

// Set camera position
core::vector3df pos = device->getSceneManager()->getActiveCamera()->getAbsolutePosition();
services->setVertexShaderConstant ( "mLightPos" , reinterpret_cast<f32*>( &pos ) , 3 );

// Set light color
video::SColorf col(0.0f,1.0f,1.0f,0.0f);
services->setVertexShaderConstant("mLightColor", reinterpret_cast<f32*>(&col), 4);

// Set transposed world matrix
core::matrix4 world = driver->getTransform(video::ETS_WORLD);
world = world.getTransposed();
services->setVertexShaderConstant("mTransWorld", world.pointer(), 16);

// set textures
int a = 0; services->setPixelShaderConstant("Texture1", (const float*)&a, 1);
a = 1; services->setPixelShaderConstant("Texture2", (const float*)&a, 1);
a = 2; services->setPixelShaderConstant("Texture3", (const float*)&a, 1);
a = 3; services->setPixelShaderConstant("Texture4", (const float*)&a, 1);

}

Le code source est là : MyShaderCallBack.h

Intéressons nous maintenant aux shaders en eux même.

Le code minimal pour les shaders

La syntaxe du GLSL est très proche du C et est donc très facile à apprendre.

Pour commencer, on voudrait récupérer les paramètres définis par notre callback. On utilisera alors le mot clé uniform :

uniform mat4 mWorldViewProj;
uniform mat4 mInvWorld;
uniform mat4 mTransWorld;
uniform vec3 mLightPos;
uniform vec4 mLightColor;

En plus d’uniform, vous trouverez aussi le mot clé varying qui permet d’envoyer des variables du vertex shader au pixel shader.

Voyons un vertex shader minimal :

void main ( void )
{
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = mWorldViewProj * gl_Vertex;
}

Rien d’extraordinaire, on récupère les coordonnées de texture et on transforme la position du vertex. Comme vous vous en doutez, ce shader ne fait rien de plus que ce que fait déjà le pipeline de rendu fixe.

Intéressons nous maintenant au pixel shader :

uniform sampler2D Texture1;
uniform sampler2D Texture2;
uniform sampler2D Texture3;
uniform sampler2D Texture4;

On récupère les 4 textures utilisés par Irrlicht.

void main ( void )
{
vec4 color = texture2D ( Texture1 , gl_TexCoord[0] );
gl_FragColor = color;
}

On récupère la couleur du pixel venant de la texture 1 en utilisant les coordonnées de texture.

Une fois de plus, le comportement est le même qu’avec le pipeline de rendu fixe.

Le résultat : notre cube bêtement texturé

Voici enfin la partie la plus amusante : jouer avec le shader pour obtenir des effets sympatiques.

Par exemple ce pixel shader rendra le cube en négatif :

void main ( void )
{
vec4 color = texture2D ( Texture1 , gl_TexCoord[0] );
color = vec4 ( 1.0 , 1.0 , 1.0 , 1.0 ) - color;
gl_FragColor = color;
}

Vous pouvez désormais expérimenter pour trouver de nouveaux effets à appliquer à vos objets !

Mentions

Discussion

    Reply