Problema con la luz especular en ángulos muy oblicuos con Blinn-Phong

Aug 17 2020

Tengo un problema con mi renderizador Blinn-Phong básico, cuando miro objetos en ángulos muy oblicuos:

No creo que esto sea un problema con mi código, aunque publicaré el GLSL de mi fragmento a continuación. Más bien, esto parece ser una consecuencia necesaria de cortar el componente especular de la iluminación a cero cuando el punto (Normal, Ligero) <= 0. (Lo que todos le dicen que haga). Pero hacer eso significa que habrá esta discontinuidad en el terminador. Quitar la abrazadera conduce a otros problemas; no hay más costura visible, pero ahora el brillo especular continúa alrededor del lado oscuro de la esfera.

¿Existe una forma sencilla de evitar esto, o es solo una desventaja inevitable del modelo Blinn-Phong?

EDITAR

TL; DR: Parece que esto no es solo una desventaja del modelo Blinn-Phong.

Investigué un poco más sobre los BRDF y encontré este artículo: Un nuevo modelo de BRDF de Ward con albedo limitado y datos de reflectancia ajustados para RADIANCE , que analiza las deficiencias del modelo de Ward, específicamente en torno a ángulos de pastoreo altos (¡exactamente mi problema!), Y cómo ajustaron el modelo para arreglarlo. Ward es un modelo anisotrópico, pero se puede simplificar para que sea isotrópico, y cuando lo hace en su forma lista para la implementación de la página 22, obtiene:

Conecté esto a mi código (actualizado a continuación), aaaannnnnd ... sin dados. Se ve bonito en los casos normales, pero exhibe los mismos modos de falla en el borde, y algunos incluso más espectaculares más allá del borde (incluso peores que Blinn-Phong):

Nota: ambos modelos utilizan el parámetro "brillante", pero significan cosas diferentes para cada modelo. Las capturas de pantalla originales tenían shiny = .8, para Ward tuve que bajarlo 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);
}

Respuestas

4 D0SBoots Aug 20 2020 at 05:35

TL; DR: Multiplique su valor especular por punto (normal, lightNormal). (Y hacer pinza que salpican producto a un mínimo de 0)

Yo, y sospecho (al ver todos los tutoriales que hay) muchos otros, hemos estado haciendo todo mal.

Estaba usando la función de distribución de reflectancia bidireccional (AKA BRDF) directamente para calcular la intensidad especular. Sin embargo, el BRDF en realidad define una cantidad diferencial que debe conectarse a una integral radiométrica. Fundamentalmente, lo que generalmente se llama la BRDF carece de un término cos (θ) que es parte de la integral total. Si tiene estómago para las matemáticas, se analiza con más detalle aquí:http://www.pbr-book.org/3ed-2018/Color_and_Radiometry/Surface_Reflection.html#TheBRDF

Cuando solo estamos tratando con fuentes de luz puntuales, no necesitamos evaluar la integral completa; el límite de la integral como fuente de luz se convierte en un punto que va al integrando. Pero el término cos (θ) sigue siendo importante.

En la práctica, esto significa que el sombreador necesita un ligero ajuste. Resultados:

Arriba está Blinn-Phong, fijo. Esto es con el mismo brillo que antes; Debido a que Blinn-Phong no tiene un término de Fresnel, la corrección de cos (θ) hace que la intensidad disminuya drásticamente en ángulos de rasante. Pero la discontinuidad se ha ido, al menos.

Este es Ward fijo, con el mismo brillo que la segunda imagen anterior. La reflexión de Fresnel (aproximadamente, la reflexión más alta en ángulos más oblicuos) se modela en Ward, por lo que se ve bien. ¡Y sin discontinuidad!