Prawidłowa forma terminu geometria GGX

9

Próbuję zaimplementować BRDF mikrofacet w moim raytracer, ale mam problemy. Wiele artykułów i artykułów, które przeczytałem, definiuje pojęcie geometrii częściowej jako funkcję widoku i pół wektorów: G1 (v, h). Jednak podczas wdrażania tego otrzymałem następujący wynik:

Pojęcie geometrii GGX z wykorzystaniem wektora połowy

(Dolny rząd jest dielektryczny z chropowatością 1,0 - 0,0, Górny rząd jest metaliczny z chropowatością 1,0 - 0,0)

Na brzegach jest dziwne światło i granica wokół nl == 0. Naprawdę nie wiedziałam, skąd to się bierze. Używam Unity jako odniesienia do sprawdzenia moich renderów, więc sprawdziłem ich źródło modułu cieniującego, aby zobaczyć, czego używają i z tego, co mogę powiedzieć, ich geometria nie jest w ogóle sparametryzowana przez wektor pół! Wypróbowałem więc ten sam kod, ale użyłem makro do normalnej powierzchni zamiast pół wektora i otrzymałem następujący wynik:

Termin geometria GGX z wykorzystaniem normalnej powierzchni makro

Moim niewprawnym oku wydaje się to znacznie bliższe pożądanego rezultatu. Ale mam wrażenie, że to nie jest poprawne? Większość artykułów, które czytam, wykorzystuje wektor połówkowy, ale nie wszystkie. Czy jest powód tej różnicy?

Używam następującego kodu jako terminu geometrycznego:

float RayTracer::GeometryGGX(const Vector3& v, const Vector3& l, const Vector3& n, const Vector3& h, float a)
{
    return G1GGX(v, h, a) * G1GGX(l, h, a);
}

float RayTracer::G1GGX(const Vector3& v, const Vector3& h, float a)
{
    float NoV = Util::Clamp01(cml::dot(v, h));
    float a2 = a * a;

    return (2.0f * NoV) / std::max(NoV + sqrt(a2 + (1.0f - a2) * NoV * NoV), 1e-7f);
}

Dla porównania jest to moja normalna funkcja dystrybucji:

float RayTracer::DistributionGGX(const Vector3& n, const Vector3& h, float alpha)
{
    float alpha2 = alpha * alpha;
    float NoH = Util::Clamp01(cml::dot(n, h));
    float denom = (NoH * NoH * (alpha2 - 1.0f)) + 1.0f;
    return alpha2 / std::max((float)PI * denom * denom, 1e-7f);
}
Erwin
źródło

Odpowiedzi:

5

TL; DR: Twoje G1 formuła jest zła.


Aby uniknąć nieporozumień, zakładam, że izotropowa wersja BRDF, model mikropacet Smitha (w przeciwieństwie do modelu z wnęką V) i rozkład mikrofacet GGX.

Według Heitz 2014 termin maskowania / cieniowaniaG1 jest

χ+(ωvωm)21+1+αo2tan2θv

i według Waltera 2007 wzór jest następujący

χ+(ωvωgωvωm)21+1+α2tan2θv

Gdzie ωm oznacza normalny kierunek mikropacet (wektor w połowie drogi), ωg jest głównym (geometrycznym) normalnym kierunkiem (normalnym), ωv jest kierunkiem przychodzącym lub wychodzącym, α jest izotropowym parametrem chropowatości, oraz χ+(a) to dodatnia funkcja charakterystyczna lub funkcja kroku Heaviside (równa się jednej, jeśli a>0 i zero w przeciwnym razie).

Jak widać, połowa wektora ωm służy tylko do upewnienia się, że G1wynosi zero, jeśli konfiguracja geometryczna jest zabroniona. Mówiąc dokładniej, zapewnia, że ​​tylna powierzchnia mikropowierzchni nigdy nie będzie widoczna zωvkierunek na przedniej stronie makrosterfy i odwrotnie (ten ostatni przypadek ma znaczenie tylko wtedy, gdy obsługiwane są również załamania). Jeśli kod wywołujący to gwarantuje, możesz oczywiście pominąć ten parametr. Prawdopodobnie dlatego zrobili to w Jedności.

