1. Introduction

Ce tutoriel a pour but d'explorer le mécanisme d'utilisation d'un shader avec OpenSceneGraph.

Celui-ci possède une interface simple sous la forme de sa classe osg::shader.

1-A. Qu'est-ce qu'un shader ?

Ceci n'étant pas le but du tutoriel, mais pouvant quand même être utile, nous allons donner une rapide définition de ce qu'est un shader.

Un shader est tout d'abord un programme, écrit dans un certain langage. Ici nous utiliserons le GLSL pour « OpenGL Shading Language ».

Les shaders servent en fait à programmer le pipeline de rendu de la carte graphique pour lui faire faire ce que l'on veut, allégeant ainsi la charge de travail du processeur.

Il existe plusieurs types de shaders :

  • les vertex shaders, qui permettent de modifier les attributs des différents sommets d'une scène ;
  • les pixels shaders, aussi appelés fragment shaders avec lesquels on va pouvoir modifier le rendu de chaque pixel qui s'affiche à l'écran.

Osg nous permet de charger tous les types de shaders très simplement, et de les activer/ désactiver comme on le souhaite.

Ces différents shaders sont ensuite incorporés dans ce qu'on appelle un « program », l'exécutable qui va permettre de faire tourner ces shaders.

2. Classes utilisées

On va utiliser ici un certain nombre de classes basiques n'ayant pas de rapport direct avec les shaders, on en donnera juste la liste. Pour leurs descriptions, voir les autres tutoriels comme celui sur la lumière qui en utilise un grand nombre en commun.

osg::Group , osgViewer::Viewer , osg::Geode, osg::ShapeDrawable, osg::Sphere (et autres primitives).

Ainsi que plusieurs classes particulières pour les shaders :

  • osg::shader ;
  • osg::Program ;
  • osg ::Uniform.

3. Mise en pratique

Osg peut charger des shaders contenus dans des fichiers, mais aussi dans une simple chaîne de caractères. Étant donné la simplicité des shaders utilisés pour ce tutoriel, nous allons simplement les stocker dans une chaîne de caractères, en tant que variable globale du programme.

Nous allons donc utiliser les deux types de shaders principaux : un vertex shader et un fragment shader.

Le vertex shader appliquera un effet d'aplatissement des formes 3D du programme, en annulant une des composantes de chaque sommet afin que la forme 3D soit aplatie sur un plan.

Le fragment shader changera simplement la couleur de ces formes par un couleur qu'on lui passera via une variable de type Uniform.

Voici nos deux shaders stockés dans deux chaînes de caractères :

 
Sélectionnez
static const char* vertSource = 
"uniform bool plat;" 
"void main(void)" 
"{" 
" 
vec4 v = vec4(gl_Vertex);" 
" 
if(plat)" 
" 
v.y = 0.0;" 
" 
gl_Position = gl_ModelViewProjectionMatrix*v;" 
"}"; 

static const char* fragSource = 
"uniform vec4 color;" 
"void main()" 
"{" 
" 
gl_FragColor = color;" 
"}"; 

3-A. Comment charger un shader et l'activer ?

Commençons d'abord par le code minimal de la fonction principale :

création d'un viewer et installation de la fenêtre, la variable pGeode est un nœud d'objet 3d que nous allons construire et auquel nous allons attacher un shader.

 
Sélectionnez
// Construction du viewer 
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer; 

// Création d’une fenêtre 512x512 à 32 32 
viewer->setUpViewInWindow( 32, 32, 512, 512 ); 

/* 
Code lié aux shaders et aux objets de la scène 
*/ 

viewer->setSceneData(pGeode); 
// exécution de la boucle principale 
return viewer->run(); 

Pour charger un shader on construit un objet de la classe osg ::shader. Le constructeur de cette classe prend en paramètre le type de shader (vertex ou fragment).

Après avoir construit l'objet on appelle la fonction setShaderSource sur celui-ci avec comme paramètre la chaîne de caractères de notre shader.

 
Sélectionnez
osg::ref_ptr<osg::Shader> f = new osg::Shader( osg::Shader::FRAGMENT ); 
f->setShaderSource( fragSource ); 
 
Sélectionnez
osg::ref_ptr<osg::Shader> v = new osg::Shader( osg::Shader::VERTEX ); 
v->setShaderSource( vertSource ); 

Ici nos shaders sont chargés et construits mais pas encore utilisables. Pour cela il faut les insérer dans un « program ».

On peut aussi les charger à partir d'un fichier externe au « program ». On utilise alors la fonction loadShaderSourceFromFile de la classe osg ::shader :

 
Sélectionnez
v->loadShaderSourceFromFile("monShader.vert"); 

On crée donc un objet de la classe osg ::progam auquel on peut donner un nom (utile pour le debug) et ajouter des shaders. Nous allons donc lui ajouter les deux shaders que nous venons de construire.

 
Sélectionnez
osg::ref_ptr<osg::Program> rPrg = new osg::Program; 
rPrg->setName( "myShader" ); 
rPrg->addShader( f.get() ); 
rPrg->addShader( v.get() ); 

