Developpez.com

Club des développeurs et IT pro
Plus de 4 millions de visiteurs uniques par mois

OpenGL Moderne

Tutoriel 13 : application des normales

Bienvenue dans notre treizième tutoriel ! Aujourd'hui, on va parler de l'application des normales.

Commentez Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Site personnel

Traducteur : Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Navigation

Tutoriel précédent : extensions OpenGL

 

Sommaire

 

Tutoriel suivant : rendu dans une texture

I. Introduction

Depuis le huitème tutoriel : shaders de base, vous savez comment obtenir un ombrage décent en utilisant les normales des triangles. Jusqu'à présent, il existait un inconvénient : on n'avait qu'une seule normale par sommet dans chaque triangle et elle va varier doucement, contrairement aux couleurs, qui sont échantillonnées à partir d'une texture. L'idée de base de l'application des normales (normal mapping) est de donner aux normales des variations similaires.

II. Textures de normales

Une « texture de normales » ressemble à cela :

Exemple texture de normales

Dans chaque texel RGB est encodé un vecteur XYZ : chacune des composantes d'une couleur est comprise entre 0 et 1 et chacune des composantes d'un vecteur est entre -1 et 1, donc la conversion d'un texel en normale s'effectue ainsi :

 
Sélectionnez
normal = (2*color)-1 // sur chaque composante

La texture a une teinte bleue car après tout, la normale pointe vers « l'extérieur de la surface » ; comme toujours X va vers la droite, Y vers le haut.

Cette texture est appliquée exactement comme la texture de diffusion ; le gros problème est la conversion de notre normale, qui est exprimée dans l'espace de chaque triangle (espace tangent, aussi appelé espace de l'image), vers l'espace modèle (car c'est ce que l'on utilise dans notre équation d'ombrage).

III. Tangente et bitangente

Maintenant, que vous connaissez les matrices, vous savez que pour définir un espace (dans notre cas, l'espace tangent), on a besoin de trois vecteurs. On a déjà le vecteur UP : c'est la normale, donnée par Blender ou calculée à partir du triangle à l'aide d'un produit scalaire. Elle est représentée en bleu, tout comme la teinte de la texture de normales :

Vecteur normale

Ensuite, on a besoin d'une tangente, T : un vecteur perpendiculaire à la surface. Mais il y a tellement de vecteurs correspondant à ce critère :

Vecteur tangent

Lequel choisir ? En théorie, n'importe lequel, mais on doit être consistant avec ses voisins pour éviter de voir apparaître d'affreuses bordures. La méthode standard est d'orienter la tangente dans la même direction que les coordonnées de textures :

Vecteur tangent à partir des UV

Comme on a besoin de trois vecteurs pour définir une base, on doit aussi calculer la bitangente B (qui peut être n'importe quel autre vecteur tangent, mais si tout est perpendiculaire, les mathématiques sont simplifiées) :

Normale, tangente et bitangente à partir des UV

Voici l'algorithme : si on appelle deltaPos1 et deltaPos2 deux côtés de notre triangle et deltaUV1 et deltaUV2 les différences de coordonnées UV correspondante, on peut exprimer notre problème avec l'équation suivante :

 
Sélectionnez
deltaPos1 = deltaUV1.x * T + deltaUV1.y * B 
deltaPos2 = deltaUV2.x * T + deltaUV2.y * B

Trouvez les solutions de ce système pour T et B et vous obtenez vos vecteurs ! Voir le code ci-dessous.

Une fois que l'on a les vecteurs T, B et N, on obtient aussi cette jolie matrice qui nous permet de passer de l'espace tangent à l'espace modèle :

kitxmlcodelatexdvp\begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix}finkitxmlcodelatexdvp

Avec cette matrice TBN, on peut transformer les normales (extraites à partir de la texture) dans l'espace modèle. Par contre, habituellement c'est l'inverse que l'on fait : transformer tout de l'espace modèle vers l'espace tangent et garder les normales telles quelles. Tous les calculs sont faits dans l'espace tangent, ce qui ne change absolument rien.

Pour effectuer cette transformation inverse, on doit simplement prendre l'inverse de la matrice, ce qui dans ce cas (une matrice orthogonale, où chaque vecteur est perpendiculaire aux autres. Voir la section « Aller plus loin ».) est aussi une transposée, moins coûteuse à calculer :

 
Sélectionnez
invTBN = transpose(TBN)

Soit :

kitxmlcodelatexdvp\begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix}^T = \begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \\ N_x & N_y & N_z \end{bmatrix}finkitxmlcodelatexdvp

