HDR, loi de Beer et aberration chromatique

C'est la cinquième partie de notre série d'articles sur le raytracing en C++. Elle fait suite à la partie intitulée "Flou, Fresnel et Blobs".

Format d'image HDR

J'ai fait quelques modifications au programme afin de supporter le format d'image HDR. Ce format est utilisé en synthèse d'image pour combler certaines lacunes des formats d'images usuelles qui sont plutôt destinés à la représentation sur un écran sans transformation (sRGB encodé sur 8 bits notamment).

Lorsque l'on prend une photo et qu'on la convertit au format JPEG on perd généralement une grande partie des informations véhiculées par la lumière. Les grandes variations d'intensités qui ont lieu dans la réalité sont aplanies pour ne plus avoir qu'un rapport maximal d'intensité de 255:1 (format 8 bit linéaire). L'intensité incidente sur notre caméra n'est pas limitée en résolution ou en détails dans les hautes et basses intensité. C'est notre mécanisme de capture qui a cet effet limitant.

Cela fait quelques années que l'on sait capturer des images qui véhiculent plus d'infos que ce qui est perçu par notre oeil ou notre caméra. Le plus souvent c'est issu d'un retraitement informatique de données capturées avec un instrument normal. Quel est l'intéret si l'oeil n'est pas capable de percevoir plus ou si le moniteur ne peut pas afficher plus que ce qui est stockable dans un Jpeg usuel ? L'intéret c'est que l'on fait de la synthèse. C'est à dire qu'une image intermédiaire va servir à calculer le reflet sur une surface ou sa lumière va être altérée par un matériel semi transparent. Ces transformations font que les assomptions que l'on a fait sur la précision et le domaine d'intensité représentable par le moniteur auront changé et que le nouveau modèle demandere plus de précision dans les basses intensités ou demandera de pouvoir faire la différence entre un point blanc parce qu'il est éclairé indirectement par la lumière ou blanc parce qu'il est directement source de lumière. Le problème est le même en retouche photo. Si le photographe a mal réglé son appareil et que l'image resultante est trop sombre ou trop claire. S'il change l'intensité sur l'image jpeg, il pourra éventuellement booster la partie avec les détails visibles mais le résultat sera bruité, gris ou avec des artefacts liés à la compression ou à la qualité du capteur utilisé.

Cette page décrit l'utilisation d'images HDR dans le traitement des films : http://www.openexr.com/about.html.

Le programme de raytracing n'utilise les images au format HDR que pour l'environment cube map. Cela n'est possible que parce que l'on fait déjà tous nos calculs en 32 bits flottant par canal. Le rendu temps réel par exemple prend souvent le parti de faire tous les calculs en 8 bits par canal avec une précision fixe, pour des raisons de complexité de calcul et de capacités de stockages peu adaptées au temps réel. On n'a pas ce genre de limitation donc on peut choisir arbitrairement la précision nécessaire. Le format .hdr est une forme un peu plus compressée que notre représentation de couleurs. Chaque pixel n'a qu'un seul exposant partagé pour les trois composants rouge vert et bleu. Ce format de pixel est appelé RGBE pour rouge vert bleu et exposant.

Le code qui permet de lire les images HDR est adapté du code de Bruce Walter : http://www.graphics.cornell.edu/online/formats/rgbe/

Les images HDR utilisées pour générer les cubemaps sont tirées de la page web de Paul Debevec : http://www.debevec.org/Probes/. Et elles restent sa propriété.

J'ai utilisé le probe de la "Basilique St Pierre" comme environment map dans cette image :

Théoriquement il est possible d'utiliser ces probes pour autre chose que de simples environment maps. Il est ainsi possible d'utiliser ces images comme source principale d'éclairage dans notre modèle diffus ou spéculaire. J'aborderai cela plus tard si j'ai le temps.

La loi de Beer

Jusqu'à présent nous avons négligé l'absorption de la lumière par le milieu transparent. Aucun milieu n'est parfaitement transparent (à part peut-être le vide). Et en particulier le verre, l'eau, le plastique et tout matériel qui transmet la lumière en absorbe aussi un peu pendant son trajet. En fait cette décimation est dépendante de la distance parcourue, à chaque micromètre parcouru un peu de l'intensité initiale est absorbée par le matériau.

