l'envoi de photons
Pour envoyer des photons on part de chaque source lumineuse et l'on envoie dans toutes les directions possibles pour cette source en particulier.
C'est à dire, on se dit au départ que l'on va envoyer x photons. On veut avoir une intensité constante pour chaque photon pour éviter les problèmes plus tard donc on va pour chaque photon (à intensité fixe) déterminer de quelle source il va partir et dans quelle direction il va aller. Cela revient à faire la somme des intensités des lumières de la scène, tirer un nombre aléatoire rapporté à cette somme et choisir la lumìère de départ en fonction de ce nombre.
Ex: 2 lumières d'intensité 60 et 20.
La somme est égale a 80. On tire un nombre entre 0 et 80 et si ce nombre est entre 0 et 60 on choisit la première lumière et si le nombre est entre 60 et 80 on choisit la seconde. Aussi simple que ça. C'est toujours le même principe de roulette russe que l'on a appliqué jusque là.
Le cas de la source ponctuelle
Pour l'instant on se limite aux sources ponctuelles. Donc une fois que l'on a choisi la source lumineuse d'où partira le photon, il faut déterminer dans quelle direction il faut le faire partir.
Joie, la probabilité est identique dans toutes les directions. Par contre pas de bol, il faut raisonner équiprobabilité en terme d'angles sur la sphère.
La solution c'est de rapporter notre tirage aléatoire à la sphère unité en utilisant un truc.
Le premier truc c'est de tirer un vecteur de trois nombres aléatoires indépendants compris dans un volume cubique comprenant la sphère unité. Tirer ce vecteur est facile, chaque coordonnée est un nombre aléatoire entre [-1,1].
Ensuite on détermine si ce vecteur est inclu dans la sphère unité. c'est à dire que sa norme au carré est inférieure à 1.
Si il ne l'est pas, ce n'est pas grave, on jette ce nombre et on en prend un autre jusqu'à ce que ça marche.
Bien évidemment avant d'en faire la direction de notre rayon, on normalise derrière. On a ainsi la garantie d'un tirage équiprobable (isotrope) en terme d'angle sur la sphère unité.
Voici le code correspondant :
Code :
- ray viewRay;
- do
- {
- viewRay.dir.x = (2.0f / RAND_MAX) * rand() - 1.0f;
- viewRay.dir.y = (2.0f / RAND_MAX) * rand() - 1.0f;
- viewRay.dir.z = (2.0f / RAND_MAX) * rand() - 1.0f;
- }
- while (viewRay.dir.x *viewRay.dir.x + viewRay.dir.y * viewRay.dir.y + viewRay.dir.z * viewRay.dir.z > 1.0f );
- float temp = viewRay.dir * viewRay.dir;
- if (temp == 0.0f)
- continue;
- viewRay.dir = invsqrtf(temp) * viewRay.dir;
- viewRay.start = myScene.lgtTab[lightNumber].pos;
- if (!addPhotonRay(viewRay, myScene, myContext, channelPhotonMap[offset], coef))
- {
- break;
- };
|
La représentation d'un photon
Voici la struct représentant un photon en mémoire :
Code :
- struct photon
- {
- point pos;
- vecteur dir ;
- float wattage;
- // DWORD flag;
- };
|
La position c'est l'endroit où le rayon lumineux a rencontré un objet qui intéragit avec la lumière de manière non limité à un simple transfert (non limité à une simple réflexion ou réfraction).
La direction c'est le vecteur d'incidence du photon sur cet objet. Le wattage est l'intensité du photon juste avant d'arriver à ce point. Ce point est important puisque c'est ce qui permet de garantir l'indépendance des calculs d'illumination.
Un wattage bas va en fait avoir un effet négatif sur l'intensité sur une surface donnée. Dans l'idéal on voudrait que l'intensité incidente dépende uniquement du nombre de photons arrivés par unité de surface.
Le flag optionnel c'est pour plus tard, au cas où l'on veut savoir si un photon est actuellement passé par une réflexion, réfraction, s'il est marqué comme "photon d'ombre" (une aide au calcul d'ombre et de pénombre) etc...
Le cache à photons
Les photons quand ils arrivent sont stockés dans un container spécialisé.
J'appelle ça un cache à photon. Qui comprend trois composants:
- le container à photon lui-même, un simple vector<photon>.
- Le container à normales qui permet de ne pas rapprocher des photons qui seraient arrivés sur des surfaces perpendiculaires (exemple sur un mur à angle droit, les photons qui heurtent le premier mur ne doivent pas être comptés comme ceux qui heurtent le deuxieme mur). C'est de même un simple vector<vecteur>.
- Enfin, un arbre. Cet arbre est issu de l'équilibrage sur place du container à photons initial. C'est la structure qu'on appelle un KD-Tree. C'est à dire que c'est un arbre avec des noeuds et chaque noeud représente une partition de l'ensemble des photons selon un plan médian auquel appartient systématiquement l'un des photons. Les plans sont dirigés selon les axes, donc on a juste à stocker quel dimension a été divisisée en deux puis quelle coordonée est considérée comme médiane dans cette direction.
cela se fait avec un noeud du type :
Code :
- struct kdnode{
- enum {
- dimensionX = 0,
- dimensionY = 1,
- dimensionZ = 2
- } dimensionPlane;
- // float fCoordinatePlane;
- int nPhotonMedian;
- };
|
L'information fCoordinatePlane est déjà comprise dans l'information nPhotonMedian donc on peut s'en débarrasser.
nPhotonMedian est un index qui pointe dans le conteneur à photons sur l'élement dont la coordonnée divise notre ensemble en deux. Bien entendu après avoir équilibré l'arbre on ne touche plus au container à photons.
Voici le cache dans toute sa splendeur.. Simplicité avant tout.
Code :
- class photoncache
- {
- public:
- vector<photon> photonTab;
- vector<vecteur> normalTab;
- vector<kdnode> nodeTab;
- void balance(vector<int>& tab, int p);
- bool bBalanced;
- };
|
Je reviendrai sur la fonction balance..
Pendant le tracé de photons, à chaque fois que l'on veut balancer un photon on se contente d'appeler addPhoton sur la photon Map.
Voici la fonction addPhoton:
Code :
- bool photonmap::addPhoton (const photon & newPhoton, const vecteur & normal)
- {
- if (!container)
- return false;
- if (int(container->photonTab.size()) >= number )
- return false;
- container->photonTab.push_back(newPhoton);
- container->normalTab.push_back(normal);
- return true;
- }
|
Le seul truc à retenir c'est qu'il renvoie false quand il n'y a plus de place dans le container à photons.
La fonction photon trace
Le tracé de photons est une version simplifiée
du tracé de rayons vue. La principale différence c'est qu'on
zappe la partie calcul d'éclairement. On ne calcule plus ici le coefficient de transfert explicitement; on se contente de tirer à la roulette russe si le photon est transmis ou absorbé et selon quelle méthode il est transmis.
Code :
- bool addPhotonRay(ray viewRay, scene &myScene, context myContext, photonmap& myPhotonMap)
- {
- int level = 0;
- do {
- point ptHitPoint;
- float t = 20000.0f;
- int currentSphere=-1;
- {
- for (unsigned int i = 0; i < myScene.sphTab.size() ; ++i) {
- if (isSphereIntersected(viewRay, myScene.sphTab[i], t)) {
- currentSphere = i;
- }
- }
- if (currentSphere == -1)
- {
- break; // Le photon s'est perdu dans l'espace infini, arretons de stocker des photons;
- }
- ptHitPoint = viewRay.start + t * viewRay.dir;
- }
- // la normale au point d'intersection
- vecteur vNormal = ptHitPoint - myScene.sphTab[currentSphere].pos;
- float temp = vNormal * vNormal;
- if (temp == 0.0f)
- break;
- vNormal = invsqrtf(temp) * vNormal;
- materialChannel currentMat = myScene.matTab[myScene.sphTab[currentSphere].material].tab[myContext.offset];
- float bInside;
- if (vNormal * viewRay.dir > 0.0f)
- {
- vNormal = -1.0f * vNormal;
- bInside = true;
- }
- else
- {
- bInside = false;
- }
- if (myScene.sphTab[currentSphere].size < 0.0f)
- {
- bInside = !bInside;
- }
- if (myContext.fTransmittance != 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
- float coef = powf(myContext.fTransmittance, t);
- float fRoulette = (1.0f / RAND_MAX) * rand();
- if (fRoulette > coef )
- {
- // Le photon s'est retrouvé absorbé avant d'arriver au point d'intersection.
- // On peut juste s'arreter de stocker des photons
- break;
- }
- }
- float fViewProjection = viewRay.dir * vNormal;
- float fReflectance, fTransmittance;
- float fCosThetaI, fSinThetaI, fCosThetaT, fSinThetaT;
- // ici je zappe pour le forum mais on calcule la reflectance et la transmittance
- // comme avant..
- fTransmittance = currentMat.refraction * (1.0f - fReflectance);
- fReflectance = currentMat.reflection * fReflectance;
- float fTotalWeight = fReflectance + fTransmittance;
- if (fTotalWeight < 0.99f)
- {
- // La surface n'est pas totalement transitive, on peut stocker le photon incident
- photon currentPhoton; // {ptHitPoint, viewRay.dir, coef};
- currentPhoton.pos = ptHitPoint;
- currentPhoton.dir = viewRay.dir;
- if (!myPhotonMap.addPhoton(currentPhoton, vNormal))
- {
- // Le container a photon a atteint ses limites.
- // On peut juste s'arreter de stocker des photons
- return false;
- };
- }
- // ça devrait être la réfractance totale de la BRDF locale
- // plutot que ce parametre diffuse1 qui ne correspond à rien.
- fTotalWeight = fTotalWeight + currentMat.diffuse1;
- float fRoulette = ( fTotalWeight / RAND_MAX) * rand();
- if (fRoulette <= fReflectance)
- {
- // Le rayon viewRay est réflechi par la normale.
- // Pour calculer le rayon refléchi on fait la projection
- // du vecteur direction du rayon vue
- // sur la direction de la normale.
- // Pour avoir le vecteur tangent il faudrait retrancher
- // cette projection au vecteur direction
- // mais ici on veut la reflexion et donc il faut retrancher
- // la projection deux fois au vecteur direction.
- float fReflection = 2.0f * fViewProjection;
- // on fait partir le nouveau rayon du point d'intersection avec la sphère courante
- // et on le dirige selon le vecteur reflexion que l'on vient de calculer
- viewRay.start = ptHitPoint;
- viewRay.dir += fReflection * vNormal;
- }
- else if(fRoulette <= fReflectance + fTransmittance)
- {
- float fOldRefractionCoef = myContext.fRefractionCoef;
- if (bInside)
- {
- myContext.fRefractionCoef = context::getDefaultAir().fRefractionCoef;
- myContext.fTransmittance = context::getDefaultAir().fTransmittance;
- }
- else
- {
- myContext.fRefractionCoef = currentMat.density;
- myContext.fTransmittance = currentMat.absorption;
- }
- // ici on calcule le rayon transmis avec la formule de Snell-Descartes
- viewRay.start = ptHitPoint;
- viewRay.dir += fCosThetaI * vNormal;
- viewRay.dir = (fOldRefractionCoef / myContext.fRefractionCoef) * viewRay.dir;
- viewRay.dir += (-fCosThetaT) * vNormal;
- }
- // else .. utiliser la brdf pour déterminer dans quelle direction renvoyer le photon
- else
- {
- // notre BRDF nous dit que le photon est soit réflechi soit réfracté
- // soit absorbé.. Pas de color bleeding dû à la diffusion.. pour l'instant
- break;
- }
- level++;
- } while (level < 10);
- return true;
- }
|
Le code devrait être assez explicit surtout qu'il reprend beaucoup du code du raytracer précédent.
Tout pour cette nuit,
la suite bientôt.
A+
LeGreg