Latest article: Exporter des données depuis Excel

Cel Shading en GLSL

Un tutoriel pour faire un effet de cel shading (effet “dessin animé”) avec OpenGL et GLSL

Un petit article rapide pour expliquer la création d’un effet de cel shading avec OpenGL et GLSL.

Le cel shading, c’est l’ “effet cartoon” que vous pouvez voir à l’œuvre dans Jet Set Radio ou Zelda The Wind Waker.

Le nom de cet effet viens des cellulos utilisés lors de la création d’un dessin animé, ce sont des feuilles de papier transparentes dont on peint le verso afin de colorier les personnages tracés au recto.

Toutefois, je n’expliquerai que la création du dégradé, pas des bordures noires. Ces dernières sont un autre sujet, passablement plus compliqué.

Je pars du principe que vous avez quelque base en GLSL, au moins savoir comment réaliser un shader tout simple (par exemple colorier un modèle en rouge).

Tout d’abord, analysons l’effet :

 

Capture d'écran de Zelda The Wind Waker

Ce qui donne cet effet cartoon, c’est avant tout les textures unies, les modèles non réalistes et les couleurs pastelles. Le but du shader sera d’appliquer les ombres sur le modèle 3D. Contrairement au rendu dit “photo-réaliste”, le cel shading emploie des ombres franches, bien marquées, comme dans les dessins animés.

Le Vertex Shader

Le vertex shader est ici très simple, puisqu’il ne fait rien de spécial.

varying vec3 Normal;

void main(void)
{

// Front color
gl_FrontColor = gl_Color;

// Determine the normal of the vertex
Normal = gl_NormalMatrix * gl_Normal;

// Textures coordinates
gl_TexCoord[0] = gl_MultiTexCoord0;

// The position of the vertex
gl_Position = ftransform();

}

Si vous êtes un minimum familier avec GLSL, ce code vous sera sans doute très parlant. Il fait le minimum que l’on exige d’un vertex shader : transformer les sommets, récupérer les coordonnées de texture et déterminer les normales.

La variable Normal sera passée au pixel shader grâce au mot-clé varying.

Le Pixel Shader

Voyons maintenant un pixel shader tout simple, qui affiche le modèle 3D de façon unie :

uniform sampler2D Texture0;

varying vec3 Normal;
void main (void)
{

vec4 color = texture2D( Texture0 , vec2( gl_TexCoord[0] ) );

gl_FragColor = color;
}

Une fois de plus, le code est vraiment très simple : il se contente de récupérer le pixel sur la texture aux coordonnées données, puis déclare que c’est ce pixel là qui sera utilisé pour l’affichage.

Remarquez que l’on retrouve notre variable Normal qui est la normale du sommet, on va en avoir besoin pour réaliser le cel shading.

Comme tout modèle d’éclairage, le cel shading a besoin d’une lumière pour fonctionner. Pour faire simple, on va se content de gérer une seule source de lumière, incolore.

On va donc rajouter dans la déclaration des variables la direction de la lumière  :

uniform vec3 LightDir;

En général, on s’en sert pour représenter le soleil.

Créons maintenant une nouvelle fonction :

vec4 CelShading ( vec4 color )

{

return color;

}

que l’on va appeller comme ceci :

void main (void)
{

vec4 color = texture2D( Texture0 , vec2( gl_TexCoord[0] ) );

color = CelShading ( color );

gl_FragColor = color;
}

C’est dans cette fonction que l’on appliquera l’effet du cel shading. L’avantage de le faire dans une fonction plutôt que directement dans main est multiple :

  • Vous pouvez rajouter une couche de “pré-processeur” à votre code qui gère les programmes afin de réaliser un équivalent du #include du C/C++ : dans ce cas vous pourrez séparer vos différents effets dans différents fichiers.
  • Et ainsi mélanger les effets entre eux facilement, par exemple :
color = RimLighting ( color );
color = CelShading ( color );

(Pour info, le Rim lighting est la technique appliquant à un modèle 3D une fausse lumière semblant venir de derrière lui. Mario Galaxy l’utilise sans cesse)

Revenons à notre fonction, on va calculer l’intensité de la lumière à un point donné :

float Intensity = dot( LightDir , normalize(Normal) );

C’est actuellement la partie la plus compliqué du cel shading (si si !).

On fait un produit scalaire entre le vecteur tracé entre la direction de la lumière, et la normale de notre sommet. Cela nous retourne l’intensité de la lumière sur ce dit sommet.

Mon but n’est pas d’expliquer la partie mathématique derrière tout ça… je pense que j’en serai bien incapable !

L’intensité est une valeur entre 0 et 1. 0 signifie pas éclairé, et 1 entièrement éclairé.

Prenez le temps d’essayer ce code :

vec4 CelShading ( vec4 color )
{
float Intensity = dot( LightDir , normalize(Normal) );

float factor = Intensity;

color *= vec4 ( factor, factor, factor, 1.0 );

return color;
}

On a alors un ombrage de Gouraud (gouraud shading), les ombres sont douces, il y a un dégradé.

Notre but sera de “casser” ce dégradé pour faire des ombres franches.

Comment faire ? C’est très simple : en testant la valeur de l’intensité non plus comme une valeur linéaire, mais comme des bornes.

vec4 CelShading ( vec4 color )
{
float Intensity = dot( LightDir , normalize(Normal) );
float factor = 1.0;
if ( Intensity < 0.5 ) factor = 0.5;
color *= vec4 ( factor, factor, factor, 1.0 );

return color;
}

Si l’intensité est inférieure à 0,5 (la moitié), alors on obscurcit le pixel, sinon (si c’est au dessus), on le laisse tel quel.

L’effet est là :

 

L'effet de cel shading

Il est désormais possible de faire des réglages, par exemple en rajoutant plusieurs étapes au dégradé :

float factor = 0.5;

if      ( Intensity > 0.95 ) factor = 1.0;
else if ( Intensity > 0.5  ) factor = 0.7;
else if ( Intensity > 0.25 ) factor = 0.4;

 

Maintenant c’est à vous d’expérimenter !

Discussion

Reply