Traçage de chemin progressif avec échantillonnage explicite de la lumière

14

J'ai compris la logique de l'échantillonnage d'importance pour la partie BRDF. Cependant, quand il s'agit d'échantillonner explicitement des sources de lumière, tout devient confus. Par exemple, si j'ai une source de lumière ponctuelle dans ma scène et si je l'échantillon directement à chaque image en permanence, dois-je la compter comme un échantillon de plus pour l'intégration de Monte Carlo? Autrement dit, je prends un échantillon de la distribution pondérée en cosinus et un autre de la lumière ponctuelle. S'agit-il de deux échantillons au total ou d'un seul? De plus, dois-je diviser le rayonnement provenant de l'échantillon direct à n'importe quel terme?

Mustafa Işık
la source

Réponses:

19

Il existe plusieurs zones dans le traçage de chemin qui peuvent être échantillonnées en importance. De plus, chacun de ces domaines peut également utiliser l'échantillonnage à importance multiple, proposé pour la première fois dans le document de 1995 de Veach et Guibas . Pour mieux expliquer, regardons un traceur de chemin vers l'arrière:

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);
    SurfaceInteraction interaction;

    // Bounce the ray around the scene
    const uint maxBounces = 15;
    for (uint bounces = 0; bounces < maxBounces; ++bounces) {
        m_scene->Intersect(ray);

        // The ray missed. Return the background color
        if (ray.GeomID == INVALID_GEOMETRY_ID) {
            color += throughput * m_scene->BackgroundColor;
            break;
        }

        // 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 emission
        if (light != nullptr) {
            color += throughput * light->Le();
        }

        interaction.Position = ray.Origin + ray.Direction * ray.TFar;
        interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
        interaction.OutputDirection = normalize(-ray.Direction);


        // Get the new ray direction
        // Choose the direction based on the bsdf        
        material->bsdf->Sample(interaction, sampler);
        float pdf = material->bsdf->Pdf(interaction);

        // Accumulate the weight
        throughput = throughput * material->bsdf->Eval(interaction) / pdf;

        // Shoot a new ray

        // Set the origin at the intersection point
        ray.Origin = interaction.Position;

        // Reset the other ray properties
        ray.Direction = interaction.InputDirection;
        ray.TNear = 0.001f;
        ray.TFar = infinity;


        // Russian Roulette
        if (bounces > 3) {
            float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
            if (sampler->NextFloat() > p) {
                break;
            }

            throughput *= 1 / p;
        }
    }

    m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}

En anglais:

  1. Tirez un rayon à travers la scène
  2. Vérifiez si nous avons touché quelque chose. Sinon, nous retournons la couleur de la skybox et la cassons.
  3. Vérifiez si nous avons touché une lumière. Si oui, nous ajoutons l'émission lumineuse à notre accumulation de couleurs
  4. Choisissez une nouvelle direction pour le rayon suivant. Nous pouvons le faire uniformément, ou un échantillon d'importance basé sur le BRDF
  5. Évaluez le BRDF et accumulez-le. Ici, nous devons diviser par le pdf de notre direction choisie, afin de suivre l'algorithme de Monte Carlo.
  6. Créez un nouveau rayon basé sur notre direction choisie et d'où nous venons
  7. [Facultatif] Utilisez la roulette russe pour choisir si nous devons mettre fin au rayon
  8. Goto 1

Avec ce code, nous n'obtenons de la couleur que si le rayon frappe finalement une lumière. De plus, il ne prend pas en charge les sources lumineuses ponctuelles, car elles n'ont pas de zone.

