O que é amostragem por importância?

33

O que é amostragem importante? Cada artigo que li sobre ele menciona 'PDF', o que é isso também?

Pelo que entendi, a amostragem de importância é uma técnica para apenas amostrar áreas de um hemisfério que importam mais que outras. Então, idealmente, eu deveria amostrar raios em direção a fontes de luz para reduzir o ruído e aumentar a velocidade. Além disso, alguns BRDFs em ângulos de pastejo têm pouca diferença no cálculo, então usar amostragem importante para evitar que seja bom?

Se eu fosse implementar amostragem importante para um BRDF da Cook-Torrance, como eu poderia fazer isso?

Arjan Singh
fonte
Este é um link para uma boa leitura que explica o que é um PDF. TL; DR a PDF é uma função que descreve a probabilidade de números aleatórios (contínuos, também conhecidos como ponto flutuante). A geração de números aleatórios a partir de um PDF específico pode ser um desafio e existem algumas técnicas para fazer isso. Isso fala sobre um deles. O artigo após este fala sobre outro caminho. blog.demofox.org/2017/08/05/…
Alan Wolfe

Respostas:

51

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)dx1NEu=1Nf(xEu)pdf(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=0 02πe-xpecado(x)dx

Vamos usar a integração Monte Carlo:

Eu1NEu=1Ne-xpecado(xEu)pdf(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 0,2π]pdf(x)=1/(2π-0 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 AffComparação entre amostragem boa e amostragem ruim

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.

Os raios de saída podem ir a 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)=eue(p,ωo)+Ωf(p,ωEu,ωo)euEu(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)G(ωEu,ωo,h)D(h)4porque(θEu)porque(θ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:

  • GGX
  • Beckmann
  • Blinn

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:

DGGX(m)=α2π((α2-1)porque2(θ)+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:

DBeckmumann(m)=1πα2porque4(θ)e-bronzeado2(θ)α2

Que pode ser amostrado com:

θ=arccos(11=α2em(1-ξ1))ϕ=ξ2

Por fim, Blinn é definido como:

DBeuEunn(m)=α+22π(porque(θ))α

Que pode ser amostrado com:

θ=arccos(1ξ1α+1)ϕ=ξ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);
}
RichieSams
fonte
Boa resposta. O OP também perguntou sobre a amostragem de importância de Cook-Torrance, que esta resposta não aborda.
PeteUK #
6
Eu atualizei a resposta para adicionar uma seção sobre Cook-Torrance
RichieSams
Por exemplo, GGX, para amostrar o ângulo de coordenadas esféricas cos (θ), usamos a fórmula amostrada de importância para calcular o ângulo e usamos isso em GGX como de costume, certo? Ou a fórmula substitui totalmente a GGX?
Arjan Singh
3
Adicionei uma seção para ajudar a responder às suas perguntas. Mas, resumindo, seu primeiro método está correto. Você usa a fórmula de amostragem para gerar uma direção, depois usa essa nova direção na fórmula GGX normal e obtém o pdf da fórmula de Monte Carlo.
precisa saber é o seguinte
Para o GGX, como calcular / amostrar wi? Eu entendo como amostrar o ângulo de coordenadas esféricas θ, mas para o vetor de direção real, como isso é feito?
Arjan Singh
11

Se você possui uma função 1D e deseja integrar essa função de 0 a 1, uma maneira de realizar essa integração é tomar N amostras aleatórias no intervalo [0, 1], avaliar para cada amostra e calcule a média das amostras. No entanto, diz-se que essa integração "ingênua" de Monte Carlo "converge lentamente", ou seja, você precisa de um grande número de amostras para se aproximar da verdade do terreno, principalmente se a função tiver altas frequências.f(x)f(x)

Com amostragem importante, em vez de coletar N amostras aleatórias no intervalo [0, 1], você coleta mais amostras nas regiões "importantes" de que mais contribuem para o resultado final. No entanto, como você polariza a amostragem para as regiões importantes da função, essas amostras devem ser ponderadas menos para compensar a polarização, que é onde o PDF (função de densidade de probabilidade) aparece. O PDF informa a probabilidade de uma amostra em determinada posição e é usado para calcular a média ponderada das amostras dividindo cada amostra com o valor do PDF em cada posição da amostra.f(x)

Com a amostragem de importância de Cook-Torrance, a prática comum é distribuir amostras com base na função de distribuição normal NDF. Se o NDF já estiver normalizado, ele poderá servir diretamente como PDF, o que é conveniente, pois cancela o termo da avaliação do BRDF. A única coisa que você precisa fazer é distribuir as posições das amostras com base no PDF e avaliar o BRDF sem o termo NDF, ou seja, E calcule a média dos resultados da amostra multiplicada pelo ângulo sólido do domínio em que você integra (por exemplo, para o hemisfério).

f=FGπ(nωEu)(nωo)
2π

Para NDF, é necessário calcular a Função de distribuição cumulativa do PDF para converter a posição da amostra distribuída uniformemente em posição de amostra ponderada em PDF. Para NDF isotrópico, isso simplifica a função 1D devido à simetria da função. Para mais detalhes sobre a derivação do CDF, você pode conferir este artigo antigo da GPU Gems .

JarkkoL
fonte