Resposta curta:
A amostragem por importância é um método para reduzir a variação na integração de Monte Carlo, escolhendo um estimador próximo ao formato da função real.
PDF é uma abreviação de Probability Density Function . Um fornece a probabilidade de uma amostra aleatória gerada ser .pdf( X )x
Resposta longa:
Para começar, vamos revisar o que é a Integração Monte Carlo e como ela é matematicamente.
Integração Monte Carlo é uma técnica para estimar o valor de uma integral. É normalmente usado quando não há uma solução de formulário fechado para a integral. Se parece com isso:
∫f( X )dx ≈ 1N∑i = 1Nf( xEu)p df( xEu)
Em inglês, isso indica que você pode aproximar uma integral calculando a média de amostras aleatórias sucessivas da função. À medida que aumenta, a aproximação se aproxima cada vez mais da solução. representa a função de densidade de probabilidade de cada amostra aleatória.Npdf( xEu)
Vamos fazer um exemplo: calcula o valor da integral .Eu
Eu= ∫2 π0 0e- xpecado( x ) dx
Vamos usar a integração Monte Carlo:
Eu≈ 1N∑i = 1Ne- xpecado( xEu)p df( xEu)
Um programa python simples para calcular isso é:
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)
Se o programa, obtemosEu= 0,4986941
Usando a separação por partes, podemos obter a solução exata:
Eu= 12( 1 - e - 2 π ) = 0,4990663
Você notará que a solução de Monte Carlo não está correta. Isso ocorre porque é uma estimativa. Dito isto, como chega ao infinito, a estimativa deve se aproximar cada vez mais da resposta correta. Já em algumas execuções são quase idênticas à resposta correta.NN= 2000
Uma observação sobre o PDF: neste exemplo simples, sempre coletamos uma amostra aleatória uniforme. Uma amostra aleatória uniforme significa que toda amostra tem exatamente a mesma probabilidade de ser escolhida. Amostramos no intervalo , portanto,[ 0 , 2 π]p df( x ) = 1 / ( 2 π- 0 )
A amostragem de importância funciona por amostragem não uniforme. Em vez disso, tentamos escolher mais amostras que contribuam muito para o resultado (importante) e menos amostras que contribuem apenas um pouco para o resultado (menos importante). Daí o nome, amostragem importante.
Se você escolher uma função de amostragem cujo pdf corresponda muito à forma de , poderá reduzir bastante a variação, o que significa que você pode colher menos amostras. No entanto, se você escolher uma função de amostragem cujo valor seja muito diferente de , poderá aumentar a variação. Veja a figura abaixo:
Imagem da dissertação de Wojciech Jarosz Apêndice Aff
Um exemplo de amostragem de importância no Path Tracing é como escolher a direção de um raio depois que ele atinge uma superfície. Se a superfície não for perfeitamente especular (isto é, um espelho ou vidro), o raio que sai pode estar em qualquer lugar do hemisfério.
Nós poderia uniformemente provar o hemisfério para gerar o novo raio. No entanto, podemos explorar o fato de que a equação de renderização tem um fator cosseno:
euo( p , ωo) = Le( p , ωo) + ∫Ωf( p , ωEu, ωo) LEu( p , ωEu) | porqueθEu| dωEu
Especificamente, sabemos que quaisquer raios no horizonte serão fortemente atenuados (especificamente, ). Portanto, os raios gerados perto do horizonte não contribuirão muito para o valor final.porque( X )
Para combater isso, usamos amostragem importante. Se gerarmos raios de acordo com um hemisfério ponderado por cosseno, garantiremos que mais raios sejam gerados bem acima do horizonte e menos próximos do horizonte. Isso reduzirá a variação e reduzirá o ruído.
No seu caso, você especificou que usará um BRDF baseado em microfacet da Cook-Torrance. A forma comum é:
f( p , ωEu, ωo) = F( ωEu, H ) L ( ωEu, ωo, H ) D ( h )4 cos( θEu) cos( θo)
Onde
F( ωEu, h ) = função FresnelG ( ωEu, ωo, h ) = Função de máscara e sombreamento da geometriaD ( h ) = Função de distribuição normal
O blog "A nota de um gráfico" tem um excelente artigo sobre como experimentar os BRDFs da Cook-Torrance. Vou encaminhá-lo para o seu blog . Dito isto, tentarei criar uma breve visão geral abaixo:
O NDF é geralmente a porção dominante do BRDF de Cook-Torrance; portanto, se formos amostrar uma amostra importante, devemos amostrar com base no NDF.
Cook-Torrance não especifica um NDF específico para usar; somos livres para escolher o que melhor se adequar à nossa fantasia. Dito isto, existem alguns NDFs populares:
Cada NDF possui sua própria fórmula, portanto cada uma deve ser amostrada de maneira diferente. Eu só vou mostrar a função de amostragem final para cada um. Se você gostaria de ver como a fórmula é derivada, consulte a postagem do blog.
GGX é definido como:
DG G X( m ) = α2π( ( α2- 1 ) cos2( θ ) + 1 )2
Para amostrar o ângulo das coordenadas esféricas , podemos usar a fórmula:θ
θ = arccos( α2ξ1( α2- 1 ) + 1------------√)
onde é uma variável aleatória uniforme.ξ
Assumimos que o NDF é isotrópico, para que possamos amostrar uniformemente:ϕ
ϕ = ξ2
Beckmann é definido como:
DB e c k m a n n( m ) = 1πα2porque4( θ )e- bronzeado2( θ )α2
Que pode ser amostrado com:
θ = arccos( 11 = α2em( 1 - ξ1)--------------√)ϕ = ξ2
Por fim, Blinn é definido como:
DB l i n n( m ) = α + 22 π( cos( θ ) )α
Que pode ser amostrado com:
θ = arccos( 1ξα + 11)ϕ = ξ2
Colocando em prática
Vejamos um rastreador básico de caminho para trá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);
}
IE. pulamos pela cena, acumulando cores e atenuação de luz à medida que avançamos. A cada salto, temos que escolher uma nova direção para o raio. Como mencionado acima, poderíamos amostrar uniformemente o hemisfério para gerar o novo raio. No entanto, o código é mais inteligente; A importância mostra a nova direção com base no BRDF. (Nota: Esta é a direção de entrada, porque somos um rastreador de caminho para trá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);
O que poderia ser implementado 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;
}
Depois de provarmos o inputDirection ('wi' no código), usamos isso para calcular o valor do BRDF. E então dividimos pelo pdf de acordo com a fórmula de Monte Carlo:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Onde Eval () é apenas a própria função BRDF (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);
}