IV. Préparation du VBO

IV-A. Calcul des tangentes et bitangentes

Comme les tangentes et bitangentes sont nécessaires en plus des normales, on doit les calculer pour la globalité du modèle. On fait cela dans une fonction à part :

 
Sélectionnez
void computeTangentBasis( 
    // entrées
    std::vector<glm::vec3> & vertices, 
    std::vector<glm::vec2> & uvs, 
    std::vector<glm::vec3> & normals, 
    // sorties
    std::vector<glm::vec3> & tangents, 
    std::vector<glm::vec3> & bitangents 
){

Pour chaque triangle, on calcule le côté (deltaPos) et le deltaUV.

 
Sélectionnez
for ( int i=0; i<vertices.size(); i+=3){ 
 
    // Raccourcis pour les sommets
    glm::vec3 & v0 = vertices[i+0]; 
    glm::vec3 & v1 = vertices[i+1]; 
    glm::vec3 & v2 = vertices[i+2]; 
 
    // Raccourcis pour les UV
    glm::vec2 & uv0 = uvs[i+0]; 
    glm::vec2 & uv1 = uvs[i+1]; 
    glm::vec2 & uv2 = uvs[i+2]; 
 
    // Côtés du triangle : delta des positions
    glm::vec3 deltaPos1 = v1-v0; 
    glm::vec3 deltaPos2 = v2-v0; 
 
    // delta UV
    glm::vec2 deltaUV1 = uv1-uv0; 
    glm::vec2 deltaUV2 = uv2-uv0;

On peut utiliser la formule pour calculer la tangente et la bitangente :

 
Sélectionnez
float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x); 
glm::vec3 tangent = (deltaPos1 * deltaUV2.y   - deltaPos2 * deltaUV1.y)*r; 
glm::vec3 bitangent = (deltaPos2 * deltaUV1.x   - deltaPos1 * deltaUV2.x)*r;

Finalement, on remplit les tampons tangents et bitangents. Rappelez-vous, ces tampons ne sont pas encore indexés, donc chaque sommet possède sa propre copie.

 
Sélectionnez
    // Définit la même tangeante pour les trois sommets du triangle.
    // Ils seront rassemblés plus tard, dans vboindexer.cpp 
    tangents.push_back(tangent); 
    tangents.push_back(tangent); 
    tangents.push_back(tangent); 
 
    // Même chose pour les binormales
    bitangents.push_back(bitangent); 
    bitangents.push_back(bitangent); 
    bitangents.push_back(bitangent); 
 
}

IV-B. Indexation

L'indexation du VBO est très similaire à ce que l'on avait l'habitude de faire, mais il y a une légère différence.

Si on trouve un sommet similaire (même position, même normale, même coordonnées de texture), on ne souhaite pas utiliser la tangente et bitangente ; au contraire, on souhaite en faire la moyenne. Donc, modifiez un peu le vieux code :

 
Sélectionnez
// Essaie de trouver un sommet similaire dans out_XXXX 
unsigned int index; 
bool found = getSimilarVertexIndex(in_vertices[i], in_uvs[i], in_normals[i],     out_vertices, out_uvs, out_normals, index); 
 
