Problema con luce speculare ad angoli fortemente obliqui con Blinn-Phong

Aug 17 2020

Ho un problema con il mio renderer Blinn-Phong di base, quando guardo oggetti ad angoli molto obliqui:

Non penso che questo sia un problema con il mio codice, anche se pubblicherò il GLSL del mio frammento di seguito. Piuttosto, questa sembra essere una conseguenza necessaria del taglio della componente speculare dell'illuminazione a zero quando il punto (Normale, Luce) <= 0. (Cosa che tutti ti dicono di fare). Ma ciò significa che ci sarà questa discontinuità al terminatore. La rimozione del bloccaggio porta ad altri problemi; non c'è più cucitura visibile, ma ora l'evidenziazione speculare continua intorno al lato oscuro della sfera.

C'è un modo semplice per aggirare questo, o è solo un inevitabile svantaggio del modello Blinn-Phong?

MODIFICARE

TL; DR: Sembra che questo non sia solo uno svantaggio del modello Blinn-Phong.

Ho fatto altre ricerche sui BRDF e ho trovato questo documento: A New Ward BRDF Model with Bounded Albedo and Fitting Reflectance Data for RADIANCE , che discute la mancanza del modello Ward, in particolare intorno agli angoli di pascolo elevati (esattamente il mio problema!), E come hanno modificato il modello per risolverlo. Ward è un modello anisotropo, ma può essere semplificato per essere isotropo, e quando lo fai nel loro modulo pronto per l'implementazione da pagina 22, ottieni:

L'ho inserito nel mio codice (aggiornato di seguito), aaaannnnnd ... nessun dado. Sembra carino nei casi normali, ma mostra le stesse modalità di guasto al limite e alcune anche più spettacolari oltre il limite (anche peggio di Blinn-Phong):

Nota: entrambi i modelli utilizzano il parametro "brillante", ma significano cose diverse per ogni modello. Gli screenshot originali erano con shiny = .8, per Ward ho dovuto abbassarlo a .1.

#version 150

#extension GL_ARB_conservative_depth : enable

in Frag {
    vec3 color;
    vec3 coord;
    vec3 center;
    float R;
};

out vec4 color_out;
layout (depth_greater) out float gl_FragDepth;

uniform mat4 VIEW;
uniform mat4 PROJ;

const vec3 gamma = vec3(1.0 / 2.2);
const float ambientPower = .15;
const float diffusePower = .75;

const bool PHONG = false;
const float specHardness = 60.0;
const float shiny = .1;

const bool WARD = true;

void main() {
    // Find intersection of ray (given by coord) with sphere
    vec3 eyeNormal = normalize(coord);
    float b = dot(center, eyeNormal);
    float c = b * b - (dot(center, center) - R * R);
    if (c < 0.0) {
        discard;  // Doesn't intersect sphere
    }
    vec3 point = (b - sqrt(c)) * eyeNormal;
    // Redo depth part of the projection matrix
    gl_FragDepth = (PROJ[2].z * point.z + PROJ[3].z) / -point.z;

    // Lighting begins here

    // The light dir is in world-space, unlike the others, so we have to project it to view space.
    // The direction (0, 1, 0) corresponds to the 2nd column. By the properties of the view matrix
    // (the 3x3 part is an orthogonal matrix), this is already normalized.
    vec3 lightNormal = VIEW[1].xyz;
    vec3 normal = normalize(point - center);
    float diffuse = dot(lightNormal, normal);

    float specular = 0.0;
    if (PHONG) {
        // Have to reverse sign for eyeNormal so it points out
        vec3 halfway = normalize(lightNormal - eyeNormal);
        specular = diffuse <= 0.0 ? 0.0 : pow(max(0.0, dot(halfway, normal)), specHardness);
    } else if (WARD) {
        const float PI = 3.14159265359;
        const float alpha = .15;
        const float invAlpha2 = 1 / (alpha * alpha);
        // Would move this computation to CPU and pass invAlpha2 as uniform if alpha were a parameter
        const float cFactor = invAlpha2 / PI;

        // Have to reverse sign for eyeNormal so it points out, note this is *unnormalized*
        vec3 halfway = lightNormal - eyeNormal;
        float dotP = dot(halfway, normal);
        float invDot2 = 1 / (dotP * dotP);
        float semiNormalizedInvDot = dot(halfway, halfway) * invDot2;
        // Note: You can't factor the exp(invAlpha2) part out as a constant term,
        // you'll blow out the floating-point range if you try.
        specular = cFactor * exp(invAlpha2-invAlpha2*semiNormalizedInvDot) * semiNormalizedInvDot * invDot2;
    }
    diffuse = max(0.0, diffuse);
    vec3 colorPre = (ambientPower + diffusePower * diffuse) * color
        + specular * shiny * vec3(1);

    color_out = vec4(pow(colorPre, gamma), 0);
}

Risposte

4 D0SBoots Aug 20 2020 at 05:35

TL; DR: Moltiplica il tuo valore speculare per punto (normale, lightNormal). (E fare morsetto che dot prodotto ad un minimo di 0!)

Io, e sospetto (dal vedere tutti i tutorial là fuori) molti altri, abbiamo sbagliato tutto.

Stavo usando la funzione di distribuzione della riflettanza bidirezionale (AKA BRDF) direttamente per calcolare l'intensità speculare. Tuttavia, il BRDF definisce effettivamente una quantità differenziale che deve essere collegata a un integrale radiometrico. Fondamentalmente, ciò che di solito è chiamato il BRDF manca un termine cos (θ), che fa parte del complesso integrale. Se hai lo stomaco per la matematica, è discusso in modo più dettagliato qui:http://www.pbr-book.org/3ed-2018/Color_and_Radiometry/Surface_Reflection.html#TheBRDF

Quando abbiamo a che fare solo con sorgenti di luce puntiforme, non abbiamo bisogno di valutare l'intero integrale; il limite dell'integrale come sorgente luminosa diventa un punto va all'integrando. Ma il termine cos (θ) è ancora importante.

In pratica questo significa che lo shader necessita di una leggera modifica. Risultati:

Sopra c'è Blinn-Phong, fisso. Questo è con la stessa lucentezza di prima; poiché Blinn-Phong non ha un termine Fresnel, la correzione cos (θ) fa sì che l'intensità diminuisca drasticamente agli angoli radenti. Ma la discontinuità è sparita, almeno.

Questo è corretto Ward, con la stessa lucentezza della seconda immagine precedente. La riflessione Fresnel (più o meno, una riflessione più alta ad angoli più obliqui) è modellata in Ward, quindi sembra buona. E nessuna discontinuità!