LeGreg | Pour ceux qui se demandent, le fond blanc est une sphère vue de
l'intérieur
et oui comme je suis très paresseux je n'ai toujours pas implémenté l'intersection rayon/plan (une ligne de code !).
Philosophiquement je me dédouane en disant que l'on suppporte les plans puisque l'on supporte les sphères de rayon très très grand . (et une sphère très très grande est localement assimilable à son plan tangent). Plus sur la fonction balance
Que se passe-t-il une fois que l'on a rajouté les photons dans le cache à photon ? Il faut les ranger, de manière à ce qu'ils soient directement accessibles ensuite par le raytracer. Comment les range-t-on ? j'ai déja évoqué le principe du KD tree. C'est un arbre tel que pour chaque noeud on a une dimension de subdivision et un plan qui coupe l'espace en deux et qui est orthogonal à cette direction.
Ainsi, suivant l'endroit où l'on se trouve par rapport à ce plan, on sait si l'on est plus proche des photons stockés à droite ou les photons stockés à gauche (en haut, en bas, en avant ou en arrière).
Il ne suffit pas d'avoir autant de plans que nécessaires pour n'avoir plus qu'un seul photon par feuille de l'arbre. Il faut aussi que l'arbre soit équilibré pour que son parcours soit optimal.
Comment équilibrer l'arbre ? c'est simple, on le fait à la construction. Soit l'ensemble de n photons qui définit un parallélépipède rectangle parallèle aux axes. On détermine quelle est la plus grande dimension de ce parallélépipède. Ce sera la dimension de notre subdivision. Ensuite on trie les photons de cet ensemble selon la ieme coordonnée correspondant à la dimension de subdivision. Une fois qu'ils sont triès on prend le photon médian (correspondant à n/2 dans l'ordre défini plus haut). Ce photon définira le plan médian et les photons d'un coté et de l'autre de ce photon iront dans la première branche ou la deuxième branche de l'arbre.
Comme on a expliqué, un kd tree est très compact. Il n'y a pas besoin de pointeurs vers les branches de l'arbre. les noeuds sont rangés dans un vecteur de manière simple. Le noeud p a deux fils 2 * p et 2 * p + 1. On commence le noeud racine pour p = 1.
Un noeud contient juste la dimension de subdivision du noeud et l'index du photon médian. C'est l'un des formats d'arbre les plus compacts que l'on puisse avoir. si l'on voulait quelque chose de plus compact encore on pourrait stocker la dimension de subdivision de manière implicite (en alternant x, y et z). Voici le code de la fonction balance.
Code :
- template<int n> struct sortingCriterion
- {
- photon *photonTab;
- bool operator() (int p1, int p2)
- {
- if (n == kdnode::dimensionX)
- {
- return photonTab[p1].pos.x < photonTab[p2].pos.x;
- }
-
- if (n == kdnode::dimensionY)
- {
- return photonTab[p1].pos.y < photonTab[p2].pos.y;
- }
-
- if (n == kdnode::dimensionZ)
- {
- return photonTab[p1].pos.z < photonTab[p2].pos.z;
- }
- }
- };
- void photoncache::balance(vector<int>& tab, int p)
- {
- // il n'y a plus rien à équilibrer
- if (tab.size() == 0)
- return;
- vector<int> tabLess;
- vector<int> tabMore;
- // tant que l'on n'atteint pas la taille d'un arbre déraisonnable
- if (2 * p + 1 < 100 * int(photonTab.size()))
- {
- kdnode node;
- node.nPhotonMedian = -1;
- if (tab.size() == 1)
- {
- node.nPhotonMedian = tab[0];
- }
- else
- {
- float minX, minY, minZ;
- float maxX, maxY, maxZ;
- minX = minY = minZ = numeric_limits<float>::max();
- maxX = maxY = maxZ = - numeric_limits<float>::max();
- tabLess.reserve(tab.size());
- tabMore.reserve(tab.size());
- for (int i = 0; i < int(tab.size()); ++i)
- {
- // calcul d'un parallélépipède rectangle
- minX = min(minX, photonTab[tab[i]].pos.x);
- minY = min(minY, photonTab[tab[i]].pos.y);
- minZ = min(minZ, photonTab[tab[i]].pos.z);
- maxX = max(maxX, photonTab[tab[i]].pos.x);
- maxY = max(maxY, photonTab[tab[i]].pos.y);
- maxZ = max(maxZ, photonTab[tab[i]].pos.z);
- }
- if ( maxX - minX >= maxY - minY )
- {
- if (maxX - minX >= maxZ - minZ)
- {
- node.dimensionPlane = kdnode::dimensionX;
- sortingCriterion<kdnode::dimensionX> pred = {&photonTab[0]};
- // on trie
- sort(tab.begin(), tab.end(), pred);
- node.nPhotonMedian = tab[int(tab.size()) / 2];
- // puis on distribue.. à droite puis à gauche
- for (int i = 0; i< int(tab.size()) / 2; ++i)
- {
- tabLess.push_back(tab[i]);
- }
- for (int i = int(tab.size()) / 2 + 1; i< int(tab.size()); ++i)
- {
- tabMore.push_back(tab[i]);
- }
- }
- else
- {
- node.dimensionPlane = kdnode::dimensionZ;
- sortingCriterion<kdnode::dimensionZ> pred = {&photonTab[0]};
- sort(tab.begin(), tab.end(), pred);
- node.nPhotonMedian = tab[int(tab.size()) / 2];
- for (int i = 0; i< int(tab.size()) / 2; ++i)
- {
- tabLess.push_back(tab[i]);
- }
- for (int i = int(tab.size()) / 2 + 1; i< int(tab.size()); ++i)
- {
- tabMore.push_back(tab[i]);
- }
- }
- }
- else
- {
- if (maxY - minY >= maxZ - minZ)
- {
- node.dimensionPlane = kdnode::dimensionY;
- sortingCriterion<kdnode::dimensionY> pred = {&photonTab[0]};
- sort(tab.begin(), tab.end(), pred);
- node.nPhotonMedian = tab[int(tab.size()) / 2];
- for (int i = 0; i< int(tab.size()) / 2; ++i)
- {
- tabLess.push_back(tab[i]);
- }
- for (int i = int(tab.size()) / 2 + 1; i< int(tab.size()); ++i)
- {
- tabMore.push_back(tab[i]);
- }
- }
- else
- {
- node.dimensionPlane = kdnode::dimensionZ;
- sortingCriterion<kdnode::dimensionZ> pred = {&photonTab[0]};
- sort(tab.begin(), tab.end(), pred);
- node.nPhotonMedian = tab[int(tab.size()) / 2];
- for (int i = 0; i< int(tab.size()) / 2; ++i)
- {
- tabLess.push_back(tab[i]);
- }
- for (int i = int(tab.size()) / 2 + 1; i< int(tab.size()); ++i)
- {
- tabMore.push_back(tab[i]);
- }
- }
- }
- }
- if (p >= int(nodeTab.size()) )
- nodeTab.resize(p + 1);
- nodeTab[p] = node;
- }
- // continue à équilibrer dans la premiere branche
- balance(tabLess, 2 * p);
- // continue à équilibrer dans la deuxième branche
- balance(tabMore, 2 * p + 1);
- }
|
Elle n'est pas optimisée en consommation mémoire on pourrait ne pas allouer les vecteur intermédiaire et limiter la recursion. Mais à ce stade on s'en fiche ce n'est pas ce qui coute le plus cher dans notre programme de photon/raytracing.
J'utilise le tri standard de la STL, avec un critère template dépendant de la dimension. Rien de bien folichon.
La localisation des photons les plus proches
On doit trouver les m photons les plus proches de notre point d'intersection. Comment obtenir ça à partir de notre KD tree?
C'est simple. Pour chaque branche que l'on parcourt on ajoute le photon médian dans une liste triée selon la distance à notre point de départ. On s'assure constamment que la taille de la liste soit inférieure à m. Une fois que l'on a m éléments on ne s'arrete pas mais on continue notre parcours de l'arbre.
Bien entendu on ne parcourt pas la totalité de l'arbre ce qui serait malvenu vu que ça nous couterait plus cher que de parcourir la liste des photons en entier. Une fois que l'on a m éléments on peut déjà savoir quelle est la distance (au carré) maximale qu'un photon doit avoir pour etre pris.
Il ne sert donc à rien de parcourir les branches dont le plan médian indique qu'elles sont plus loin que la distance maximale de notre point de départ. C'est ce qu'on appelle du pruning (élagage) et qui nous garantit que l'algo de recherche sera optimal.
Bien entendu tant que l'on n'a pas trouvé m photons il y a un risque que l'on parcourt trop de branches de photons dont on n'aura pas besoin, on va donc limiter notre distance de recherche de départ (à 10000.0f ici mais on pourrait calculer une valeur adaptée à chaque ensemble de photons).
Voici le code de locate photons:
Code :
- void photonmap::locatePhotons (const point& pos, int nPhotons, std::map<float,int> &priorityHeap, int firstNode)
- {
- photon * photonTab = &container->photonTab[0];
- kdnode * nodeTab = &container->nodeTab[0];
- // si l'on a pas dépassé la taille de notre arbre
- if (2 * firstNode + 1 < int(container->nodeTab.size()))
- {
- // il y a parfois des noeuds vides en fin de branche
- // dans ce cas le photon médian est marqué à -1
- if (nodeTab[firstNode].nPhotonMedian >= 0)
- {
- int currentPhoton = nodeTab[firstNode].nPhotonMedian;
- point ptPhotonPos = photonTab[currentPhoton].pos;
- vecteur vDistToPhoton = pos - ptPhotonPos;
- float fDistSquare = vDistToPhoton * vDistToPhoton;
- if (int(priorityHeap.size()) == nPhotons )
- {
- if ( (*priorityHeap.rbegin()).first > fDistSquare)
- {
- priorityHeap.erase((*priorityHeap.rbegin()).first);
- priorityHeap[fDistSquare] = currentPhoton;
- }
- }
- else
- {
- priorityHeap[fDistSquare] = currentPhoton;
- }
- float fDist = 0.0f;
- switch (nodeTab[firstNode].dimensionPlane)
- {
- case kdnode::dimensionX:
- fDist = pos.x - ptPhotonPos.x;
- break;
- case kdnode::dimensionY:
- fDist = pos.y - ptPhotonPos.y;
- break;
- case kdnode::dimensionZ:
- fDist = pos.z - ptPhotonPos.z;
- break;
- }
- if (fDist <= 0.0f)
- {
- float fDistSquare = fDist * fDist;
- locatePhotons(pos, nPhotons, priorityHeap, 2 * firstNode);
- if ( 10000.0f >= fDistSquare && (!(int(priorityHeap.size()) == nPhotons ) || ((*priorityHeap.rbegin()).first >= fDistSquare)))
- {
- locatePhotons(pos, nPhotons, priorityHeap, 2 * firstNode + 1);
- }
- }
- else
- {
- float fDistSquare = fDist * fDist;
- locatePhotons(pos, nPhotons, priorityHeap, 2 * firstNode + 1);
- if ( 10000.0f >= fDistSquare && (!(int(priorityHeap.size()) == nPhotons ) || ((*priorityHeap.rbegin()).first >= fDistSquare)))
- {
- locatePhotons(pos, nPhotons, priorityHeap, 2 * firstNode);
- }
- }
- }
- }
- }
|
La pour garantir que notre liste de photons déjà trouvés est triee on utilise encore un map<float, int>. (il n'y a pas obligation de garantir qu'elle soit triee, on peut se contenter de pouvoir localiser rapidement l'objet le plus distant et le remplacer par un moins distant).
Voici enfin le code de la fonction find nearest photons :
Code :
- int photonmap::findNearestPhotons (const point& pos, const vecteur & normal, float maxAngle, int nPhotons, photon * resultTab )
- {
- float fMinCos = fabsf(cosf(maxAngle));
- map<float, int> priorityHeap;
- locatePhotons(pos, nPhotons, priorityHeap, 1);
- int i = 0;
- photon * photonTab = &container->photonTab[0];
- vecteur * normalTab = &container->normalTab[0];
- for (map<float, int>::const_iterator it =priorityHeap.begin(); it != priorityHeap.end(); ++it)
- {
- if ((normalTab[(*it).second] * normal > fMinCos) && (photonTab[(*it).second].dir * normal < 0.0f) )
- {
- resultTab[i] = container->photonTab[(*it).second];
- ++i;
- }
- if (i >= nPhotons)
- break;
- }
- return i;
- }
|
La boucle à la fin remplit notre résultat container à photon. Il ne retourne pas tous les photons à l'envoyeur, seuls ceux qui sont considérés comme "compatibles" sont renvoyés (ceux qui ne donneront pas une luminosité de zéro ou ceux qui ont été stockés pour une surface similaire à celle que l'on cherche à évaluer).
Any questions ?
LeGreg Message édité par LeGreg le 23-09-2003 à 12:39:57
|