if ( found ){ // Un sommet similaire est déjà dans le VBO, on l'utilise à la place ! 
    out_indices.push_back( index ); 
 
    // Moyenne des tangentes et bitangentes
    out_tangents[index] += in_tangents[i]; 
    out_bitangents[index] += in_bitangents[i]; 
}else{ // Sinon, on doit l'ajouter dans les données de sortie. 
    // Faire comme d'habitude
    [...] 
}

Notez que l'on ne normalise rien ici. En réalité c'est pratique, car de cette façon, les petits triangles, qui ont une tangente et bitangente plus petites, auront un effet diminué sur le vecteur final par rapport aux grands triangles (qui contribueront plus à la forme finale).

V. Le shader

V-A. Tampons et variables uniformes supplémentaires

On a besoin de deux nouveaux tampons, un pour les tangentes, l'autre pour les bitangentes :

 
Sélectionnez
GLuint tangentbuffer; 
glGenBuffers(1, &tangentbuffer); 
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer); 
glBufferData(GL_ARRAY_BUFFER, indexed_tangents.size() * sizeof(glm::vec3), &indexed_tangents[0], GL_STATIC_DRAW); 
 
GLuint bitangentbuffer; 
glGenBuffers(1, &bitangentbuffer); 
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer); 
glBufferData(GL_ARRAY_BUFFER, indexed_bitangents.size() * sizeof(glm::vec3), &indexed_bitangents[0], GL_STATIC_DRAW);

On a aussi besoin d'une nouvelle variable uniforme pour notre nouvelle texture de normales :

 
Sélectionnez
[...] 
GLuint NormalTexture = loadTGA_glfw("normal.tga"); 
[...] 
GLuint NormalTextureID  = glGetUniformLocation(programID, "NormalTextureSampler");

Et d'une autre pour la matrice 3x3 de modèle-vue. Cela n'est pas strictement obligatoire, mais c'est plus simple ; plus d'informations là-dessus plus tard. On a besoin que de la partie 3 x 3 supérieure gauche car on va multiplier des directions : on peut donc se débarrasser de la partie liée à la translation.

 
Sélectionnez
GLuint ModelView3x3MatrixID = glGetUniformLocation(programID, "MV3x3");

Voici ce qu'est devenu le code d'affichage :

 
Sélectionnez
// Nettoie l'écran
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
 
// Utilise le shader
glUseProgram(programID); 
 
// Calcule la matrice MVP à partir des entrées clavier et souris
computeMatricesFromInputs(); 
glm::mat4 ProjectionMatrix = getProjectionMatrix(); 
glm::mat4 ViewMatrix = getViewMatrix(); 
glm::mat4 ModelMatrix = glm::mat4(1.0); 
glm::mat4 ModelViewMatrix = ViewMatrix * ModelMatrix; 
glm::mat3 ModelView3x3Matrix = glm::mat3(ModelViewMatrix); // Take the upper-left part of ModelViewMatrix 
glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix; 
 
// Envoie les transformations au shader actuellement lié, 
// dans la variable uniforme "MVP"
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]); 
glUniformMatrix4fv(ModelMatrixID, 1, GL_FALSE, &ModelMatrix[0][0]); 
glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]); 
glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]); 
glUniformMatrix3fv(ModelView3x3MatrixID, 1, GL_FALSE, &ModelView3x3Matrix[0][0]); 
 
glm::vec3 lightPos = glm::vec3(0,0,4); 
glUniform3f(LightID, lightPos.x, lightPos.y, lightPos.z); 
 
// Lie notre texture diffuse à l'unité de texture 0
glActiveTexture(GL_TEXTURE0); 
glBindTexture(GL_TEXTURE_2D, DiffuseTexture); 
// Définit notre échantillonneur "DiffuseTextureSampler" à l'unité de texture 0 
glUniform1i(DiffuseTextureID, 0); 
 
// Lie notre texture de normales à l'unité de texture 1
glActiveTexture(GL_TEXTURE1); 
glBindTexture(GL_TEXTURE_2D, NormalTexture); 
// Définit notre échantilloneur "Normal    TextureSampler" à l'unité de texture 1
glUniform1i(NormalTextureID, 1); 
 