Z drugiej strony, twoja implementacja używa wektora połowy do obliczenia cosinusa ωv kierunek w stosunku do mikrofaketu, co prowadzi do obliczenia czegoś innego niż przedstawione formuły.

Jeśli to pomoże, to jest to moja implementacja G1 czynnik:

float SmithMaskingFunctionGgx(
    const Vec3f &aDir,  // the direction to compute masking for (either incoming or outgoing)
    const Vec3f &aMicrofacetNormal,
    const float  aRoughnessAlpha)
{
    PG3_ASSERT_VEC3F_NORMALIZED(aDir);
    PG3_ASSERT_VEC3F_NORMALIZED(aMicrofacetNormal);
    PG3_ASSERT_FLOAT_NONNEGATIVE(aRoughnessAlpha);

    if (aMicrofacetNormal.z <= 0)
        return 0.0f;

    const float cosThetaVM = Dot(aDir, aMicrofacetNormal);
    if ((aDir.z * cosThetaVM) < 0.0f)
        return 0.0f; // up direction is below microfacet or vice versa

    const float roughnessAlphaSqr = aRoughnessAlpha * aRoughnessAlpha;
    const float tanThetaSqr = Geom::TanThetaSqr(aDir);
    const float root = std::sqrt(1.0f + roughnessAlphaSqr * tanThetaSqr);

    const float result = 2.0f / (1.0f + root);

    PG3_ASSERT_FLOAT_IN_RANGE(result, 0.0f, 1.0f);

    return result;
}
ivokabel
źródło
Dziękuję za odpowiedź. Wdrożyłem formułę, którą dostarczyłeś i uzyskałem identyczne wyniki, jak moje własne (przy użyciu normalnej makrosurface). Wygląda na to, że jest to tylko inna forma (mam ją z: graphicrants.blogspot.nl/2013/08/specular-brdf-reference.html ) Byłem zdezorientowany co do połowy wektora, ponieważ kurs matematyki SIGGRAPH 2015 PBS wyraźnie określa geometrię funkcja zależy od widoku, światła i pół wektorów. To jest błąd na slajdach?
Erwin
@Erwin, teraz, gdy podałeś także samą formułę, jest o wiele jaśniejszy. Następnym razem zrób to na początku, to pomaga. Tak, obie wersje (moja i Twoja) są równoważne, ale żadna z nich nie używa wektora w połowie drogi do obliczania funkcji sinusoidalnej lub stycznej. To używanv zamiast hvpodobnie jak w przypadku implementacji - wydaje się, że to pomyłka. Podejrzewam, że popełniłeś ten sam błąd przy nowej implementacji.
ivokabel
Użyłem N kropki V w mojej nowej implementacji, co dało mi identyczne wyniki z drugim obrazem, który opublikowałem. Ale nadal nie jestem pewien , dlaczego slajdy kursu PBS stwierdzają, że należy użyć wektora w połowie drogi (patrz: blog.selfshadow.com/publications/s2015-shading-course/hoffman/… , Slajd 88).
Erwin
Czy rozumiem to poprawnie za pomocą hv zamiast nvbył problem? Odnośnie zastosowania wektora w połowie drogi wG1: W rzeczywistości jest on używany w obu wersjach, które opublikowałem (popełniłem błąd przy konstruowaniu formuły LaTeX i zapisałem normę geometryczną w pierwszej, wkrótce ją naprawię), ale chodzi o to, że wektor w połowie nie jest używane do obliczania wartości cosinus (tj. nie ma hvużywany).
ivokabel
Tak, to był problem. Ale moje główne pytanie brzmiało: do czego służy półwektor, ponieważ pojawia się on w definicji funkcji. O ile rozumiem teraz, jest on używany tylko do sprawdzenia, czy punkt H punktu V jest dodatni. Dziękujemy za poświęcenie czasu na napisanie odpowiedzi.
Erwin,