Pour résoudre ce problème, nous échantillonnons les lumières directement à chaque rebond. Nous devons faire quelques petits changements:

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);
    SurfaceInteraction interaction;

    // Bounce the ray around the scene
    const uint maxBounces = 15;
    for (uint bounces = 0; bounces < maxBounces; ++bounces) {
        m_scene->Intersect(ray);

        // The ray missed. Return the background color
        if (ray.GeomID == INVALID_GEOMETRY_ID) {
            color += throughput * m_scene->BackgroundColor;
            break;
        }

        // 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 this is the first bounce or if we just had a specular bounce,
        // we need to add the emmisive light
        if ((bounces == 0 || (interaction.SampledLobe & BSDFLobe::Specular) != 0) && light != nullptr) {
            color += throughput * light->Le();
        }

        interaction.Position = ray.Origin + ray.Direction * ray.TFar;
        interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
        interaction.OutputDirection = normalize(-ray.Direction);


        // Calculate the direct lighting
        color += throughput * SampleLights(sampler, interaction, material->bsdf, light);


        // Get the new ray direction
        // Choose the direction based on the bsdf        
        material->bsdf->Sample(interaction, sampler);
        float pdf = material->bsdf->Pdf(interaction);

        // Accumulate the weight
        throughput = throughput * material->bsdf->Eval(interaction) / pdf;

        // Shoot a new ray

        // Set the origin at the intersection point
        ray.Origin = interaction.Position;

        // Reset the other ray properties
        ray.Direction = interaction.InputDirection;
        ray.TNear = 0.001f;
        ray.TFar = infinity;


        // Russian Roulette
        if (bounces > 3) {
            float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
            if (sampler->NextFloat() > p) {
                break;
            }

            throughput *= 1 / p;
        }
    }

    m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}

Tout d'abord, nous ajoutons "color + = throughput * SampleLights (...)". Je vais entrer dans les détails de SampleLights () un peu. Mais, essentiellement, il boucle à travers toutes les lumières, et rend leur contribution à la couleur, atténuée par le BSDF.

C'est très bien, mais nous devons apporter un changement supplémentaire pour le corriger; en particulier, ce qui se passe lorsque nous frappons une lumière. Dans l'ancien code, nous avons ajouté l'émission de lumière à l'accumulation de couleurs. Mais maintenant, nous échantillonnons directement la lumière à chaque rebond, donc si nous ajoutions l'émission de lumière, nous ferions un "double dip". Par conséquent, la bonne chose à faire est ... rien; nous sautons en accumulant l'émission de lumière.

Cependant, il existe deux cas d'angle:

  1. Le premier rayon
  2. Rebonds parfaitement spéculaires (aka miroirs)

Si le premier rayon frappe la lumière, vous devriez voir directement l'émission de la lumière. Donc, si nous la sautons, toutes les lumières apparaîtront en noir, même si les surfaces autour d'elles sont allumées.

Lorsque vous touchez une surface parfaitement spéculaire, vous ne pouvez pas directement échantillonner une lumière, car un rayon d'entrée n'a qu'une seule sortie. Eh bien, techniquement, nous pourrions vérifier si le rayon d'entrée va frapper une lumière, mais cela ne sert à rien; la boucle de traçage principale le fera de toute façon. Par conséquent, si nous frappons une lumière juste après avoir frappé une surface spéculaire, nous devons accumuler la couleur. Sinon, les lumières seront noires dans les miroirs.

Maintenant, examinons SampleLights ():

float3 SampleLights(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
    std::size_t numLights = m_scene->NumLights();

    float3 L(0.0f);
    for (uint i = 0; i < numLights; ++i) {
        Light *light = &m_scene->Lights[i];

        // Don't let a light contribute light to itself
        if (light == hitLight) {
            continue;
        }

        L = L + EstimateDirect(light, sampler, interaction, bsdf);
    }

    return L;
}

En anglais:

  1. Parcourez toutes les lumières
  2. Saute la lumière si on la frappe
    • Ne double pas
  3. Accumulez l'éclairage direct de toutes les lumières
  4. Retour de l'éclairage direct

Enfin, EstimateDirect () évalue simplementBSDF(p,ωi,ωo)Li(p,ωi)

Pour les sources lumineuses ponctuelles, ceci est simple:

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        return float3(0.0f);
    }

    interaction.InputDirection = normalize(light->Origin - interaction.Position);
    return bsdf->Eval(interaction) * light->Li;
}