Une modélisation existe pour les milieux transparents à faible concentration, cette modélisation a abouti à la loi de Beer. En gros si I0 désigne l'intensité initiale, c la concentration "molaire" en produit colorant (pour un liquide), alors l'intensité après avoir parcouru la distance l sera de :

epsilon est une propriété d'absorption du produit dissous. Elle est indépendante de la concentration en ce produit (si la concentration elle-meme est suffisamment faible). Bien entendu on n'a pas besoin de connaitre epsilon ou c dans notre modèle simplifié. Notre milieu peut être caractérisé par un seul paramètre que j'appelle absorption :

Le code à ajouter est très simple, l'absorption modifie le coéficient de transmission de la lumière, et la distance utilisée dans la formule est la distance parcourue par le rayon entre deux intersections de géométrie (les unités sont arbitraires).

if (myContext.absorption != 1.0f)
{
    // la lumière transmise dans le médium transparent
    // est affectée par la diffusion due aux impuretés
    // la beer's law s'applique pour les faibles concentrations
    coef = coef * powf(myContext.absorption, t);
}

L'absorption est déterminée en amont, lors de l'intersection précédente suivant que l'on entre ou que l'on sort d'un objet. Par exemple lorsque le rayon est réfracté:

if (bInside)
{
    myContext.fRefractionCoef = context::getDefaultAir().fRefractionCoef;
}
else
{
    myContext.fRefractionCoef = currentMatChannel.density;
}

L'image suivante illustre le principe de Beer, avec trois sphères "colorées". Chaque sphère est transparente et laisse passer l'image de la basilique mais en supprimant certaines longueurs d'ondes au passage, le résultat est une image à dominante verte, bleue ou rouge suivant la nature des impuretés :

Dans le modèle décrit, cette absorption est représentée comme une concentration molaire d'impuretés. Mais l'absorption peut également avoir lieu dans un milieu pur. L'eau absorbe également certaines longueur d'onde par vibration de ses molécules, les vibrations sont plus fortes dans les longueurs d'ondes élevées laissant donc passer plus de lumière bleue au final. La lumière traversant l'eau est donc colorée en bleu et tout objet plongé dans l'eau est soumis à une lumière qui semble de plus en plue bleutée suivant la quantité de liquide à traverser.

Cela n'est pas la seule façon de colorer un matériel transparent. Outre l'absorption qui est décrite par cette loi de Beer, il y a également la dispersion qui est à l'origine du bleu du ciel par exemple. La différence entre l'absorption et la dispersion étant que dans le cas de l'absorption le milieu n'a pas de couleur en lui même mais teinte les objets qui sont observés à travers lui et dans le cas de la dispersion le milieu a une couleur propre qui s'ajoute à celle de la couleur de fond (noir dans le cas du ciel). Je ne traite pas la dispersion ici, mais ça pourrait faire l'objet d'un article sur la modélisation de l'atmosphère terrestre par exemple.

Aberration chromatique

Un autre aspect de l'optique qui peut être observé dans la vie courante est l'existence de propriétés différente des matériaux suivant la longueur d'onde. Cela parait évident puisque cela est à l'origine des couleurs des objets sous une lumière blanche. Mais cela peut prendre des formes plus complexes comme des coefficients de réfraction différents suivant la longueur d'onde ou des interférences (liées à la nature ondulatoire de la lumière). On va s'intéresser aux coéfficients de réfraction.

Si l'on attache à chaque canal (rouge vert bleu) une densité différente, on va obtenir des trajets de lumière différents puisque la loi géométrique qui gouverne la réfraction est dépendante de la densité et que la lumière blanche est constituée d'une réunion de photons avec une longueur d'onde donnée. Le résultat c'est que toute lumière passant à travers un tel milieu verra son rayon se décomposer en trois sous-rayons (un par canal). Le milieu transparent jouant le rôle de prisme.

Cela nécessite bien entendu de faire quelques modifications de notre code afin de permettre au rayon de suivre des chemins différents en fonction de sa longueur d'onde. Tous les calculs de rayon seront dupliqués par canal, et les tirages de roulette russe auront une granularité par canal également. Pour cela j'ai découpé la structure material en un tableau de trois sous structures materialChannel.