Nos shaders sont maintenant utilisables, il ne nous reste donc plus qu'à les activer. Mais il faut d'abord avoir quelque chose sur quoi les activer. Nous allons donc créer un ensemble d'objets 3D sur lesquels nos shaders vont agir :

 
Sélectionnez
/* Construction de la scène */ 
osg::Geode* pGeode = new osg::Geode(); 

// On crée quelques formes simples 
pGeode->addDrawable( new osg::ShapeDrawable( 
new osg::Sphere(osg::Vec3(0.0f,0.0f,0.0f),0.5f) ) ); 

pGeode->addDrawable( new osg::ShapeDrawable( 
new osg::Box(osg::Vec3(2.0f,0.0f,0.0f),2.0f) ) ); 

pGeode->addDrawable( new osg::ShapeDrawable( 
new osg::Cone(osg::Vec3(4.0f,0.0f,0.0f),0.5f,3.0f) ) ); 

pGeode->addDrawable( new osg::ShapeDrawable( 
new osg::Cylinder(osg::Vec3(6.0f,0.0f,0.5f),0.5f,3.0f) ) ); 

pGeode->addDrawable( new osg::ShapeDrawable( 
new osg::Capsule(osg::Vec3(8.0f,0.0f,1.0f),0.5f,3.0f) ) ); 

Nous voila donc avec une sphère, un cube, un cône, un cylindre et une capsule que l'on va ajouter à notre scène et sur lesquels ont va pouvoir activer nos shaders.

Pour activer ceux-ci, il faut passer par le membre StateSet, donc nous avons déjà présenté les fonctions dans plusieurs tutoriels précédents.

 
Sélectionnez
// on active le shader 
pGeode->getOrCreateStateSet()->setAttributeAndModes( rPrg.get(), osg::StateAttribute::ON ); 

Il est nécessaire d'activer le « program » au moins une fois, sinon celui-ci n'est pas pris en compte. Dans notre programme de test, sa désactivation via osg ::StateAttribute ::OFF semble n'avoir aucun effet.

Ici nos shaders n'auront donc d'effet que sur les objets 3D contenus dans pGeode et uniquement dans ceux-là.

3-B. Comment passer un ou plusieurs paramètres à un shader ?

Comme on peut le voir dans le code des shaders ceux-ci possèdent des variables de type « uniform ». Ce sont elles qui permettent de faire communiquer le programme principal et le shader.

Les valeurs de ces variables se modifient grâce encore une fois à la classe StateSet et à sa fonction addUniform, qui prend en paramètre un objet de type osg ::Uniform. À la construction de cet objet Uniform on doit préciser le nom de la variable uniform correspondant dans le shader (variable à laquelle la valeur va être attribuée) ainsi que la variable qui va être envoyée. Cette variable peut être d'un grand nombre de type (voir la documentation Osg qui contient toutes les surcharges de constructeurs).

On peut ainsi passer des types simples : int, bool, float, des textures (sampler) mais aussi des types plus complexes d'Osg comme des vec2, vec3 ou bien des objets de type Matrix.

 
Sélectionnez
Geode->getOrCreateStateSet()->addUniform( new osg::Uniform("color",osg::Vec4(1.0,1.0,0.0,1.0))); 

Donc, le code ci-dessus dit : « Donne la valeur 1.0, 1.0, 0.0, 1.0 à la variable Uniform nommée color du shader attaché à pGeode ».

Il doit bien évidement n'y avoir qu'une seule valeur Uniform de ce nom dans tous les shaders attachés à pGeode. Le type de la variable passée doit être du même type dans le shader. Ici on passe un osg::Vec4 la variable Uniform du shader est aussi un vec4, Osg fait la conversion tout seul.

On peut ensuite récupérer cette variable ou bien la modifier grâce aux fonctions de la classe osg ::Uniform.

3-C. Le passage de texture

Le passage de texture à un shader sous Osg utilise lui aussi la classe osg::Uniform avec en plus les possibilités de chargement de Textures d'Osg.

On crée d'abord un objet de type osg::Texture que l'on paramètre comme on le veut (image à charger, filtre, ect.). On attache cette texture à un StateSet en lui donnant un identifiant comme si on voulait l'utiliser normalement. Puis on crée un objet Uniform auquel on passe le nom du sampler dans le shader et l'identifiant de la texture et le reste fonctionne comme précédemment.

3-D. Le programme de test

Le programme de test construit pour ce tutoriel propose le chargement d'un vertex shader et d'un pixel shader et le passage de paramètres uniform à ceux-ci.

On a en plus défini une gestion des évènements clavier pour pouvoir interagir en temps réel avec nos shaders. Pour plus de détails sur les évènements clavier, voir le tutoriel qui leur est dédié. Cette gestion d'évènements permet via les touches fléchées, de changer la couleur des objets 3D affichés, mais aussi de les rendre plats avec la touche Alt gauche. Toutes ces transformations sont bien sûr faites avec les shaders.

Image non disponible
On peut changer la couleur des primitives grâce aux touches fléchées

Le code source est disponible avec le tutoriel pour plus de détails bien qu'une grande partie du code ait été décrite précédemment.

Image non disponible
Mais aussi changer la géométrie pour rendre les objets plats

4. Pour aller plus loin

Pour plus d'informations sur les classes utilisées ici, voir la documentation d'OSG :

5. Remerciements

Merci jacques_jean pour les corrections.