Cependant, si nous voulons que les lumières aient une surface, nous devons d'abord échantillonner un point sur la lumière. Par conséquent, la définition complète est:

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    float3 directLighting = float3(0.0f);

    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        float pdf;
        float3 Li = light->SampleLi(sampler, m_scene, interaction, &pdf);

        // Make sure the pdf isn't zero and the radiance isn't black
        if (pdf != 0.0f && !all(Li)) {
            directLighting += bsdf->Eval(interaction) * Li / pdf;
        }
    }

    return directLighting;
}

Nous pouvons implémenter light-> SampleLi comme nous le voulons; nous pouvons choisir le point uniformément, ou l'échantillon d'importance. Dans les deux cas, nous divisons la radiosité par le pdf de choix du point. Encore une fois, pour satisfaire aux exigences de Monte Carlo.

Si le BRDF dépend fortement de la vue, il peut être préférable de choisir un point basé sur le BRDF, au lieu d'un point aléatoire sur la lumière. Mais comment choisissons-nous? Échantillon basé sur la lumière, ou basé sur le BRDF?

Pourquoi pas les deux? Entrez l'échantillonnage à importance multiple. En bref, nous évaluons plusieurs fois, en utilisant différentes techniques d'échantillonnage, puis en les moyenne ensemble en utilisant des poids basés sur leurs pdfs. En code, c'est:BSDF(p,ωi,ωo)Li(p,ωi)

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    float3 directLighting = float3(0.0f);
    float3 f;
    float lightPdf, scatteringPdf;


    // Sample lighting with multiple importance sampling
    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        float3 Li = light->SampleLi(sampler, m_scene, interaction, &lightPdf);

        // Make sure the pdf isn't zero and the radiance isn't black
        if (lightPdf != 0.0f && !all(Li)) {
            // Calculate the brdf value
            f = bsdf->Eval(interaction);
            scatteringPdf = bsdf->Pdf(interaction);

            if (scatteringPdf != 0.0f && !all(f)) {
                float weight = PowerHeuristic(1, lightPdf, 1, scatteringPdf);
                directLighting += f * Li * weight / lightPdf;
            }
        }
    }


    // Sample brdf with multiple importance sampling
    bsdf->Sample(interaction, sampler);
    f = bsdf->Eval(interaction);
    scatteringPdf = bsdf->Pdf(interaction);
    if (scatteringPdf != 0.0f && !all(f)) {
        lightPdf = light->PdfLi(m_scene, interaction);
        if (lightPdf == 0.0f) {
            // We didn't hit anything, so ignore the brdf sample
            return directLighting;
        }

        float weight = PowerHeuristic(1, scatteringPdf, 1, lightPdf);
        float3 Li = light->Le();
        directLighting += f * Li * weight / scatteringPdf;
    }

    return directLighting;
}

En anglais:

  1. Tout d'abord, nous échantillonnons la lumière
    • Cela met à jour l'interaction.
    • Nous donne le Li pour la lumière
    • Et le pdf de choisir ce point sur la lumière
  2. Vérifiez que le pdf est valide et que l'éclat n'est pas nul
  3. Évaluer le BSDF à l'aide de la InputDirection échantillonnée
  4. Calculez le pdf pour le BSDF étant donné la InputDirection échantillonnée
    • Essentiellement, quelle est la probabilité de cet échantillon, si nous devions échantillonner en utilisant le BSDF, au lieu de la lumière
  5. Calculez le poids à l'aide du pdf léger et du pdf BSDF
    • Veach et Guibas définissent deux façons différentes de calculer le poids. Expérimentalement, ils ont trouvé l'heuristique de puissance avec une puissance de 2 pour fonctionner le mieux dans la plupart des cas. Je vous renvoie à l'article pour plus de détails. L'implémentation est ci-dessous
  6. Multipliez le poids avec le calcul de l'éclairage direct et divisez par le pdf léger. (Pour Monte Carlo) Et ajouter à l'accumulation de lumière directe.
  7. Ensuite, nous échantillonnons le BRDF
    • Cela met à jour l'interaction.
  8. Évaluer le BRDF
  9. Obtenez le pdf pour choisir cette direction sur la base du BRDF
  10. Calculez le pdf léger, compte tenu de la InputDirection échantillonnée
    • C'est le miroir d'avant. Quelle est la probabilité de cette direction, si nous devions échantillonner la lumière
  11. Si lightPdf == 0.0f, alors le rayon a manqué la lumière, il suffit donc de renvoyer l'éclairage direct de l'échantillon de lumière.
  12. Sinon, calculez le poids et ajoutez l'éclairage direct BSDF à l'accumulation
  13. Enfin, restituez l'éclairage direct accumulé