struct materialChannel {
    float diffuse;
    //Deuxième couleur diffuse optionnelle pour les matériaux procéduraux
    float diffuse2;
    float reflection, refraction, density, absorption;
    float specular;
    float power;
};

struct material {
    enum {
        gouraud=0,
        noise=1,
        marble=2,
        turbulence=3
    } type;

    float bump;

    materialChannel tab[COLOROFFSET_MAX];

    materialChannel & getChannel(COLOROFFSET offset) { return tab[int(offset)]; }
    const materialChannel & getChannel(COLOROFFSET offset) const { return tab[int(offset)]; }
};

Voici le nouveau prototype de la fonction addRay qui travaille désormais par canal et renvoie un simple nombre flottant. Le reste du code est terriblement similaire.

float addRay(ray viewRay, scene &myScene, context myContext)

Voici les nombres du verre "Flint" (Silex) caractérisé par une forte aberration chromatique : rouge = 1.596, vert = 1.640, bleu = 1.680. Ce sont les indices de réfraction qui diffèrent mais on peut aussi jouer sur l'albedo la reflexion ou même le type de texture différent par canal.


Annexe I : un meilleur AA

J'en profite également pour introduire ce que j'appelerai un meilleur antialiasing. En fait ça dépend d'avis subjectifs et d'autres plus ou moins objectifs. L'un des problèmes que ce nouvel antialiasing essaie de régler est la présence de "directions privilégiées" dans notre représentation écran. Notre écran est agencé en grille ordonnée de pixels carrés ce qui fait que tout véhiculage d'informations passe par le filtre déformant de cette grille, si je veux faire une diagonale, je vais faire un dégradé sur une colonne puis je vais passer à la colonne suivante et continuer mon dégradé etc.. Pour un objet blanc sur fond noir, j'aurais en moyenne un gradient de 50% entre deux pixels adjacents et au pire un gradient de 100%. A résolution basse les colonnes qui constituent ma diagonale seront très visibles. Le mouvement est un autre problème. Si j'animais mon rendu et que je faisais déplacer mon objet à une vitesse fixe sur mon écran, l'oeil aurait tendance à capturer facilement les variations brusques d'une configuration de pixels à une autre, si ces variations entraient en résonance (par la bonne configuration de géométrie et de déplacement sur l'écran) on aurait un effet de vagues assez visibles. On ne peut pas totalement éliminer ce risque à part peut-etre avec une résolution infinie et dans ce cas la forme des pixels ne nous intéresse plus. Par contre on peut essayer de le réduire.

La nouvelle méthode d'antialiasing consiste à imaginer qu'on intégre un ensemble de canevas sur la représentation de notre écran, chacun de ces canevas représente elle même une grille de pixels indépendants de notre représentation écran (mais partageant la même résolution et orientation). Un pixel "virtuel" qui intersecte un pixel de notre représentation contribuera à hauteur de l'aire de l'intersection à la valeur finale du pixel. En pratique on prend toujours un certain nombre d'échantillons pour chaque pixel mais ces échantillons sont physiquement en dehors du pixel et n'ont pas tous le même poids. Les échantillons du centre sont plus importants que ceux sur les bords. Pas de miracles dans les très basses résolution on voit toujours les pixels carrés, mais à une résolution classique, les artefacts sur les bords deviennent quasiment indiscernables comme vous pouvez le constater sur l'image suivante.

Les positions des échantillons par pixel et leur poids relatif est entré dans un tableau statique, ce qui permet de garder un code similaire pour les différentes formes d'antialiasing (tant qu'elles reposent sur le même principe).

Résumé des épisodes :

un blob éclairé par une cubemap HDR, avec du bumpmapping, réflexion/réfraction, aberration chromatique et absorption de lumière par le liquide. Le tout antialiasé pour un rendu "smooth".

Téléchargement du code source ici : raytrace_page5.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.

Pour télecharger les fichiers textures (y compris la cubemap au format HDR de la basilique St Pierre) c'est ici : textures.zip (3.16 Mo)


Direction page 6 : "Photon mapping".


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