Réponse courte:
Échantillonnage d'importance est une méthode pour réduire la variance dans l'intégration de Monte Carlo en choisissant un estimateur proche de la forme de la fonction réelle.
PDF est l'abréviation de Probability Density Function . A pdf(x) donne la probabilité qu'un échantillon aléatoire soit généré x .
Longue réponse:
Pour commencer, examinons ce qu'est l'intégration de Monte Carlo et à quoi elle ressemble mathématiquement.
L’intégration de Monte Carlo est une technique permettant d’estimer la valeur d’une intégrale. Il est généralement utilisé lorsqu'il n'y a pas de solution de forme fermée à l'intégrale. Cela ressemble à ceci:
∫f(x)dx≈1N∑i=1Nf(xi)pdf(xi)
En anglais, cela signifie que vous pouvez approximer une intégrale en faisant la moyenne des échantillons aléatoires successifs de la fonction. Lorsque N devient grand, l’approximation se rapproche de plus en plus de la solution. pdf(xi) représente la fonction de densité de probabilité de chaque échantillon aléatoire.
Faisons un exemple: Calculer la valeur de l'intégrale I .
I=∫2π0e−xsin(x)dx
Utilisons Monte Carlo Integration:
I≈1N∑i=1Ne−xsin(xi)pdf(xi)
Un programme python simple pour calculer ceci est:
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 on lance le programme on obtient I=0.4986941
En utilisant la séparation par pièces, nous pouvons obtenir la solution exacte:
I=12(1−e−2π)=0.4990663
NN=2000
[0,2π]pdf(x)=1/(2π−0)
L’échantillonnage d’importance ne fonctionne pas n'échantillant uniformément. Au lieu de cela, nous essayons de choisir plus d’échantillons qui contribuent beaucoup au résultat (important) et moins d’échantillons qui contribuent peu au résultat (moins important). D'où le nom, l'importance d'échantillonnage.
ff
Un exemple d’échantillonnage d’importance dans le suivi de trajectoire est la manière de choisir la direction d’un rayon après qu’il atteigne une surface. Si la surface n'est pas parfaitement spéculaire (miroir ou verre), le rayon sortant peut se trouver n'importe où dans l'hémisphère.
Nous pourrions échantillonner uniformément l'hémisphère pour générer le nouveau rayon. Cependant, nous pouvons exploiter le fait que l’équation de rendu a un facteur de cosinus:
Lo( P , ωo) = Le( P , ωo) + ∫ΩF( P , ωje, ωo) Lje( P , ωje) | cosθje| réωje
Plus précisément, nous savons que tous les rayons à l’horizon seront fortement atténués (en particulier, cos( x )). Ainsi, les rayons générés près de l'horizon ne contribueront pas beaucoup à la valeur finale.
To combat this, we use importance sampling. If we generate rays according to a cosine weighted hemisphere, we ensure that more rays are generated well above the horizon, and less near the horizon. This will lower variance and reduce noise.
In your case, you specified that you will be using a Cook-Torrance, microfacet-based BRDF. The common form being:
f(p,ωi,ωo)=F(ωi,h)G(ωi,ωo,h)D(h)4cos(θi)cos(θo)
where
F(ωi,h)=Fresnel functionG(ωi,ωo,h)=Geometry Masking and Shadowing functionD(h)=Normal Distribution Function
The blog "A Graphic's Guy's Note" has an excellent write up on how to sample Cook-Torrance BRDFs. I will refer you to his blog post. That said, I will try to create a brief overview below:
The NDF is generally the dominant portion of the Cook-Torrance BRDF, so if we are going to importance sample, the we should sample based on the NDF.
Cook-Torrance doesn't specify a specific NDF to use; we are free to choose whichever one suits our fancy. That said, there are a few popular NDFs:
Each NDF has it's own formula, thus each must be sampled differently. I am only going to show the final sampling function for each. If you would like to see how the formula is derived, see the blog post.
GGX is defined as:
DGGX(m)=α2π((α2−1)cos2(θ)+1)2
To sample the spherical coordinates angle θ, we can use the formula:
θ=arccos(α2ξ1(α2−1)+1−−−−−−−−−−−−√)
where ξ is a uniform random variable.
We assume that the NDF is isotropic, so we can sample ϕ uniformly:
ϕ=ξ2
Beckmann is defined as:
DBeckmann(m)=1πα2cos4(θ)e−tan2(θ)α2
Which can be sampled with:
θ=arccos(11=α2ln(1−ξ1)−−−−−−−−−−−−−−√)ϕ=ξ2
Lastly, Blinn is defined as:
DBlinn(m)=α+22π(cos(θ))α
Which can be sampled with:
θ=arccos(1ξα+11)ϕ=ξ2
Putting it in Practice
Let's look at a basic backwards path tracer:
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. we bounce around the scene, accumulating color and light attenuation as we go. At each bounce, we have to choose a new direction for the ray. As mentioned above, we could uniformly sample the hemisphere to generate the new ray. However, the code is smarter; it importance samples the new direction based on the BRDF. (Note: This is the input direction, because we are a backwards path tracer)
// 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);
Which could be implemented as:
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;
}
After we sample the inputDirection ('wi' in the code), we use that to calculate the value of the BRDF. And then we divide by the pdf as per the Monte Carlo formula:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Where Eval() is just the BRDF function itself (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);
}