// 1er tampon d'attributs : sommets
glEnableVertexAttribArray(0); 
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); 
glVertexAttribPointer( 
    0,                  // attribut 
    3,                  // taille
    GL_FLOAT,           // type 
    GL_FALSE,           // normalisé ? 
    0,                  // nombre d'octets séparant deux sommets dans le tampon
    (void*)0            // décalage du tableau de tampon
); 
 
// 2d tampon d'attributs : UVs 
glEnableVertexAttribArray(1); 
glBindBuffer(GL_ARRAY_BUFFER, uvbuffer); 
glVertexAttribPointer( 
    1,                                // attribut
    2,                                // taille
    GL_FLOAT,                         // type 
    GL_FALSE,                         // normalisé ? 
    0,                                // nombre d'octets séparant deux sommets dans le tampon
    (void*)0                          // décalage du tableau de tampon
); 
 
// 3e tampon d'attributs : normales
glEnableVertexAttribArray(2); 
glBindBuffer(GL_ARRAY_BUFFER, normalbuffer); 
glVertexAttribPointer( 
    2,                                // attribut 
    3,                                // taille
    GL_FLOAT,                         // type 
    GL_FALSE,                         // normalisé ? 
    0,                                // nombre d'octets séparant deux sommets dans le tampon
    (void*)0                          // décalage du tableau de tampon
); 
 
// 4e tampon d'attributs : tangentes
glEnableVertexAttribArray(3); 
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer); 
glVertexAttribPointer( 
    3,                                // attribut 
    3,                                // taille
    GL_FLOAT,                         // type 
    GL_FALSE,                         // normalisé ? 
    0,                                // nombre d'octets séparant deux sommets dans le tampon
    (void*)0                          // décalage du tableau de tampon
); 
 
// 5e tampon d'attributs : bitangentes 
glEnableVertexAttribArray(4); 
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer); 
glVertexAttribPointer( 
    4,                                // attribut
    3,                                // taille
    GL_FLOAT,                         // type 
    GL_FALSE,                         // normalisé ? 
    0,                                // nombre d'octets séparant deux sommets dans le tampon
    (void*)0                          // décalage du tableau de tampon
); 
 
// Tampon d'indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer); 
 
// Affiche les triangles ! 
glDrawElements( 
    GL_TRIANGLES,      // mode 
    indices.size(),    // nombre 
    GL_UNSIGNED_INT,   // type 
    (void*)0           // décalage du tableau de tampon
); 
 
glDisableVertexAttribArray(0); 
glDisableVertexAttribArray(1); 
glDisableVertexAttribArray(2); 
glDisableVertexAttribArray(3); 
glDisableVertexAttribArray(4); 
  
// Échange les tampons
glfwSwapBuffers();

V-B. Vertex shader

Comme dit précédemment, on va tout faire dans l'espace de la caméra, car il est plus simple d'obtenir la position du fragment dans cet espace. C'est pourquoi on multiplie nos vecteurs T, B et N avec la matrice de modèle-vue.

 
Sélectionnez
vertexNormal_cameraspace = MV3x3 * normalize(vertexNormal_modelspace); 
vertexTangent_cameraspace = MV3x3 * normalize(vertexTangent_modelspace); 
vertexBitangent_cameraspace = MV3x3 * normalize(vertexBitangent_modelspace);

Ces trois vecteurs définissent la matrice TBN, qui est créée de cette façon :

 
Sélectionnez
mat3 TBN = transpose(mat3( 
        vertexTangent_cameraspace, 
        vertexBitangent_cameraspace, 
        vertexNormal_cameraspace 
    )); // Vous pouvez utiliser des produits scalaire au lieu de créer cette matrice et de la transposée. Voir les références.

