Afin de comprendre la roulette russe, regardons un traceur de chemin vers l'arrière très basique:
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera.CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// Fetch the material
Material *material = m_scene->GetMaterial(ray.geomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.geomID);
// If we hit a light, add the emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
C'EST À DIRE. nous rebondissons autour de la scène, accumulant des atténuations de couleur et de lumière au fur et à mesure. Pour être complètement mathématiquement impartiaux, les rebonds doivent aller à l'infini. Mais ce n'est pas réaliste et, comme vous l'avez noté, pas visuellement nécessaire; pour la plupart des scènes, après un certain nombre de rebonds, disons 10, le montant de la contribution à la couleur finale est très très minime.
Ainsi, afin d'économiser les ressources informatiques, de nombreux traceurs de chemin ont une limite stricte au nombre de rebonds. Cela ajoute du parti pris.
Cela dit, il est difficile de choisir quelle devrait être cette limite. Certaines scènes ont fière allure après 2 rebonds; d'autres (disons avec transmission ou SSS) peuvent prendre jusqu'à 10 ou 20.
Si nous choisissons trop bas, l'image sera visiblement biaisée. Mais si nous choisissons trop haut, nous perdons de l'énergie et du temps de calcul.
Une façon de résoudre ce problème, comme vous l'avez noté, est de terminer le chemin après avoir atteint un certain seuil d'atténuation. Cela ajoute également des biais.
Le serrage après un seuil fonctionnera , mais encore une fois, comment choisissons-nous le seuil? Si nous choisissons trop grand, l'image sera visiblement biaisée, trop petite et nous gaspillons des ressources.
La roulette russe tente de résoudre ces problèmes de manière impartiale. Tout d'abord, voici le code:
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera.CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// Fetch the material
Material *material = m_scene->GetMaterial(ray.geomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.geomID);
// If we hit a light, add the emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Russian Roulette
// Randomly terminate a path with a probability inversely equal to the throughput
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
// Add the energy we 'lose' by randomly terminating paths
throughput *= 1 / p;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
La roulette russe termine aléatoirement un chemin avec une probabilité inversement égale au débit. Ainsi, les chemins à faible débit qui ne contribueront pas beaucoup à la scène sont plus susceptibles d'être interrompus.
Si nous nous arrêtons là, nous sommes toujours biaisés. Nous «perdons» l'énergie du chemin que nous terminons au hasard. Pour le rendre non biaisé, nous augmentons l'énergie des chemins non terminés par leur probabilité d'être terminée. Ceci, en plus d'être aléatoire, rend la roulette russe impartiale.
Pour répondre à vos dernières questions:
- La roulette russe donne-t-elle un résultat impartial?
- La roulette russe est-elle nécessaire pour un résultat impartial?
- Cela dépend de ce que vous entendez par impartial. Si vous voulez dire mathématiquement, alors oui. Cependant, si vous voulez dire visuellement, alors non. Il vous suffit de choisir très soigneusement la profondeur de trajectoire maximale et le seuil de coupure. Cela peut être très fastidieux car il peut changer d'une scène à l'autre.
- Pouvez-vous utiliser une probabilité fixe (coupure), puis redistribuer l'énergie «perdue». Est-ce impartial?
- Si vous utilisez une probabilité fixe, vous ajoutez un biais. En redistribuant l'énergie «perdue», vous réduisez le biais, mais il est toujours biaisé mathématiquement. Pour être totalement impartial, il doit être aléatoire.
- Si l'énergie qui serait perdue en terminant un rayon sans redistribuer son énergie est finalement perdue de toute façon (comme les rayons auxquels elle est redistribuée sont également finalement arrêtés), comment cela améliore-t-il la situation?
- La roulette russe arrête seulement le rebond. Il ne supprime pas complètement l'échantillon. De plus, l'énergie «perdue» est comptabilisée dans les rebonds jusqu'à la terminaison. Donc, la seule façon pour que l'énergie soit «finalement perdue de toute façon» serait d'avoir une pièce complètement noire.
En fin de compte, la roulette russe est un algorithme très simple qui utilise une très petite quantité de ressources informatiques supplémentaires. En échange, il peut économiser une grande quantité de ressources de calcul. Par conséquent, je ne vois pas vraiment de raison de ne pas l'utiliser.
to be completely unbiased it must be random
. Je pense que vous pouvez toujours obtenir des résultats mathématiques corrects en utilisant le poids fractionné des échantillons, plutôt que le passage / baisse binaire que la roulette russe impose, c'est juste que la roulette convergera plus rapidement car elle opère un échantillonnage d'importance parfaite.