Matériau speculaire et post processing

Comme on a vu dans la première partie, un matériau diffus renvoie la les rayons de lumière incidents de manière uniforme dans toutes les directions. A l'opposé les miroirs ne renvoient que dans la direction oposée à la direction du rayon incident.

La réalité n'est pas aussi rigide que ça et les surfaces d'objets sont souvent complexes et difficiles à modéliser simplement. Je vais décrire deux modèles empiriques de représentation de surfaces "brillantes" et qui ne soient pas de purs miroirs.

C'est la deuxième partie de notre série d'articles sur le raytracing en C++. Elle fait suite à la partie intitulée "Premiers pas".

Phong

Bui Tuong Phong était étudiant à l'université de l'Utah quand il a inventé le procédé qui a pris son nom (éclairage Phong). Il permet de rendre des objets qui refletent la lumière dans une direction privilégiée, dans ce cas la direction privilégiée est celle du vecteur lumière réfléchi par le vecteur normal à la surface.

On parle de terme spéculaire, contrairement au terme de diffusion de Lambert, les termes spéculaires varient en fonction de la position de l'observateur (ce qui se voit dans l'équation qui fait intervenir viewRay.dir).

float reflet = 2.0f * (lightRay.dir * n);
vecteur phongDir = lightRay.dir - reflet * n;
float phongTerm = _MAX(phongDir * viewRay.dir, 0.0f) ;
phongTerm = currentMat.specvalue * powf(phongTerm, currentMat.specpower) * coef;
red += phongTerm * current.red;
green += phongTerm * current.green;
blue += phongTerm * current.blue;

Voici le résultat en image de l'image de base avec éclairage de Phong en prime:

Digression: Une anecdote amusante à propos de l'éclairage Phong et que dans la plupart des papiers que Bui Tuong Phong a écrit, son nom est écrit dans cet ordre là, qui est l'ordre traditionnel au Vietnam. (et c'est parfois aussi le cas en France où l'on met le patronyme en tête avant le prénom). Mais pour les américains il n'y a pas d'ambiguité, le first name (prénom) est toujours en premier et le last name (patronyme) en dernier. La conclusion est donc que tous les gens qui ont lu les papiers qui présentaient le modèle d'éclairage ont assumé que le prénom de l'étudiant était Bui Tuong et son nom de famille Phong. Or c'était évidemment l'inverse. L'éclairage de Phong aurait donc du en toute logique s'appeler l'éclairage de Bui Tuong.

Blinn-Phong

Une variation possible sur l'éclairage spéculaire de Phong est celle apportée par le professeur de Bui Tuong Phong à l'époque où celui-ci était à Salt Lake City. C'est Jim Blinn, qui est connu pour une bonne partie des avancées dans le domaine du graphisme de l'époque. Il est d'ailleurs toujours en vie et travaille pour Microsoft Research.

Blinn a rajouté des considérations physiques au modèle empirique de départ. Cela passe par le calcul d'un vecteur intermédiaire h qui est à mi chemin entre la direction de la lumière et celle de l'observateur. Puis le calcul consiste à calculer le produit scalaire de ce vecteur de Blinn avec la normale à la surface. Un petit calcul suffit à se rendre compte que l'éclairage de Blinn est pareillement maximal lorsque le rayon vue est dans la direction réfléchie de la direction de la source de lumière. C'est donc bien un éclairage spéculaire.

vecteur blinnDir = lightRay.dir - viewRay.dir;
float temp = sqrtf( blinnDir * blinnDir);
if (temp != 0.0f )
{
blinnDir = (1.0f / temp) * blinnDir;
float blinnTerm = _MAX(blinnDir * n, 0.0f);
blinnTerm = currentMat.specvalue * powf(blinnTerm , currentMat.specpower) * coef;

red += blinnTerm * current.red ;
green += blinnTerm * current.green ;
blue += blinnTerm * current.blue;
}

Le résultat est assez proche comme vous pouvez le voir sur cette image:

Antialiasing

Il y a de nombreuses méthodes d'antialiasing. Mais la plus simple (en terme d'instructions) et la seule que je vais appliquer ici s'appelle le supersampling (suréchantillonnage).