La matrice passe de l'espace de la caméra à l'espace tangent (la même matrice, mais avec XXX_modelspace à la place, permettrai de passer de l'espace modèle à l'espace tangent). On peut l'utiliser pour calculer la direction de la lumière et la direction de l'œil, dans l'espace tangent :

 
Sélectionnez
LightDirection_tangentspace = TBN * LightDirection_cameraspace; 
EyeDirection_tangentspace =  TBN * EyeDirection_cameraspace;

V-C. Fragment shader

La normale, dans l'espace tangent, est immédiate à obtenir, c'est la texture :

 
Sélectionnez
// Normale locale, dans l'espace tangent
vec3 TextureNormal_tangentspace = normalize(texture2D( NormalTextureSampler, UV ).rgb*2.0 - 1.0);

Donc, on a tout ce dont nous avons besoin. La lumière diffuse utilise clamp(dot(n,l),0,1), avec n et l exprimé dans l'espace tangent (l'espace dans lequel vous effectuez vos produits scalaire et vectoriel n'importe pas ; la chose importante est que l et n soit tous les deux exprimés dans le même espace). La lumière spéculaire utilise clamp(dot(E,R),0, 1), où, encore une fois, E et R sont exprimés dans l'espace tangent. Super !

VI. Résultats

Voici le résultat obtenu. Vous pouvez remarquer que :

  • les briques sont bosselées car on a de nombreuses variations dans les normales ;
  • le ciment semble plat car la texture de normales est complètement bleue.
Application des textures de normales

VII. Aller plus loin

VII-A. Orthogonalisation

Dans le vertex shader on prend la transposée au lieu de l'inverse, car c'est plus rapide. Mais cela ne fonctionne que si l'espace représenté par la matrice est orthogonal, ce qui n'est pas encore le cas. Heureusement, c'est facilement corrigeable : on doit simplement faire que la tangente soit perpendiculaire à la normale à la fin de computeTangentBasis() :

 
Sélectionnez
t = glm::normalize(t - n * glm::dot(n, t));

La formule peut être difficile à saisir, donc voici un petit schéma pour aider :

Rendre perpendiculaire deux vecteurs

n et t sont presque perpendiculaire, donc on « pousse » t dans la direction de -n multiplié par dot(n,t).

Voici une petite application qui explique aussi cela (utilisez seulement deux vecteurs).

VII-B. Règle de la main droite

Vous n'avez normalement pas à vous en inquiéter, mais dans quelques cas, lorsque vous utilisez des modèles symétriques, les coordonnées UV sont orientées dans le mauvais sens et votre T possède la mauvaise orientation.

Pour vérifier si elle doit être inversée ou pas, c'est simple : TBN doit former un système de coordonnées répondant à la règle de la main droite. Par exemple, cross(n,t) doit avoir la même orientation que b.

En mathématiques, « Un vecteur A à la même orientation qu'un vecteur B » se traduit par dot(A,B) > 0, donc on doit vérifier si dot(cross(n,t),b) > 0.

Si c'est incorrect, inversez t :

 
Sélectionnez
if (glm::dot(glm::cross(n, t), b) < 0.0f){ 
    t = t * -1.0f; 
}

Cela est aussi effectué pour chaque sommet à la fin de la fonction computeTangentBasis().

VII-C. Texture spéculaire

Pour le fun, j'ai ajouté une texture spéculaire au code. Elle ressemble à cela :

Image non disponible

et je l'ai utilisée pour remplacer le simple gris « vec3(0.3,0.3,0.3) » que l'on utilise pour la couleur spéculaire :

Le ciment est toujours noir : la texture indique qu'il n'y a pas de composante spéculaire.

VII-D. Débogage avec le mode immédiat

L'objectif réel de ce site Web est que vous N'utilisiez PAS le mode immédiat, qui est obsolète, lent et problématique en de nombreux aspects.

Par contre, il arrive qu'il soit très pratique pour le débogage :

Le mode immédiat pour le débogage

Ici, on observe notre espace tangent avec les lignes dessinées dans le mode immédiat.

