Wie in den Kommentaren erwähnt, würde ich dringend empfehlen, mit der vollständigen volumetrischen Streuung zu beginnen. Dies ist zweifach:
- Da Sie die Pfadverfolgung durchführen, ist das Hinzufügen von Volumetrics nicht besonders schwierig.
- Das vollständige Verständnis der Funktionsweise der vollständigen volumetrischen Streuung ist eine gute Grundlage für das Verständnis der Schätzungen. Darüber hinaus kann es hervorragende "Referenzen" liefern, um festzustellen, ob Ihre Schätzungen gut / korrekt funktionieren.
In diesem Sinne finden Sie im Folgenden eine grundlegende Einführung in die Implementierung der vollständigen volumetrischen Streuung in einem Rückwärtspfad-Tracer.
Schauen wir uns zunächst den Code für einen Rückwärtspfad-Tracer an, der nur Reflexion und keine Transmission / Refraktion enthält:
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);
}
Auf Englisch:
- Schieße einen Strahl durch die Szene.
- Überprüfen Sie, ob wir etwas getroffen haben. Wenn nicht, geben wir die Skybox-Farbe zurück und brechen ab.
- Wenn dies der erste Strahl ist oder wir gerade von einer spiegelnden Oberfläche abprallen, prüfen Sie, ob wir auf ein Licht treffen. In diesem Fall addieren wir die Lichtemission zu unserer Farbakkumulation.
- Probieren Sie die direkte Beleuchtung.
- Wählen Sie eine neue Richtung für den nächsten Strahl. Wir können dies einheitlich tun oder eine wichtige Stichprobe basierend auf dem BRDF.
- Bewerten Sie das BRDF und akkumulieren Sie es.
- Erstellen Sie einen neuen Strahl basierend auf unserer gewählten Richtung und woher wir gerade gekommen sind.
- [Optional] Verwenden Sie russisches Roulette, um zu entscheiden, ob der Strahl beendet werden soll.
- Gehe zu 1.
Weitere Informationen zur direkten Lichtabtastung finden Sie in dieser Antwort .
BSDFs
Um die Übertragung hinzuzufügen, müssen wir zuerst einige unserer BRDF-Materialien auf vollständige BSDFs mit einer BTDF-Funktion aktualisieren.
Zum Beispiel ist ein BSDF für ein ideales spiegelndes Dielektrikum (dh Glas):
class IdealSpecularDielectric : public BSDF {
public:
IdealSpecularDielectric(float3 albedo, float ior)
: BSDF(BSDFLobe::Specular, albedo),
m_ior(ior) {
}
private:
float m_ior;
public:
float3 Eval(SurfaceInteraction &interaction) const override {
return m_albedo;
}
void Sample(SurfaceInteraction &interaction, UniformSampler *sampler) const override {
float VdotN = dot(interaction.OutputDirection, interaction.Normal);
float IORo = m_ior;
if (VdotN < 0.0f) {
IORo = 1.0f;
interaction.Normal = -interaction.Normal;
VdotN = -VdotN;
}
float eta = interaction.IORi / IORo;
float sinSquaredThetaT = SinSquaredThetaT(VdotN, eta);
float fresnel = Fresnel(interaction.IORi, IORo, VdotN, sinSquaredThetaT);
float rand = sampler->NextFloat();
if (rand <= fresnel) {
// Reflect
interaction.InputDirection = reflect(interaction.OutputDirection, interaction.Normal);
interaction.SampledLobe = BSDFLobe::SpecularReflection;
interaction.IORo = interaction.IORi;
} else {
// Refract
interaction.InputDirection = refract(interaction.OutputDirection, interaction.Normal, VdotN, eta, sinSquaredThetaT);
interaction.SampledLobe = BSDFLobe::SpecularTransmission;
interaction.IORo = IORo;
}
if (AnyNan(interaction.InputDirection)) {
printf("nan");
}
}
float Pdf(SurfaceInteraction &interaction) const override {
return 1.0f;
}
};
Der interessante Teil des Codes ist Sample()
. Hier entscheidet ein Strahl, ob er reflektiert oder gebrochen wird. Wie wir das machen, liegt wirklich bei uns, wie in dieser Antwort gezeigt . Eine naheliegende Wahl wäre jedoch eine Stichprobe auf der Grundlage der Fresnel-Gleichungen .
Im Fall eines idealen Spiegeldielektrikums gibt es nur zwei mögliche Ausgangsrichtungen: perfekte Brechung oder perfekte Reflexion. Also bewerten wir den Fresnel und wählen zufällig die Brechung / Reflexion mit einem Anteil, der dem Fresnel entspricht.
Medien
Als nächstes müssen wir über Medien sprechen und wie sie sich auf das herumprallende Licht auswirken. Wie von Nathan Reed in dieser Antwort erklärt :
Ich denke gerne über Volumenstreuung nach, dass ein Photon, das sich durch ein Medium bewegt, eine bestimmte Wahrscheinlichkeit pro Längeneinheit der Wechselwirkung hat (gestreut oder absorbiert wird). Solange es nicht interagiert, verläuft es ungehindert und ohne Energieverlust in einer geraden Linie. Je größer die Entfernung ist, desto größer ist die Wahrscheinlichkeit, dass sie irgendwo in dieser Entfernung interagiert. Die Interaktionswahrscheinlichkeit pro Längeneinheit ist der Koeffizient , den Sie in den Gleichungen sehen. Wir haben normalerweise separate Koeffizienten für Streu- und Absorptionswahrscheinlichkeiten, alsoσσ=σs+σein
Diese Wahrscheinlichkeit pro Längeneinheit ist genau der Ursprung des Beer-Lambert-Gesetzes. Schneiden Sie ein Strahlensegment in infinitesimale Intervalle, behandeln Sie jedes Intervall als einen unabhängigen möglichen Ort für die Interaktion und integrieren Sie es dann entlang des Strahls. Sie erhalten eine Exponentialverteilung (mit dem Ratenparameter σσ) für die Wahrscheinlichkeit der Wechselwirkung als Funktion der Entfernung.
Sie können den Abstand zwischen Ereignissen technisch wählen, wie Sie möchten, solange Sie den Pfad korrekt gewichten, um die Wahrscheinlichkeit zu ermitteln, dass ein Photon es zwischen zwei benachbarten Ereignissen schaffen kann, ohne mit dem Medium zu interagieren. Mit anderen Worten, jedes Pfadsegment innerhalb des Mediums trägt einen Gewichtsfaktor von , wobei die Länge des Segments ist.e- σxx
In Anbetracht dessen ist es normalerweise eine gute Wahl für die Entfernung, sie aus der Exponentialverteilung zu ermitteln. Mit anderen Worten, Sie setzenx = - ( lnξ) / σ
Zusammenfassend:
- Ein Strahl, der sich durch ein Medium bewegt, hat eine gewisse Wahrscheinlichkeit:
- Sei absorbiert
- Sei verstreut
- Da dies die Monte-Carlo-Integration ist, können wir den Interaktionsabstand frei wählen, wie wir möchten. Eine gute Wahl ist jedoch die Wichtigkeit einer Stichprobe aus einer Exponentialverteilung ähnlich wie bei Beer-Lambert.
Ich entschied mich dafür, dies mit einem Streukoeffizienten zu implementieren und Beer-Lambert den Absorptionskoeffizienten zu überlassen.
class Medium {
public:
Medium(float3 absorptionColor, float absorptionAtDistance)
: m_absorptionCoefficient(-log(absorptionColor) / absorptionAtDistance) {
// This method for calculating the absorption coefficient is borrowed from Burley's 2015 Siggraph Course Notes "Extending the Disney BRDF to a BSDF with Integrated Subsurface Scattering"
// It's much more intutive to specify a color and a distance, then back-calculate the coefficient
}
virtual ~Medium() {
}
protected:
const float3a m_absorptionCoefficient;
public:
virtual float SampleDistance(UniformSampler *sampler, float tFar, float *weight, float *pdf) const = 0;
virtual float3a SampleScatterDirection(UniformSampler *sampler, float3a &wo, float *pdf) const = 0;
virtual float ScatterDirectionPdf(float3a &wi, float3a &wo) const = 0;
virtual float3 Transmission(float distance) const = 0;
};
Ein nicht streuendes Medium ist ein Medium, durch das sich ein Photon gerade bewegt, das jedoch nach Beer-Lambert auf dem Weg abgeschwächt wird:
class NonScatteringMedium : public Medium {
public:
NonScatteringMedium(float3 color, float atDistance)
: Medium(color, atDistance) {
}
public:
float SampleDistance(UniformSampler *sampler, float tFar, float *weight, float *pdf) const override {
*pdf = 1.0f;
return tFar;
}
float3a SampleScatterDirection(UniformSampler *sampler, float3a &wo, float *pdf) const override {
return wo;
}
float ScatterDirectionPdf(float3a &wi, float3a &wo) const override {
return 1.0f;
}
float3 Transmission(float distance) const override {
return exp(-m_absorptionCoefficient * distance);
}
};
Ein isotropes Streumedium ermöglicht es dem Strahl, Streuereignisse (Reflexionsereignisse) zu haben, während der Strahl durch ihn wandert. Der Strahl kann in jeder Richtung in der Kugel um den Wechselwirkungspunkt reflektiert werden (daher isotrop).
Der Abstand zwischen Reflexionen ist zufällig, basierend auf einer exponentiellen "Streu" -Wahrscheinlichkeit.
class IsotropicScatteringMedium : public Medium {
public:
IsotropicScatteringMedium(float3 absorptionColor, float absorptionAtDistance, float scatteringDistance)
: Medium(absorptionColor, absorptionAtDistance),
m_scatteringCoefficient(1 / scatteringDistance) {
}
private:
float m_scatteringCoefficient;
public:
float SampleDistance(UniformSampler *sampler, float tFar, float *weight, float *pdf) const override {
float distance = -logf(sampler->NextFloat()) / m_scatteringCoefficient;
// If we sample a distance farther than the next intersecting surface, clamp to the surface distance
if (distance >= tFar) {
*pdf = 1.0f;
return tFar;
}
*pdf = std::exp(-m_scatteringCoefficient * distance);
return distance;
}
float3a SampleScatterDirection(UniformSampler *sampler, float3a &wo, float *pdf) const override {
*pdf = 0.25f * M_1_PI; // 1 / (4 * PI)
return UniformSampleSphere(sampler);
}
float ScatterDirectionPdf(float3a &wi, float3a &wo) const override {
return 0.25f * M_1_PI; // 1 / (4 * PI)
}
float3 Transmission(float distance) const override {
return exp(-m_absorptionCoefficient * distance);
}
};
Bei hohen Streuabständen verhält sich das Material fast wie ein NonScatteringMedium, da es eine sehr geringe Streuwahrscheinlichkeit aufweist, bevor es sich durch das Medium bewegt.
Bei geringen Streuabständen verhält sich das Material wie Wachs oder Jade.
Wenn der Streuabstand immer kleiner wird, haben wir eine immer höhere Wahrscheinlichkeit, auf dem Weg durch das Medium zu streuen. Dies wirft einige Probleme auf:
- Die Anzahl der Bounces pro Strahl wird explodieren.
- Wenn russisches Roulette den Strahl tötet oder wenn Sie "maxBounces" überschreiten, "verlieren" Sie jeden Farbbeitrag aus dem Medium heraus.
Daher müssen Sie maxBounces auf eine hohe Zahl einstellen und sich im allgemeinen Fall auf russisches Roulette verlassen, um Strahlen zu beenden. Darüber hinaus ist IsotropicScatteringMedium für die Darstellung meist undurchsichtiger Materialien äußerst ineffizient. Um eine bessere Leistung zu erzielen, sollten wir eine nicht-isotrope Phasenfunktion wie Henyey-Greenstein oder Schlick verwenden . Diese spannen die Streuung in eine bestimmte Richtung vor. So könnten wir sie auf eine hohe Rückstreuung einstellen, wodurch das Problem der "verlorenen" Strahlen verringert wird.
Alles zusammenfügen
Wie modifizieren wir mit diesen neuen Informationen den Integrator, um BSDFs und Medien zu verstehen?
Zuerst müssen wir verfolgen, in welchem Medium wir uns gerade befinden und welchen Brechungsindex (IOR).
SurfaceInteraction interaction;
interaction.IORi = 1.0f; // Vacuum
Medium *medium = nullptr; // nullptr == vacuum
Dann teilen wir den Integrator in zwei Teile: Übertragung und Materialinteraktion.
void RenderPixel(uint x, uint y, UniformSampler *sampler) const {
Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
SurfaceInteraction interaction;
interaction.IORi = 1.0f; // Air
Medium *medium = nullptr;
bool hitSurface = false;
// Bounce the ray around the scene
uint bounces = 0;
const uint maxBounces = 1500;
for (; 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;
}
// We hit an object
hitSurface = true;
// Calculate any transmission
if (medium != nullptr) {
float weight = 1.0f;
float pdf = 1.0f;
float distance = medium->SampleDistance(sampler, ray.TFar, &weight, &pdf);
float3 transmission = medium->Transmission(distance);
throughput = throughput * weight * transmission;
if (distance < ray.TFar) {
// Create a scatter event
hitSurface = false;
ray.Origin = ray.Origin + ray.Direction * distance;
// Reset the other ray properties
float directionPdf;
float3a wo = normalize(ray.Direction);
ray.Direction = medium->SampleScatterDirection(sampler, wo, &directionPdf);
ray.TNear = 0.001f;
ray.TFar = infinity;
ray.GeomID = INVALID_GEOMETRY_ID;
ray.PrimID = INVALID_PRIMATIVE_ID;
ray.InstID = INVALID_INSTANCE_ID;
ray.Mask = 0xFFFFFFFF;
ray.Time = 0.0f;
}
}
if (hitSurface) {
// 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);
interaction.IORo = 0.0f;
// Calculate the direct lighting
color += throughput * SampleOneLight(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;
// Update the current IOR and medium if we refracted
if (interaction.SampledLobe == BSDFLobe::SpecularTransmission) {
interaction.IORi = interaction.IORo;
medium = material->medium;
}
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
if (AnyNan(ray.Direction))
printf("bad");
ray.TNear = 0.001f;
ray.TFar = infinity;
ray.GeomID = INVALID_GEOMETRY_ID;
ray.PrimID = INVALID_PRIMATIVE_ID;
ray.InstID = INVALID_INSTANCE_ID;
ray.Mask = 0xFFFFFFFF;
ray.Time = 0.0f;
}
// 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;
}
}
if (bounces == maxBounces) {
printf("Over max bounces");
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
Auf Englisch:
- Schieße einen Strahl durch die Szene.
- Überprüfen Sie, ob wir etwas getroffen haben. Wenn nicht, geben wir die Skybox-Farbe zurück und brechen ab.
- Überprüfen Sie, ob wir uns derzeit in einem Medium befinden (nicht in einem Vakuum).
- Probieren Sie das Medium für eine Entfernung.
- Bewerten Sie die Übertragung des Mediums und akkumulieren Sie den Durchsatz.
- Wenn der Streuabstand kleiner als der Abstand vom Strahlursprung zur nächsten Oberfläche ist, führen Sie ein Streuereignis durch.
- Probieren Sie das Medium für eine Streurichtung.
- Überprüfen Sie, ob wir auf eine Oberfläche treffen (dh wir hatten kein Streuereignis).
- Wenn dies der erste Strahl ist oder wir gerade von einer spiegelnden Oberfläche abprallen, prüfen Sie, ob wir auf ein Licht treffen. In diesem Fall addieren wir die Lichtemission zu unserer Farbakkumulation.
- Probieren Sie die direkte Beleuchtung.
- Wählen Sie eine neue Richtung für den nächsten Strahl. Wir können dies einheitlich tun oder eine wichtige Stichprobe basierend auf dem BRDF.
- Bewerten Sie das BRDF und akkumulieren Sie es.
- Wenn wir das Material gebrochen haben, aktualisieren Sie die IOR und das Medium für den nächsten Sprung.
- Erstellen Sie einen neuen Strahl basierend auf unserer gewählten Richtung und woher wir gerade gekommen sind.
- [Optional] Verwenden Sie russisches Roulette, um zu entscheiden, ob der Strahl beendet werden soll.
- Gehe zu 1.