.

inline float PowerHeuristic(uint numf, float fPdf, uint numg, float gPdf) {
    float f = numf * fPdf;
    float g = numg * gPdf;

    return (f * f) / (f * f + g * g);
}

Il y a un certain nombre d'optimisations / améliorations que vous pouvez faire dans ces fonctions, mais je les ai analysées pour essayer de les rendre plus faciles à comprendre. Si vous le souhaitez, je peux partager certaines de ces améliorations.

Échantillonnage d'une seule lumière

Dans SampleLights (), nous parcourons toutes les lumières et obtenons leur contribution. Pour un petit nombre de lumières, c'est bien, mais pour des centaines ou des milliers de lumières, cela coûte cher. Heureusement, nous pouvons exploiter le fait que l'intégration de Monte-Carlo est une moyenne géante. Exemple:

Définissons

h(x)=f(x)+g(x)

Actuellement, nous estimons par:h(x)

h(x)=1Ni=1Nf(xi)+g(xi)

Mais, calculer à la fois et coûte cher, donc à la place nous faisons:f(x)g(x)

h(x)=1Ni=1Nr(ζ,x)pdf

Où est une variable aléatoire uniforme et est défini comme:ζr(ζ,x)

r(ζ,x)={f(x),0.0ζ<0.5g(x),0.5ζ<1.0

Dans ce cas car le pdf doit s'intégrer à 1, et il y a 2 fonctions à choisir.pdf=12

En anglais:

  1. Choisissez aléatoirement ou pour évaluer.g ( x )f(x)g(x)
  2. Divisez le résultat par (car il y a deux éléments)12
  3. Moyenne

Lorsque N devient grand, l'estimation convergera vers la bonne solution.

On peut appliquer ce même principe à l'échantillonnage de lumière. Au lieu d'échantillonner chaque lumière, nous en choisissons une au hasard et multiplions le résultat par le nombre de lumières (c'est la même chose que la division par le pdf fractionnaire):

float3 SampleOneLight(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
    std::size_t numLights = m_scene->NumLights();

    // Return black if there are no lights
    // And don't let a light contribute light to itself
    // Aka, if we hit a light
    // This is the special case where there is only 1 light
    if (numLights == 0 || numLights == 1 && hitLight != nullptr) {
        return float3(0.0f);
    }

    // Don't let a light contribute light to itself
    // Choose another one
    Light *light;
    do {
        light = m_scene->RandomOneLight(sampler);
    } while (light == hitLight);

    return numLights * EstimateDirect(light, sampler, interaction, bsdf);
}

Dans ce code, toutes les lumières ont une chance égale d'être cueillies. Cependant, nous pouvons un échantillon d'importance, si nous le souhaitons. Par exemple, nous pouvons donner à des lumières plus grandes une chance plus élevée d'être cueillies, ou des lumières plus proches de la surface touchée. Il suffit de diviser le résultat par le pdf, qui ne serait plus .1numLights

Importance multiple échantillonner la direction "New Ray"

Le code actuel n'a d'importance que la direction "New Ray" basée sur le BSDF. Et si nous voulons également accorder une importance à l'échantillon en fonction de l'emplacement des lumières?

D'après ce que nous avons appris ci-dessus, une méthode consisterait à tirer deux "nouveaux" rayons et à pondérer chacun en fonction de leur pdfs. Cependant, cela est à la fois coûteux en calcul et difficile à mettre en œuvre sans récursivité.

Pour surmonter cela, nous pouvons appliquer les mêmes principes que nous avons appris en n'échantillonnant qu'une seule lumière. Autrement dit, choisissez au hasard un échantillon, et divisez par le pdf de le choisir.

// Get the new ray direction

// Randomly (uniform) choose whether to sample based on the BSDF or the Lights
float p = sampler->NextFloat();

Light *light = m_scene->RandomLight();

if (p < 0.5f) {
    // Choose the direction based on the bsdf 
    material->bsdf->Sample(interaction, sampler);
    float bsdfPdf = material->bsdf->Pdf(interaction);

    float lightPdf = light->PdfLi(m_scene, interaction);
    float weight = PowerHeuristic(1, bsdfPdf, 1, lightPdf);

    // Accumulate the throughput
    throughput = throughput * weight * material->bsdf->Eval(interaction) / bsdfPdf;

} else {
    // Choose the direction based on a light
    float lightPdf;
    light->SampleLi(sampler, m_scene, interaction, &lightPdf);

    float bsdfPdf = material->bsdf->Pdf(interaction);
    float weight = PowerHeuristic(1, lightPdf, 1, bsdfPdf);

    // Accumulate the throughput
    throughput = throughput * weight * material->bsdf->Eval(interaction) / lightPdf;
}

Que tous dit, avons-nous vraiment envie d'échantillon d'importance direction « New Ray » en fonction de la lumière? Pour l' éclairage direct , la radiosité est affectée à la fois par le BSDF de la surface et par la direction de la lumière. Mais pour l' éclairage indirect , la radiosité est presque exclusivement définie par le BSDF de la surface touchée auparavant. Donc, ajouter un échantillonnage d'importance légère ne nous donne rien.

Par conséquent, il est courant de n'échantillonner que la «nouvelle direction» avec le BSDF, mais d'appliquer un échantillonnage à importance multiple à l'éclairage direct.

RichieSams
la source
Merci pour la réponse explicative! Je comprends que si nous utilisions un traceur sans échantillonnage explicite de la lumière, nous ne toucherions jamais une source lumineuse ponctuelle. Donc, nous pouvons essentiellement ajouter sa contribution. D'un autre côté, si nous échantillonnons une source lumineuse de zone, nous devons nous assurer que nous ne devons pas la frapper à nouveau avec l'éclairage indirect afin d'éviter un double creux
Mustafa Işık
Exactement! Y a-t-il une partie sur laquelle vous avez besoin d'éclaircissements? Ou il n'y a pas assez de détails?
RichieSams
De plus, l'échantillonnage à importance multiple est-il utilisé uniquement pour le calcul de l'éclairage direct? Peut-être que j'ai raté mais je n'en ai pas vu un autre exemple. Si je tire un seul rayon par rebond dans mon traceur de trajectoire, il semble que je ne puisse pas le faire pour le calcul de l'éclairage indirect.
Mustafa Işık
2
L'échantillonnage d'importance multiple peut être appliqué partout où vous utilisez l'échantillonnage d'importance. La puissance de l'échantillonnage d'importance multiple est que nous pouvons combiner les avantages des techniques d'échantillonnage multiples. Par exemple, dans certains cas, l'échantillonnage de faible importance sera meilleur que l'échantillonnage BSDF. Dans d'autres cas, vice versa. MIS combinera le meilleur des deux mondes. Cependant, si l'échantillonnage BSDF est meilleur à 100%, il n'y a aucune raison d'ajouter la complexité du SIG. J'ai ajouté quelques sections à la réponse pour développer ce point
RichieSams
1
Il semble que nous ayons séparé les sources de rayonnement entrantes en deux parties: directe et indirecte. Nous échantillonnons les lumières de manière explicite pour la partie directe et lors de l'échantillonnage de cette partie, il est raisonnable d'importer les lumières ainsi que les BSDF. Pour la partie indirecte, cependant, nous n'avons aucune idée de la direction qui peut potentiellement nous donner des valeurs de rayonnement plus élevées, car c'est le problème lui-même que nous voulons résoudre. Cependant, nous pouvons dire quelle direction peut contribuer davantage selon le terme cosinus et BSDF. Voilà ce que je comprends. Corrigez-moi si je me trompe et merci pour votre réponse géniale.
Mustafa Işık