Pour cela, vous devez abandonner le profil core 3.3 :

 
Sélectionnez
glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

Puis passer les matrices au vieux pipeline OpenGL (vous pouvez aussi écrire un autre shader, mais c'est plus simple de cette façon et vous ête en train de bidouiller de toute façon) :

 
Sélectionnez
glMatrixMode(GL_PROJECTION); 
glLoadMatrixf((const GLfloat*)&ProjectionMatrix[0]); 
glMatrixMode(GL_MODELVIEW); 
glm::mat4 MV = ViewMatrix * ModelMatrix; 
glLoadMatrixf((const GLfloat*)&MV[0]);

Désactiver les shaders :

 
Sélectionnez
glUseProgram(0);

Et dessiner vos lignes (dans ce cas, les normales, normalisées et multipliées par 0.1 et appliquées au bon sommet) :

 
Sélectionnez
glColor3f(0,0,1); 
glBegin(GL_LINES); 
for (int i=0; i<indices.size(); i++){ 
    glm::vec3 p = indexed_vertices[indices[i]]; 
    glVertex3fv(&p.x); 
    glm::vec3 o = glm::normalize(indexed_normals[indices[i]]); 
    p+=o*0.1f; 
    glVertex3fv(&p.x); 
} 
glEnd();

Rappelez-vous : n'utilisez pas le mode immédiat dans une vraie application ! Uniquement pour du débogage ! Et n'oubliez pas de réactiver le profil core après coup, cela vous assurera de ne pas faire de telles choses.

VII-E. Débogage avec les couleurs

Lors du débogage, il peut être utile de visualiser la valeur d'un vecteur. La façon la plus simple pour ce faire est d'écrire sa valeur dans le tampon d'image au lieu de la couleur actuelle. Par exemple, pour visualiser LightDiretion_tangentspace :

 
Sélectionnez
color.xyz = LightDirection_tangentspace;
Débogage en affichant les vecteurs

Cela signifie que :

  • sur la partie droite du cylindre, la lumière (représentée par la fine ligne blanche) est vers le HAUT (dans l'espace tangent). En d'autres mots, la lumière est dans la direction de la normale des triangles ;
  • sur le milieu du cylindre, la lumière est dans la direction de la tangente (avant+X).

Quelques conseils :

  • suivant ce que vous essayez de voir, vous pouvez souhaiter le normaliser ;
  • si vous ne trouvez pas de signification à ce que vous voyez, visualisez toutes les composantes séparément en forçant par exemple le vert et le bleu à 0 ;
  • évitez de jouer avec l'alpha, c'est trop compliqué Image non disponible ;
  • si vous souhaitez visualiser une valeur négative, vous pouvez utiliser la même astuce que celle pour notre texture de normales : visualisez (v+1.0)/2.0 à la place. Le noir signifie -1 et la couleur +1. Toutefois, c'est difficile d'interpréter le rendu.

VII-F. Débogage avec les noms des variables

Comme dit précédemment, il est important de connaître exactement dans quel espace vos vecteurs se trouvent. Ne faites pas le produit scalaire d'un vecteur situé dans l'espace de la caméra avec un vecteur dans l'espace modèle.

L'ajout de l'espace de chaque sommet à son nom (« ..._modelspace ») aide à la correction de terribles bogues mathématiques.

VII-G. Comment créer une texture de normales

Créé par James O'Hare. Cliquez pour agrandir :

Tutoriel de création d'une texture de normales

VIII. Exercices

  • Normalisez les vecteurs dans indexVBO_TBN avant l'addition et voyez ce que cela fait.
  • Visualisez les autres vecteurs (par exemple, EyeDirection_tangentspace) dans le mode de débogage avec les couleurs et essayez de donner du sens à ce que vous voyez.

IX. Outils et liens

X. Références

XI. Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.

Navigation

Tutoriel précédent : extensions OpenGL

 

Sommaire

 

Tutoriel suivant : rendu dans une texture

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2014 opengl-tutorial.org. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.