Rendu Non Photoréaliste avec des Shaders


Jean-Baptiste SILVY - Clément VIGUIER
ENSIMAG 2A - IMAGE




  1. Introduction
  2. Premières tentatives
  3. Entrée en matière
    1. Bords simples
    2. Bords avancés
    3. Bords simples avec style
    4. Bords simples avec style et suppression de pixels
    5. Toon Shading avec lumières
    6. Accentuation des bords pour le toon shading
    7. Gestion des ombres
    8. Affichage de lignes pour donner une impression de mouvement
    9. Effet dessin
  4. Organisation
  5. Conclusion


  1. Introduction

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.

  1. Premières tentatives

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, ...

  1. Entrée en matière

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.

  1. Bords simples


Méthode
CgLags, Notre programme
Version VS
1.1
Version PS
1.1

BE1.jpg
BE2.jpg

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.

  1. Bords avancés


Méthode
CgLags, Notre programme
Version VS
1.1
Version PS
1.1

AE1.jpg
AE2.jpg

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.

  1. Bords simples avec style


Méthode
CgLags
Version VS
1.1
Version PS
1.1

BES1.jpg
BES2.jpg

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.

  1. Bords simples avec style et suppression de pixels


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.

  1. Toon Shading avec lumières


Méthode
CgLags, Notre programme
Version VS
1.1
Version PS
1.1

chevy.jpg
chevy2.jpg

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.

  1. Accentuation des bords pour le toon shading


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.

  1. Gestion des ombres


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.

  1. Affichage de lignes pour donner une impression de mouvement


Méthode
CgLabs, Notre programme
Version VS
1.1
Version PS
2.0

chevy_depl.jpg
Porsche_depl.jpg

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.

  1. Effet dessin


Méthode
Notre programme
Version VS
1.1
Version PS
1.1

S1.jpg
S2.jpg

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.

  1. Organisation

Nous avons procédé en plusieurs étapes, comme nous l'avons indirectement dis jusqu'à maintenant. Nous rappelons rapidement l'évolution au fil des semaines et comment nous nous sommes organisés pendant cette pèriode :
  1. Conclusion

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.