Respuesta corta:
El muestreo de importancia es un método para reducir la varianza en la integración de Monte Carlo al elegir un estimador cercano a la forma de la función real.
PDF es una abreviatura de la función de densidad de probabilidad . A pdf(x) da la probabilidad de que una muestra aleatoria generada sea x .
Respuesta larga:
Para comenzar, repasemos qué es la integración de Monte Carlo y cómo se ve matemáticamente.
La integración de Monte Carlo es una técnica para estimar el valor de una integral. Por lo general, se usa cuando no hay una solución de forma cerrada para la integral. Se parece a esto:
∫f(x)dx≈1N∑i=1Nf(xi)pdf(xi)
En inglés, esto dice que puede aproximar una integral promediando muestras aleatorias sucesivas de la función. A medida que norte crece, la aproximación se acerca más y más a la solución. p dF( xyo) representa la función de densidad de probabilidad de cada muestra aleatoria.
Hagamos un ejemplo: Calcular el valor de la integral yo .
yo= ∫2 π0 0mi- xpecado( x ) dX
Usemos la integración de Monte Carlo:
yo≈ 1norte∑i = 1nortemi- xpecado( xyo)p dF( xyo)
Un programa simple de Python para calcular esto es:
import random
import math
N = 200000
TwoPi = 2.0 * math.pi
sum = 0.0
for i in range(N):
x = random.uniform(0, TwoPi)
fx = math.exp(-x) * math.sin(x)
pdf = 1 / (TwoPi - 0.0)
sum += fx / pdf
I = (1 / N) * sum
print(I)
Si ejecutamos el programa obtenemos yo= 0.4986941
Usando la separación por partes, podemos obtener la solución exacta:
yo= 12( 1 - e - 2 π ) = 0.4990663
Notarás que la solución Monte Carlo no es del todo correcta. Esto se debe a que es una estimación. Dicho esto, a medida que norte llega al infinito, la estimación debería acercarse cada vez más a la respuesta correcta. Ya en norte= 2000 algunas corridas son casi idénticas a la respuesta correcta.
Una nota sobre el PDF: en este sencillo ejemplo, siempre tomamos una muestra aleatoria uniforme. Una muestra aleatoria uniforme significa que cada muestra tiene exactamente la misma probabilidad de ser elegida. Tomamos muestras en el rango [ 0 , 2 π] , entonces, p dF( x ) = 1 / ( 2 π- 0 )
El muestreo de importancia funciona mediante un muestreo no uniforme. En cambio, tratamos de elegir más muestras que contribuyan mucho al resultado (importante) y menos muestras que solo contribuyan un poco al resultado (menos importante). De ahí el nombre, muestreo de importancia.
FF
Un ejemplo de muestreo importante en Path Tracing es cómo elegir la dirección de un rayo después de que golpea una superficie. Si la superficie no es perfectamente especular (es decir, un espejo o vidrio), el rayo saliente puede estar en cualquier parte del hemisferio.
Nos podíamos probar de manera uniforme el hemisferio para generar el nuevo rayo. Sin embargo, podemos explotar el hecho de que la ecuación de representación tiene un factor coseno:
Lo( p , ωo) = Lmi( p , ωo) + ∫ΩF( p , ωyo, ωo) Lyo( p , ωyo) | cosθyoEl | reωyo
cos( x )
Para combatir esto, utilizamos muestras de importancia. Si generamos rayos de acuerdo con un hemisferio ponderado de coseno, nos aseguramos de que se generen más rayos muy por encima del horizonte y menos cerca del horizonte. Esto reducirá la varianza y reducirá el ruido.
En su caso, especificó que utilizará un BRDF Cook-Torrance, basado en microfacet. La forma común es:
F( p , ωyo, ωo) = F( ωyo, h ) G ( ωyo, ωo, h ) D ( h )4 cos( θyo) cos( θo)
dónde
F( ωyo, h ) = función FresnelG ( ωyo, ωo, h ) = Función de enmascaramiento y sombreado de geometríaD ( h ) = Función de distribución normal
El blog "A Graphic's Guy's Note" tiene una excelente redacción sobre cómo probar los BRDF de Cook-Torrance. Te referiré a su blog . Dicho esto, intentaré crear una breve descripción general a continuación:
El NDF es generalmente la porción dominante del Cook-Torrance BRDF, por lo que si vamos a una muestra importante, deberíamos tomar una muestra basada en el NDF.
Cook-Torrance no especifica un NDF específico para usar; somos libres de elegir el que más le convenga. Dicho esto, hay algunos NDF populares:
Cada NDF tiene su propia fórmula, por lo tanto, cada uno debe muestrearse de manera diferente. Solo voy a mostrar la función de muestreo final para cada uno. Si desea ver cómo se deriva la fórmula, consulte la publicación del blog.
GGX se define como:
reG G X( m ) = α2π( ( α2- 1 ) cos2( θ ) + 1 )2
Para muestrear el ángulo de coordenadas esféricas θ, podemos usar la fórmula:
θ = arcos( α2ξ1( α2- 1 ) + 1------------√)
dónde ξ es una variable aleatoria uniforme
Suponemos que el FDN es isotrópico, por lo que podemos tomar muestras ϕ uniformemente
ϕ = ξ2
Beckmann se define como:
reB e c k m a n n( m ) = 1πα2cos4 4( θ )mi- bronceado2( θ )α2
Que se puede muestrear con:
θ = arcos( 11 = α2En( 1 - ξ1)--------------√)ϕ = ξ2
Por último, Blinn se define como:
reB l i n n( m ) = α + 22 π( cos( θ ) )α
Que se puede muestrear con:
θ = arcos( 1ξα + 11)ϕ = ξ2
Poniéndolo en práctica
Echemos un vistazo a un trazador de ruta básico hacia atrás:
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);
}
ES DECIR. saltamos alrededor de la escena, acumulando color y atenuación de luz a medida que avanzamos. En cada rebote, tenemos que elegir una nueva dirección para el rayo. Como se mencionó anteriormente, podríamos muestrear uniformemente el hemisferio para generar el nuevo rayo. Sin embargo, el código es más inteligente; Su importancia muestra la nueva dirección basada en el BRDF. (Nota: esta es la dirección de entrada, porque somos un trazador de ruta hacia atrás)
// 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);
Que podría implementarse como:
void LambertBRDF::Sample(float3 outputDirection, float3 normal, UniformSampler *sampler) {
float rand = sampler->NextFloat();
float r = std::sqrtf(rand);
float theta = sampler->NextFloat() * 2.0f * M_PI;
float x = r * std::cosf(theta);
float y = r * std::sinf(theta);
// Project z up to the unit hemisphere
float z = std::sqrtf(1.0f - x * x - y * y);
return normalize(TransformToWorld(x, y, z, normal));
}
float3a TransformToWorld(float x, float y, float z, float3a &normal) {
// Find an axis that is not parallel to normal
float3a majorAxis;
if (abs(normal.x) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(1, 0, 0);
} else if (abs(normal.y) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(0, 1, 0);
} else {
majorAxis = float3a(0, 0, 1);
}
// Use majorAxis to create a coordinate system relative to world space
float3a u = normalize(cross(normal, majorAxis));
float3a v = cross(normal, u);
float3a w = normal;
// Transform from local coordinates to world coordinates
return u * x +
v * y +
w * z;
}
float LambertBRDF::Pdf(float3 inputDirection, float3 normal) {
return dot(inputDirection, normal) * M_1_PI;
}
Después de muestrear inputDirection ('wi' en el código), lo usamos para calcular el valor de BRDF. Y luego dividimos por el pdf según la fórmula de Monte Carlo:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Donde Eval () es solo la función BRDF en sí misma (Lambert, Blinn-Phong, Cook-Torrance, etc.):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}