W śledzeniu ścieżki istnieje wiele obszarów, które można próbkować pod kątem ważności. Ponadto każdy z tych obszarów może również wykorzystywać próbkowanie wielokrotnego znaczenia, zaproponowane po raz pierwszy w pracy Veacha i Guibasa z 1995 r . Aby lepiej to wyjaśnić, spójrzmy na znacznik ścieżki wstecznej:
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);
}
Po angielsku:
- Wystrzel promień po scenie
- Sprawdź, czy coś trafiliśmy. Jeśli nie, zwracamy kolor skybox i psujemy się.
- Sprawdź, czy trafiliśmy w światło. Jeśli tak, dodajemy emisję światła do naszej akumulacji kolorów
- Wybierz nowy kierunek dla następnego promienia. Możemy to zrobić w sposób jednolity lub ważny na podstawie BRDF
- Ocenić BRDF i zgromadzić go. Tutaj musimy podzielić przez pdf naszego wybranego kierunku, aby postępować zgodnie z algorytmem Monte Carlo.
- Utwórz nowy promień na podstawie naszego wybranego kierunku i miejsca, z którego właśnie przybyliśmy
- [Opcjonalnie] Użyj rosyjskiej ruletki, aby wybrać, czy mamy zakończyć promień
- Idź 1
Dzięki temu kodowi uzyskujemy kolor tylko wtedy, gdy promień ostatecznie trafi na światło. Ponadto nie obsługuje punktowych źródeł światła, ponieważ nie mają one obszaru.
Aby to naprawić, próbkujemy światła bezpośrednio przy każdym odbiciu. Musimy zrobić kilka drobnych zmian:
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);
}
Najpierw dodajemy „kolor + = przepustowość * SampleLights (...)”. Za chwilę zajmę się szczegółami na temat SampleLights (). Ale w zasadzie zapętla wszystkie światła i zwraca swój udział w kolorze, osłabionym przez BSDF.
To świetnie, ale musimy wprowadzić jeszcze jedną zmianę, aby była poprawna; w szczególności, co dzieje się, gdy trafimy w światło. W starym kodzie dodaliśmy emisję światła do akumulacji kolorów. Ale teraz bezpośrednio odbijamy światło przy każdym odbiciu, więc jeśli dodamy emisję światła, „podwójnie zanurzymy”. Dlatego właściwą rzeczą jest… nic; pomijamy akumulację światła.
Istnieją jednak dwa narożne przypadki:
- Pierwszy promień
- Idealnie odbijane odbicia (inaczej lustra)
Jeśli pierwszy promień trafi w światło, powinieneś bezpośrednio zobaczyć jego emisję. Więc jeśli go pomińmy, wszystkie światła pojawią się jako czarne, mimo że powierzchnie wokół nich są oświetlone.
Kiedy uderzasz w idealnie błyszczące powierzchnie, nie możesz bezpośrednio próbkować światła, ponieważ promień wejściowy ma tylko jedno wyjście. Technicznie moglibyśmy sprawdzić, czy promień wejściowy uderzy w światło, ale nie ma sensu; główna pętla śledzenia ścieżki i tak to zrobi. Dlatego jeśli uderzymy w światło tuż po tym, jak uderzymy w lustrzaną powierzchnię, musimy zgromadzić kolor. Jeśli tego nie zrobimy, światła będą czarne w lusterkach.
Teraz zagłębimy się w 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;
}
Po angielsku:
- Zapętlić wszystkie światła
- Pomiń światło, jeśli go uderzymy
- Nie podwójnie zanurzaj się
- Zbierz bezpośrednie oświetlenie ze wszystkich świateł
- Zwróć bezpośrednie oświetlenie
Wreszcie, EstimateDirect () właśnie oceniaB S.D F.( p , ωja, ωo) L.ja( p , ωja)
W przypadku punktowych źródeł światła jest to proste:
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;
}
Jeśli jednak chcemy, aby światła miały powierzchnię, najpierw musimy wypróbować punkt na świetle. Dlatego pełna definicja to:
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;
}
Możemy wdrożyć light-> SampleLi, jak chcemy; możemy wybrać punkt równomiernie lub próbkę ważności. W obu przypadkach dzielimy radość przez pdf wyboru punktu. Ponownie, aby spełnić wymagania Monte Carlo.
Jeśli BRDF jest silnie zależny od widoku, może być lepszy wybór punktu opartego na BRDF, zamiast losowego punktu na świetle. Ale jak wybieramy? Próbka oparta na świetle, czy oparta na BRDF?
Dlaczego nie oba? Wprowadź próbkowanie wielokrotnego znaczenia. W skrócie, oceniamy wiele razy, przy użyciu różnych technik próbkowania, a następnie uśrednij je razem, używając wag opartych na ich plikach pdf. W kodzie jest to:B S.D F.( p , ωja, ωo) L.ja( p , ωja)
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;
}
Po angielsku:
- Najpierw próbkujemy światło
- To aktualizuje interakcję.InputDirection
- Daje nam Li za światło
- I pdf wyboru tego punktu na światło
- Sprawdź, czy plik pdf jest prawidłowy, a promienność jest różna od zera
- Oszacuj BSDF za pomocą próbkowanego InputDirection
- Oblicz pdf dla BSDF, biorąc pod uwagę próbkowany InputDirection
- Zasadniczo, jakie jest prawdopodobieństwo tej próbki, gdybyśmy próbkowali przy użyciu BSDF zamiast światła
- Oblicz wagę, używając lekkiego pdf i BSDF pdf
- Veach i Guibas określają kilka różnych sposobów obliczania masy. Eksperymentalnie odkryli, że siła heurystyczna o mocy 2 działa najlepiej w większości przypadków. Więcej szczegółów odsyłam do artykułu. Implementacja jest poniżej
- Pomnóż wagę przez bezpośrednie obliczenie oświetlenia i podziel przez światło pdf. (Dla Monte Carlo) I dodaj do bezpośredniej akumulacji światła.
- Następnie próbkujemy BRDF
- To aktualizuje interakcję.InputDirection
- Oceń BRDF
- Pobierz pdf, aby wybrać ten kierunek na podstawie BRDF
- Oblicz lekki pdf, biorąc pod uwagę próbkowany InputDirection
- To jest lustro wcześniej. Jak prawdopodobny jest ten kierunek, gdybyśmy próbkowali światło
- Jeśli lightPdf == 0,0f, promień nie trafił w światło, więc po prostu zwróć bezpośrednie oświetlenie z próbki światła.
- W przeciwnym razie obliczyć masę i dodać do akumulacji bezpośrednie oświetlenie BSDF
- Na koniec zwróć zgromadzone bezpośrednie oświetlenie
.
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);
}
Istnieje wiele optymalizacji / ulepszeń, które możesz zrobić w tych funkcjach, ale zmniejszyłem je, aby ułatwić ich zrozumienie. Jeśli chcesz, mogę udostępnić niektóre z tych ulepszeń.
Próbkowanie tylko jednego światła
W SampleLights () przeglądamy wszystkie światła i otrzymujemy ich wkład. W przypadku niewielkiej liczby świateł jest to w porządku, ale w przypadku setek lub tysięcy świateł staje się to drogie. Na szczęście możemy wykorzystać fakt, że integracja Monte Carlo jest gigantyczną średnią. Przykład:
Zdefiniujmy
h ( x ) = f( x ) + g( x )
Obecnie szacujemy przez:h(x)
h(x)=1N∑i=1Nf(xi)+g(xi)
Ale obliczenia zarówno i jest drogie, więc zamiast tego zrobić:f(x)g(x)
h(x)=1N∑i=1Nr(ζ,x)pdf
Gdzie jest jednolitą zmienną losową, a jest zdefiniowane jako:ζr(ζ,x)
r(ζ,x)={f(x),g(x),0.0≤ζ<0.50.5≤ζ<1.0
W tym przypadku ponieważ pdf musi być zintegrowany z 1, a do wyboru są 2 funkcje.pdf=12
Po angielsku:
- Losowo wybierz lub do oceny.g ( x )f(x)g(x)
- Podziel wynik przez (ponieważ istnieją dwa elementy)12
- Średni
Gdy N staje się duże, oszacowanie zbiegnie się z właściwym rozwiązaniem.
Możemy zastosować tę samą zasadę do próbkowania światła. Zamiast próbkować każde światło, losowo wybieramy jedno i mnożymy wynik przez liczbę świateł (jest to to samo, co dzielenie przez ułamek pdf):
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);
}
W tym kodzie wszystkie światła mają jednakową szansę na wybranie. Jeśli jednak chcemy, możemy zrobić próbkę. Na przykład, możemy dać większym światłom większą szansę na wybranie lub światła bliżej powierzchni uderzenia. Musisz tylko podzielić wynik przez pdf, który nie byłby już .1numLights
Wielokrotne znaczenie Próbkowanie w kierunku „New Ray”
Obecny kod ma znaczenie tylko próbki kierunku „New Ray” na podstawie BSDF. Co zrobić, jeśli chcemy również podkreślić próbkę na podstawie położenia świateł?
Biorąc z tego, czego się nauczyliśmy powyżej, jedną z metod byłoby strzelenie dwoma „nowymi” promieniami i wagą na podstawie ich plików pdf. Jest to jednak zarówno kosztowne obliczeniowo, jak i trudne do wdrożenia bez rekurencji.
Aby temu zaradzić, możemy zastosować te same zasady, których się nauczyliśmy, próbkując tylko jedno światło. Oznacza to, że losowo wybierz próbkę i podziel przez pdf wyboru.
// 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;
}
To powiedziawszy, czy naprawdę chcemy naświetlić próbkę kierunku „New Ray” w oparciu o światło? W przypadku bezpośredniego oświetlenia na radość wpływa zarówno BSDF powierzchni, jak i kierunek światła. Ale w przypadku oświetlenia pośredniego radość jest prawie wyłącznie określona przez BSDF wcześniej uderzanej powierzchni. Tak więc dodanie lekkości próbkowania nie daje nam niczego.
Dlatego powszechne jest, aby próbkować tylko „Nowy kierunek” za pomocą BSDF, ale stosować próbkowanie wielokrotnego znaczenia do bezpośredniego oświetlenia.