In der Pfadverfolgung gibt es mehrere Bereiche, für die eine Stichprobe erstellt werden kann. Darüber hinaus kann in jedem dieser Bereiche auch Multiple Importance Sampling verwendet werden, das erstmals in der Veröffentlichung von Veach und Guibas aus dem Jahr 1995 vorgeschlagen wurde . Schauen wir uns zur besseren Erklärung einen Rückwärtspfad-Tracer an:
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);
}
Auf Englisch:
- Schießen Sie einen Strahl durch die Szene
- Überprüfen Sie, ob wir etwas getroffen haben. Wenn nicht, geben wir die Farbe der Skybox zurück und brechen.
- Überprüfen Sie, ob wir ein Licht schlagen. Wenn ja, addieren wir die Lichtemission zu unserer Farbakkumulation
- Wähle eine neue Richtung für den nächsten Strahl. Wir können dies einheitlich oder anhand des BRDF-Beispiels durchführen
- Bewerten Sie das BRDF und akkumulieren Sie es. Hier müssen wir durch das pdf unserer gewählten Richtung dividieren, um dem Monte-Carlo-Algorithmus zu folgen.
- Erstellen Sie einen neuen Strahl, der auf der gewählten Richtung und der Herkunft basiert
- [Optional] Verwenden Sie russisches Roulette, um zu entscheiden, ob wir den Strahl beenden sollen
- Gehe zu 1
Mit diesem Code erhalten wir nur dann Farbe, wenn der Strahl irgendwann auf ein Licht trifft. Darüber hinaus werden pünktliche Lichtquellen nicht unterstützt, da sie keine Fläche haben.
Um dies zu beheben, testen wir die Lichter direkt bei jedem Sprung. Wir müssen ein paar kleine Änderungen vornehmen:
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);
}
Zuerst fügen wir "color + = throughput * SampleLights (...)" hinzu. Ich werde gleich auf SampleLights () eingehen. Im Wesentlichen durchläuft es jedoch alle Lichter und gibt ihren Beitrag zur Farbe zurück, die vom BSDF gedämpft wird.
Das ist großartig, aber wir müssen noch eine Änderung vornehmen, um es richtig zu machen. speziell, was passiert, wenn wir ein Licht schlagen. Im alten Code haben wir die Lichtemission zur Farbakkumulation addiert. Aber jetzt tasten wir das Licht bei jedem Sprung direkt ab. Wenn wir also die Emission des Lichts hinzufügen, würden wir "doppelt eintauchen". Daher ist das Richtige, was zu tun ist ... nichts; Wir überspringen die Ansammlung der Lichtemission.
Es gibt jedoch zwei Eckfälle:
- Der erste Strahl
- Perfekt spiegelnde Bounces (auch Spiegel genannt)
Wenn der erste Strahl auf das Licht trifft, sollten Sie die Emission des Lichts direkt sehen. Wenn wir es also überspringen, werden alle Lichter schwarz angezeigt, obwohl die Oberflächen um sie herum beleuchtet sind.
Wenn Sie auf eine perfekt spiegelnde Oberfläche treffen, können Sie ein Licht nicht direkt abtasten, da ein Eingangsstrahl nur einen Ausgang hat. Nun, technisch könnten wir prüfen, ob der Eingangsstrahl auf ein Licht trifft, aber es macht keinen Sinn; Die Hauptschleife für die Pfadverfolgung wird dies sowieso tun. Wenn wir also direkt nach dem Auftreffen auf eine spiegelnde Oberfläche auf ein Licht treffen, müssen wir die Farbe akkumulieren. Andernfalls leuchten die Spiegel schwarz.
Nun wollen wir uns mit SampleLights () befassen:
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;
}
Auf Englisch:
- Schlaufe durch alle Lichter
- Überspringe das Licht, wenn wir es treffen
- Tauchen Sie nicht doppelt ein
- Sammeln Sie die direkte Beleuchtung von allen Lichtern
- Bringen Sie die direkte Beleuchtung zurück
Schließlich wertet Estimatedirect () nur aus B SD F( p , ωich, ωÖ) Lich( p , ωich)
Für pünktliche Lichtquellen ist dies einfach:
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;
}
Wenn wir jedoch möchten, dass Lichter eine Fläche haben, müssen wir zuerst einen Punkt auf dem Licht abtasten. Daher lautet die vollständige Definition:
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;
}
Wir können light-> SampleLi implementieren, wie wir wollen; wir können den Punkt einheitlich wählen oder die Stichprobe der Wichtigkeit. In beiden Fällen teilen wir die Radiosität durch das PDF, indem wir den Punkt auswählen. Auch hier gilt, die Anforderungen von Monte Carlo zu erfüllen.
Wenn die BRDF stark von der Ansicht abhängig ist, ist es möglicherweise besser, einen Punkt basierend auf der BRDF zu wählen, als einen zufälligen Punkt auf dem Licht. Aber wie wählen wir aus? Probe basierend auf dem Licht oder basierend auf dem BRDF?
B SD F( p , ωich, ωÖ) Lich( p , ωich)
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;
}
Auf Englisch:
- Zuerst probieren wir das Licht
- Dadurch wird die Interaktion.InputDirection aktualisiert
- Gibt uns das Li für das Licht
- Und das PDF zur Auswahl dieses Punktes im Licht
- Vergewissern Sie sich, dass das PDF gültig ist und der Strahlungsgrad ungleich Null ist
- Bewerten Sie die BSDF mit der abgetasteten InputDirection
- Berechnen Sie das PDF für das BSDF anhand der abgetasteten InputDirection
- Wie wahrscheinlich ist diese Stichprobe, wenn wir die BSDF anstelle des Lichts verwenden
- Berechnen Sie das Gewicht mit dem Light-PDF und dem BSDF-PDF
- Veach und Guibas definieren verschiedene Methoden zur Gewichtsberechnung. Experimentell fanden sie die Potenzheuristik mit einer Potenz von 2 für die meisten Fälle am besten. Ich verweise Sie auf das Papier für weitere Details. Die Implementierung ist unten
- Multiplizieren Sie das Gewicht mit der direkten Beleuchtungsberechnung und dividieren Sie es durch das PDF-Licht. (Für Monte Carlo) Und zur direkten Lichtakkumulation hinzufügen.
- Dann probieren wir die BRDF
- Dadurch wird die Interaktion.InputDirection aktualisiert
- Bewerten Sie die BRDF
- Holen Sie sich das PDF für die Auswahl dieser Richtung basierend auf dem BRDF
- Berechnen Sie das Light-PDF anhand der abgetasteten InputDirection
- Dies ist der Spiegel der Vergangenheit. Wie wahrscheinlich ist diese Richtung, wenn wir das Licht abtasten
- Wenn lightPdf == 0.0f, dann hat der Strahl das Licht verfehlt, also leiten Sie einfach das direkte Licht von der Lichtprobe zurück.
- Berechnen Sie andernfalls das Gewicht und addieren Sie die BSDF-Direktbeleuchtung zur Akkumulation
- Zuletzt die akkumulierte Direktbeleuchtung zurückgeben
.
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);
}
Es gibt eine Reihe von Optimierungen / Verbesserungen, die Sie an diesen Funktionen vornehmen können, aber ich habe sie reduziert, um sie verständlicher zu machen. Wenn Sie möchten, kann ich einige dieser Verbesserungen mitteilen.
Nur ein Licht abtasten
In SampleLights () durchlaufen wir alle Lichter und erhalten ihren Beitrag. Für eine kleine Anzahl von Lichtern ist dies in Ordnung, aber für Hunderte oder Tausende von Lichtern wird dies teuer. Glücklicherweise können wir die Tatsache ausnutzen, dass die Monte-Carlo-Integration ein gigantischer Durchschnitt ist. Beispiel:
Lassen Sie uns definieren
h ( x ) = f( x ) + g( x )
h ( x )
h ( x ) = 1N∑i = 1Nf( xich) + g( xich)
f( x )G( x )
h ( x ) = 1N∑i = 1Nr ( ζ, x )p df
ζr ( ζ, x )
r ( ζ, x ) = { f( x ) ,G( x ) ,0,0 ≤ ζ< 0,50,5 ≤ ζ< 1,0
p df= 12 weil das pdf zu 1 integriert werden muss und 2 Funktionen zur Auswahl stehen.
Auf Englisch:
- f( x )G( x )
- Teilen Sie das Ergebnis durch 12 (da es zwei Gegenstände gibt)
- Durchschnittlich
Wenn N groß wird, konvergiert die Schätzung zur richtigen Lösung.
Wir können dasselbe Prinzip auf die Lichtabtastung anwenden. Anstatt jedes Licht abzutasten, wählen wir zufällig eines aus und multiplizieren das Ergebnis mit der Anzahl der Lichter (Dies entspricht der Division durch das PDF-Bruchteil):
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);
}
1numLights
Mehrfache Bedeutung Abtastung der "New Ray" -Richtung
Die Bedeutung des aktuellen Codes tastet nur die Richtung "New Ray" ab, basierend auf der BSDF. Was ist, wenn wir auch eine wichtige Stichprobe basierend auf dem Ort der Lichter haben wollen?
Nach dem, was wir oben gelernt haben, besteht eine Methode darin, zu schießen zwei "neue" Strahlen und ein Gewicht jeweils auf ihren PDFS basieren. Dies ist jedoch sowohl rechenintensiv als auch schwer ohne Rekursion zu implementieren.
Um dies zu überwinden, können wir die gleichen Prinzipien anwenden, die wir durch Abtasten nur eines Lichts gelernt haben. Das heißt, wählen Sie nach dem Zufallsprinzip eine aus, die Sie probieren möchten, und dividieren Sie die Auswahl durch das PDF.
// 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;
}
Wollen wir wirklich die "New Ray" -Richtung anhand des Lichts abtasten? Bei direkter Beleuchtung wird die Radiosität sowohl vom BSDF der Oberfläche als auch von der Richtung des Lichts beeinflusst. Aber für indirekte Beleuchtung wird die Radiosität jedoch fast ausschließlich von der BSDF der zuvor getroffenen Oberfläche bestimmt. Das Hinzufügen von Lichtwertabtastungen gibt uns also nichts.
Daher ist es üblich, nur die "Neue Richtung" mit der BSDF abzutasten, aber die direkte Beleuchtung mit Multiple Wichtigkeit abzutasten.