Pour le projet, nous avons préféré utiliser les pixels et vertex shaders, contrairement à ce qui était initialement proposé. "shaders" voulant dire "nuanceurs", les Vertex Shaders sont des nuanceurs pour les vertices et les Pixels Shaders sont des nuanceurs pour les pixels. En fait, c'est le nom que l'on donne aux programmes destinés au GPU permettant de faire un traitement pour chaque vertex (dans le cas des VS) ou pour chaque pixel (dans le cas de PS). Ce procédé remplace le pipeline classique de rendu des anciennes cartes graphiques.
Les shaders sont apparus sur les GeForce 3 pour les cartes de chez NVidia en version 1.1 pour les Vertex et Pixel Shaders. Puis est arrivée la Radeon 8500 d'ATI supportant la version 1.1 des Vertex Shaders et la version 1.4 des Pixel Shaders. Ensuite, sont apparues les GeForce 4 gérant la version 1.1 des VS et la version 1.3 des PS. Puis les Radeon 9700 qui passe aux versions supérieures avec une version 2.0 pour les Vertex et Pixel Shaders. Côté NVidia, c'est la GeForce FX qui implémente la version 2.0 des Shaders. Actuellement, les nouvelles génération de cartes ont été annoncées : la GeForce 6 (NV40) chez NVidia qui gérera les versions 3.0 des Shaders et les cartes à base de X800 chez ATI qui resteront dans les Shaders en version 2.0 mais avec, d'aprés eux, beaucoup d'optimisations.
Pourquoi les shaders ?
Le principal avantage des shaders est de pouvoir programmer de nombreux rendus en temps réel avec peu ou sans de ralentissements. En outre, ils sont de plus en plus utilisés et les capacités des GPU ne cessant de s'accroitrent, il devient nécessaire de s'initier aux shaders. Connaissant cela, les exemples seront plus clairs. Il est néanmoins nécessaire de connaître un minimum le fonctionnement classique pour comprendre à quoi correspondent les valeurs que reçoivent en entrée les shaders.
Tout d'abord, le but de notre projet est "voir quels rendus non photoréalistes on peut faire avec des shaders et OpenGL". Par conséquent aucun but précis n'était à atteindre, même si nous nous sommes focalisés sur certains effets particuliers pour arriver à quelques chose de concret et de gratifiant.
Ensuite, il est important de savoir qu'au début, un shader se programmait en assembleur (spécifique aux cartes graphiques). Cette méthode avait l'avantage d'obliger le programmeur à comprendre le fonctionnement de la carte graphique, mais avait le désavantage d'être lourd et long à écrire. Fort de cette remarque, des personnes ont essayé de faire des langages de haut niveau compilables avec les différents langages d'assemblage. Microsoft a créé le langage HLSL (High Level Shading Language) destiné uniquement à DirectX. Pour OpenGL, le langage GLSL (openGL Shading Language) est en cours de création et devrait être inclus dans la version 2.0 de l'API (même si un compilateur existe déjà). Malheureusement, aucun de ces langages n'est utilisable à la fois sous DirectX et OpenGL, ce qui peut poser problème pour certains développeurs. Alors est arrivé NVidia avec le Cg (C for graphics) qui lui est portable. Il suffit de lui donner le profil (version) du shader, et il compile en faisant attention aux limitations de chacun. L'avantage de ce langage est son indépendance et son évolutivité. En effet, dès qu'il y aura de nouvelles versions de shaders, il suffira de mettre à jour son compilateur pour pouvoir les utiliser. De plus, l'API C++ fournie permet de demander l'utilisation de la meilleure version possible d'un shader selon les limitations des profils et selon la machine sur laquelle le shader se compile. De cette façon, le programme C/C++ utilisant du Cg n'a même pas besoin d'etre recompilé, il s'adapte automatiquement.
Dans le cadre de ce projet, le Cg s'est donc imposé comme le langage de programmation de nos shaders.
Pour nous familiariser avec ce langage, nous avons utilisé un outils fournit par NVidia : CgLabs (ou CgTutorials). Ce programme fournit un environnement de développement pour Cg et permet de programmer/compiler des programmes Cg avec un rendu direct dans la fenêtre d'affichage. De plus, plusieurs exemples très différents sont fournis et permettent de voir ce que l'on peut faire avec les Shaders. Nous avons donc fait quelques tests et programmes indépendants du projet afin de nous imprégner de la philosophie de cette programmation particulière. Nous ne montrerons pas ces tests ici pour ne pas charger le compte rendu, mais voici leur description très brève : passage de la couleur aux nuances de gris, éclairage par vertex (qu'il faut refaire puisque les shaders casse le pipeline classique !), éclairage par pixel, isolation des canaux de couleurs, ...
Aprés cette documentation et ces tests, nous avons commencé à faire des shaders liés au rendu non photoréaliste. Nous allons décrire dans l'ordre tous les programmes que nous avons écris. On précise qu'au début nous avons développé avec CgLabs pour plus de facilité, mais au fur et à mesure que nous voulions faire des choses complexes, il nous a été nécessaire de faire notre propre programme C++. Nous indiquerons pour chacun d'eux la ou les méthodes que nous avons essayées.
Méthode |
CgLags,
Notre programme |
Version
VS |
1.1 |
Version
PS |
1.1 |
Le but de ce programme était de simuler une détection de bords avec une texture 1D qui permettrait d'assombrir plus ou moins la couleur des vertex selon leur normale. Pour faire cela, il suffit de calculer le produit scalaire entre la normale d'un vertex donné (recupéré dans le VS) et le vecteur de direction du vertex à la caméra. Si les deux vecteurs sont normalisés, la valeur récupérée est dans [-1,1], il suffit donc de couper cet intervalle de façon à ce que toutes les valeurs négatives deviennent égales à zéro. Ainsi, on récupère une valeur dans [0,1], ce qui est idéal pour faire une coordonnée de texture. Il suffit ensuite de remarquer que le produit scalaire vaut 0 quand la normale désigne une face avant non visible ou perpendiculaire au vecteur de la caméra, ce qui permet de faire une texture de bords (dont les couleurs servent de facteur multiplicatif pour la couleur finale) adaptée à la détection de bords.
Nous avons essayé plusieurs textures pour simuler les bords : Noir/Blanc avec passage net de l'une à l'autre, Noir vers Blanc avec dégradé, Noir vers Blanc par paliers.
Cette méthode ne marche pas très bien pour les détections de bords et de silhouette : premièrement, si le modèle n'est pas arrondi, on est obligé de prolonger la partie sombre de la texture pour voir apparaître les bords, ce qui peut entraîner la détection de grosses partie du modèle comme des bords, alors qu'elles ne le sont pas. Deuxièmement, cette détection dépend de l'orientation du modèle par rapport à la caméra ce qui est très génant pour avoir un bon rendu permanent. Et enfin troisièmement, la détection ne marche pratiquement pas sur des modèles avec des angles marqués ou trop plat.
On remarquera que la détection de bords avec cette méthode se transforme en Toon Shading si on utilise une texture par paliers.
Méthode |
CgLags,
Notre programme |
Version
VS |
1.1 |
Version
PS |
1.1 |
On reprend le même principe que les bords simples sauf que cette fois on utilise un texture de bords en 2D. La coordonnée u est toujours calculée de la même façon (produit scalaire), et la coordonnée v peut être calculée de plusieurs façon. L'idée est de donner un effet de dessin selon l'axe V, on peut donc choisir plusieurs style d'évolution de la texture.
Nous avons essayé deux calculs pour la coordonnée v : calcul simple (vertex.x + vertex.y), calcul complexe tenant compte de la distance de la caméra par rapport a la texture pour pouvoir reduire ou augmenter la précision.
Le premier calcul donne un bon rendu si on ne s'approche pas trop du modèle. Par contre, si le modèle possède de gros polygones, l'étirement de la texture est trop visible.
Le deuxième calcul est mieux puisqu'il tient compte de la distance, donc de la taille des polygones. La texture est toujours plaquée de la même façon (en tout cas, cela donne cette impression) pourvu que les coordonnées dans la texture soient assez grandes : il ne faut pas travailler sur [0,1], mais sur un intervalle plus grand, c'est-à-dire sur [0,4] par exemple. En effet, un décalage de 0.2 dans l'intervalle [0,1] est plus visible que dans l'intervalle [0,4].
Nous avons ensuite essayé plusieurs textures : paliers bruités, paliers avec pseudo dégradés à l'aide de traits, ...
A part la première qui donne un bon effet, les autres n'étaient pas satisfaisantes.
Méthode |
CgLags |
Version
VS |
1.1 |
Version
PS |
1.1 |
On reprend les bords simples avec la texture par paliers qui donne l'effet Toon Shading et on essaye de donner une sorte de style de dessin. Pour cela on projette sur la scene une texture donnant le style. Cette projection ce fait par rapport à la caméra, ce qui fait que si la caméra ne bouge pas, la projection est toujours la même : elle ne suit donc pas le vertex !
L'effet est plutôt intéressant quelque soit la texture projetée. Nous avons essayé plusieurs style : traits horizontaux, verticaux, obliques, cadrillage, effet crayon, effet points.
Méthode |
CgLags |
Version
VS |
1.1 |
Version
PS |
2.0 |
On reprend les bords simples avec styles sauf que l'on enlève des pixels selon la texture projetée. Par exemple, avec le style donnant l'effet de points, on enleve tous les pixels qui ne sont pas des points. Cette technique donne un rendu correct, mais le probème reste que les polygones faces à la caméra, mais normalement derrière d'autres polygones, apparaissent dans les trous laissés par les pixels enlevés.
Méthode |
CgLags,
Notre programme |
Version
VS |
1.1 |
Version
PS |
1.1 |
On reprend l'effet Toon Shading
donné par les bords simples avec la texture par paliers, sauf
qu'au lieu de comparer les normales à la direction de la
caméra, on les compare à la direction de la
lumière. D'autres calculs sont ensuite fait, comme
l'atténuation de la lumière selon la distance ou la
couleur de la lampe, pour donner un meilleur rendu. Pour utiliser
plusieurs lumières, il suffit d'additionner les couleurs
obtenues avec chacune des lumières. En version PS 1.1,
on peut gérer 2 lumières à la fois. Si on
augmentait de version, on pourrait en gérer plus, mais cela
n'aurait pas d'intérêt dans le cadre du projet.
Méthode |
CgLags,
Notre programme |
Version
VS |
1.1 |
Version
PS |
1.1 |
Comme nous l'avons remarqué, les bords que nous utilisons jusqu'à présent ne permettent pas d'avoir toujours une ligne noire qui délimite l'objet 3D comme dans les bandes dessinée. Nous avons alors utilisé une méthode qui consite à étendre les faces cachées. Il suffit alors de déplacer légèrement les vertex cachés dans le sens de la normale pour qu'une ligne supplémentaire apparraisse. Les vertex cachés sont ceux dont le produit scalaire entre la normale et la direction avec la camera est négatif. Il faut ensuite mettre en noir les pixels qui sont sur les faces cachées pour que la ligne devienne noire. On remarque cependant que le déplacement selon la somme entre le normale et la direction de la camera donne un resultat plus homogène.
Méthode |
CgLabs |
Version
VS |
1.1 |
Version
PS |
1.1 |
CgLabs permet de générer automatiquement la Shadow Map d'une scène. On peut alors facilement ajouter les ombres. Le résultat est evidemment plus réaliste. Cependant, même si le principe de la shadow map n'est pas compliqué, son implémentation avec OpenGL et Cg nécessite un temps apprentissage que nous n'avons malheureusement pas pu avoir durant le projet.
Méthode |
CgLabs,
Notre programme |
Version
VS |
1.1 |
Version
PS |
2.0 |
Une technique souvent utilisée
en bande dessinée pour
donner une impression de mouvement est l'ajout de traits noirs à
l'arrière d'un objet. Nous avons essayé d'implanter cette
technique directement avec les shaders. Comme on ne peut ajouter
directement des vertex ou des pixels, nous avons du trouver une
solution en utilisant les vertex existant. Une technique consiste
à étendre les faces cachées qui sont
opposées à la direction du mouvement comme pour
l'accentuation de bord. On enlève ensuite tous les pixels
déplacés à l'exception de quelques lignes noires
dans la direction du mouvement. Cependant ne connaissant pas la
position des pixels les uns par rapport aux autres, on ne peut pas
tracer
facilement des lignes. On cherche alors un axe sur lequel les vertex
alignés se projetent au même endroit. Un tel axe est
obtenu en
faisant le produit vectoriel entre la direction du mouvement et la
direction de la caméra. La projection de la position des vertex
alignés donne alors sensiblement la même valeur. En
utilisant cette valeur sur une texture 1D avec quelques
zones noires, on obtient des traits à l'arrière de
l'objet. Dans une première ébauche, nous ne dessinions
les traits qu'en utilisant les textures cachées car en
translatant les vertex, on modifie la forme du modèle. Nous
avons alors utilisé deux passes, une calculant uniquement les
traits et l'autre le reste du toon shading.
Méthode |
Notre
programme |
Version
VS |
1.1 |
Version
PS |
1.1 |
Nous avons essayé de faire
l'effet dessiné. Cela consiste a extraire les bords comme pour
l'effet 6, en appliquant la couleur blanche sur les vertices aux bords
et la couleur noire sur les autres. Ainsi, un dégradé se
forme entre les bords et l'intérieur du modèle. On
applique sur les vertex une image permettant de simuler tel ou tel
outil de dessin (feutre, crayon, ...). Il suffit ensuite d'utiliser la
valeur du dégradé variant dans [0,1] comme
coordonnée
de texture u. On utilise pour
la coordonnée v la
valeur absolue du produit scalaire de la normale d'un vertex au bord
avec le vecteur de vue. En plus de cela, on ajoute deux
paramêtres
: une valeur de déplacement des bords qui permet d'extraire plus
ou moins les vertices aux bords, et une valeur F de départ dans
la texture. Cette dernière permet de faire varier la
coordonnée
u dans [F,1], ce qui peut
être utile pour les modèles ayant des angles
marqués.
En plus de cela, on affiche une sky box avec une texture de papier
permettant de simuler le dessin sur une feuille et on fait du blending
sur les traits selon le degradé décrit. Cela permet de
fusionner les couleurs et de donner un effet d'encre.
Ce projet nous a permis de comprendre
les shaders et d'apprendre un langage permettant de les manipuler. On
s'est aussi apperçu que pour obtenir des rendus complexes et
précis
il
fallait utiliser les fonctionnalités d'OpenGL afin de
préparer
les
modèles, ou pour donner plus d'informations aux shaders
(exemples :
récupérer les pixels voisins du pixel courant, ordre
d'affichage des
faces, ...). Malheureusement, l'utilisation d'OpenGL de facon
avancée
nécessite un apprentissage assez long (bien plus que le Cg) et
nous
n'avons pas eu le temps d'en utiliser pour les derniers effets qui en
nécessitaient.
L'utilisation des shaders via Cg est tres intéressante et assez simple à apprendre. Nous pensons qu'il serait bien de l'inclure dans le cours d'OpenGL en 3ème année.