Le suréchantillonnage consiste pour une image à résolution x,y à faire un rendu dans une image à résolution 2*x, 2*y (pour du 4x-supersampling) puis à calculer chaque pixel de l'image finale comme la moyenne de quatre pixels adjacents de la grande image. Quand on parle de moyenne ce n'est pas forcément strictement parlé une moyenne arithmétique (Cela peut faire intervenir la transformation gamma, j'y reviendrai.) et cela peut couvrir en réalité plus de quatre pixels. Même si pour des raisons de conservations d'énergie il ne faut pas qu'un pixel soit calculé à partir de plus ou moins de l'équivalent de quatre pixels (en ajoutant les poids du filtre) sinon il y a bien évidemment amplification ou réduction du signal.

Le code suivant détermine la couleur de chaque pixel en ajoutant la valeur de quatre sous pixels diminué au quart. La position relative de ces quatre sous pixels est libre. Pour des raisons de simplicité on choisit de les ordonner en grille régulière (comme si on avait vraiment calculé une image quatre fois plus grande). On aurait pu les placer de manière à former une grille tournée à 20 degré afin d'améliorer le lissage géométrique. Ou encore de faire varier de manière aléatoire (ou semi régulière) les sous échantillons par pixel afin de réduire les phénomènes de moiré. Je m'atterderai plus tard sur l'antialiasing et ses implications.

for (int y = 0; y < myScene.sizey; ++y) 
for (int x = 0; x < myScene.sizex; ++x)
{
float red = 0, green = 0, blue = 0;
for(float fragmentx = x; fragmentx < x + 1.0f; fragmentx += 0.5f)
for(float fragmenty = y; fragmenty < y + 1.0f; fragmenty += 0.5f)
{
// la contribution de chaque rayon est diminuée au quart.
float coef = 0.25f;
// on continue les traitements comme si de rien n'était

}
// puis on écrit la contribution globale de tous les fragments de pixels dans le frame buffer

}

Voici le résultat après Zoom de l'application d'un supersampling 4x:

Fonction gamma

La correction gamma est à l'origine un moyen de corriger la mauvaise restituation des moniteurs type CRTs. La restitution fait que pour une variation de variation linéaire en entrée l'intensité perçue sur l'écran varie selon une fonction Pow(I, gamma). Avec généralement gamma plus grand que 1.

Afin de pouvoir restituer correctement des dégradés réguliers et corriger les problèmes liés au dithering et à l'antialiasing (j'expliquerai plus en détail plus tard). Il est nécessaire d'appliquer la fonction inverse au signal Pow(I, 1/gamma) avant d'envoyer le signal au moniteur. Cette correction gamma peut-être faite n'importe où, dans le convertisseur analogique numérique de la carte graphique, dans le logiciel d'affichage des images, ou même directement à la source lors de l'écriture de l'image. C'est cette dernière manière qui m'intéresse.

