Problema com luz especular em ângulos altamente oblíquos com Blinn-Phong

Aug 17 2020

Estou tendo um problema com meu renderizador Blinn-Phong básico, ao olhar para objetos em ângulos muito oblíquos:

Não acho que isso seja um problema com meu código, embora irei postar o GLSL do meu fragmento abaixo. Em vez disso, esta parece ser uma consequência necessária de cortar o componente especular da iluminação para zero quando ponto (Normal, Luz) <= 0. (O que todos dizem para você fazer.) Mas fazer isso significa que haverá essa descontinuidade no Exterminador do Futuro. A remoção da fixação leva a outros problemas; não há mais costura visível, mas agora o destaque especular continua ao redor do lado escuro da esfera.

Existe uma maneira simples de contornar isso ou isso é apenas uma desvantagem inevitável do modelo Blinn-Phong?

EDITAR

TL; DR: Parece que isso não é apenas uma desvantagem do modelo Blinn-Phong.

Fiz mais pesquisas sobre BRDFs e encontrei este artigo: Um novo modelo de BRDF de Ward com albedo limitado e dados de refletância de ajuste para RADIANCE , que discute a deficiência do modelo de Ward, especificamente em torno de ângulos rasos altos (exatamente o meu problema!), E como eles ajustaram o modelo para consertá-lo. Ward é um modelo anisotrópico, mas pode ser simplificado para ser isotrópico, e quando você faz isso no formulário pronto para implementação na página 22, você obtém:

Eu pluguei isso em meu código (atualizado abaixo), aaaannnnnd ... no dice. Parece bonito nos casos normais, mas exibe os mesmos modos de falha na borda, e alguns ainda mais espetaculares além da borda (ainda pior do que Blinn-Phong):

Observação: ambos os modelos estão usando o parâmetro "brilhante", mas significam coisas diferentes para cada modelo. As imagens originais estavam com brilhante = 0,8, para Ward eu tive que diminuí-lo para 0,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);
}

Respostas

4 D0SBoots Aug 20 2020 at 05:35

TL; DR: Multiplique seu valor especular por ponto (normal, normal claro). (E fazer grampo que pontilham produto a um mínimo de 0!)

Eu, e eu suspeito (vendo todos os tutoriais por aí) muitos outros, temos feito tudo errado.

Eu estava usando a função de distribuição de refletância bidirecional (AKA BRDF) diretamente para calcular a intensidade especular. No entanto, o BRDF na verdade define uma quantidade diferencial que deve ser conectada a uma integral radiométrica. Fundamentalmente, o que é geralmente chamado a BRDF carece de um termo cos (θ), que faz parte da integral total. Se você tem estômago para a matemática, ela é discutida em mais detalhes aqui:http://www.pbr-book.org/3ed-2018/Color_and_Radiometry/Surface_Reflection.html#TheBRDF

Quando estamos lidando apenas com fontes pontuais de luz, não precisamos avaliar a integral inteira; o limite da integral como uma fonte de luz se torna um ponto que vai para o integrando. Mas o termo cos (θ) ainda é importante.

Na prática, isso significa que o sombreador precisa de um leve ajuste. Resultados:

Acima está Blinn-Phong, corrigido. Isso é com o mesmo brilho de antes; como Blinn-Phong não tem um termo de Fresnel, a correção cos (θ) faz com que a intensidade caia drasticamente em ângulos rasos. Mas a descontinuidade se foi, pelo menos.

Este é o Ward fixo, com o mesmo brilho da 2ª imagem anterior. A reflexão de Fresnel (grosso modo, reflexão mais alta em ângulos mais oblíquos) é modelada em Ward, então parece bom. E sem descontinuidade!