Fournir un format qui soit calibré pour toutes les configs (moniteurs, logiciels d'affichage, cartes graphiques) existantes est la quadrature du cercle. Cela n'existe pas. Pourtant il y a bien un standard qui tente d'adapter le format à l'imperfection du système. C'est le format sRGB. Dans ce format les images ne sont plus représenté dans l'espace des intensités linéaires mais dans l'espace sRGB, où les intensités sont mises à la puissance de 1/2.2 avant d'être converties en 8 bits par canal et écrites dans le fichier. L'idée principale est évidemment de faire la transformation avant de réduire la précision à 8 bits par canal. Faire la transformation après n'a aucun sens, on va le voir tout de suite.

Voici: une image au format sRGB, si votre navigateur/bureau/carte graphique/écran collaborent bien vous devriez voir une différence minime entre les parties grises et les parties en damier noir et blanc.

Quel est l'intérêt de conserver ses images au format sRGB ? Économiser l'application du gamma en bout de chaine n'a pas forcément de sens puisque comme on l'a vu le sRGB n'est pas adapté à toutes les configs et conditions de lumières ce qui fait qu'il est souvent nécessaire d'appliquer une correction résiduelle même sur les images sRGB. Non l'intérêt est simplement d'obtenir une qualité de stockage légèrement supérieure. 8 bits par canaux est tout juste suffisant pour que l'oeil humain ne perçoive presque plus le banding (présence de bandes dans les dégradés de couleurs). Et l'oeil est d'autant plus sensible au différences d'intensité que l'intensité est basse. Pour limiter le banding il peut donc être intéressant de booster les basses intensités avant le stockage et de les restituer normalement à l'affichage (la chaine gamma finit par devenir donc très compliquée).

On peut écrire directement au format sRGB dans nos fichiers TGAs. Bien entendu du JPEG ou du PNG seraient plus adaptés vu qu'il y a la possibilité de préciser l'espace de stockage des images dans chacun de ces formats afin que le logiciel qui les affichera sache quel niveau de correction appliquer aux données ensuite (pour éviter qu'elles soient trop claires ou trop sombres). Le code dans le programme de raytracing est super simple. Juste avant d'écrire le pixel on fait :

 float invgamma = 0.45; // invgamma est égale à la valeur retenue par le standard sRGB
blue = powf(blue, invgamma );
red = powf(red, invgamma );
green = powf(green, invgamma );
imageFile.put(min(blue*255.0f,255.0f)).put(min(green*255.0f, 255.0f)).put(min(red*255.0f, 255.0f));

Voilà, on transforme selon invgamma AVANT de faire la conversion en entiers de 0 à 255. Car le nombre avant conversion est un flottant qui a une précision très large ce qui permet de s'assurer que les 8 bits de précision seront bien utilisés. Si la correction avait lieu après on aurait une perte effective de précision (à la fois dans les basses intensités et dans les hautes intensité !).

Exposition photo

J'ai évoqué plus tôt le fait que l'utilisation de la fonction min dans la conversion de mes nombres flottants en entier était une approche "naïve". On appelle cet opérateur min un opérateur de saturation. Qu'est-ce que la saturation ? Ce terme est utilisé partout en électronique, en musique, en photographie etc.. Cela signifie qu'un signal qui variait sur un large intervalle est ramené à un plus petit intervalle en ramenant toutes les valeurs trop grandes à la valeur max du nouvel intervalle. Ainsi avec cette approche les valeurs 1 et 2 seront toutes les deux perçus comme 1.

Cela marche bien si toutes les valeurs intéressantes sont inférieures à 1. Mais dans la réalité et dans les images de synthèses ce n'est pas le cas. On veut autant de détails présents dans les hautes intensités que de détails présents dans les basses intensités.

Une solution acceptable viendra de la photographie. En photographie argentique on utilise un matériel ou des composents photosensibles migrent (de photo sensible à inerte) au fur et à mesure qu'ils sont "excités" par les photons. Ils ne migrent pas tous d'un coup et la réaction n'est pas linéaire en nombre de photons reçus. Non, ils migrent comme dans une désintégration nucléaire : par demi-vie. En effet la probabilité qu'un photon touche un composant non migré diminue avec le temps d'exposition car de plus en plus de composants ont migré. Le négatif noircit donc par une fonction exponentielle de l'intensité de la lumière reçue et du temps d'exposition. Comme il s'agit du négatif la fonction d'intensité perçue est donc 1 - exp(lambda * I * durée).

Voici le code correspondant :

 float exposure = - 0.66f;
blue = 1.0f - expf(blue * exposure);
red = 1.0f - expf(red * exposure);
green = 1.0f - expf(green * exposure);

Bien entendu pas besoin d'être aussi précis dans un logiciel qui ne prend pas de vraies photos : exposure est un terme simplifié censé être une fonction du lambda et du temps d'exposition présumé.

La première image utilise l'opérateur de saturation et est soumis à une lumière forte. La deuxième image utilise l'opérateur d'exposition et est soumis à la même lumière.

Le calcul d'exposition a bien entendu lieu avant le calcul du gamma. Définir exposure automatiquement n'est pas simple et devrait être laissé en partie à l'appréciation de l'artiste suivant l'effet désiré. On peut aussi rajouter des effets de blooming par dessus. Le blooming est du à un défaut des capteurs/pellicules photosensibles dans les appareil photographiques. On peut par exemple modéliser ça comme la création d'un catalyseur de la réaction de migration ou comme des fuites de courant d'une cellule surexcitée vers ses voisines. Le résultat est que les points hautement lumineux semblent se diffuser aux alentours et forment des halos ou des taches lumineuses qui empiêtent sur les zones les plus sombres. Les halos peuvent egalement être créés par les conditions atmosphérique (brume, poussière, réfraction) et par des imperfections du système optique (lentilles de caméras, rayures sur la pupille de l'oeil, etc.)

Téléchargement du code source ici : raytrace_page2.zip Vous n'avez pas besoin de librairie particulière pour le compiler et le faire tourner, seulement un compilateur C++ livré avec la standard C++ library. Testé avec le compilateur de GCC 3.2 et visual C++ .net 2003.

Voici le résultat du programme avec les notions abordées dans cette page :

Direction page 3 : "Textures".


Quick Navigation :

page 1 : "Premiers pas".
page 2 : "Matériaux spéculaires et post processing".
page 3 : "Textures".
page 4 : "Flou Fresnel et Blobs".
page 5 : "HDR loi de beer et aberration chromatique".
page 6 : "Photon mapping".

Plus d'articles et commentaires : Retour au journal.

Copyright © Grégory Massal 1976-2005

Partner websites : LEGREG | GRAPHICS | GRAPHISME | PHOTOGRAPHY | OUT OF MY MIND | ANIMATION MENTOR | GREEN LIVING | VOXEL | RAY TRACING