Освещение Oren-Nayar в OpenGL (как рассчитать направление просмотра в фрагментном шейдере)

Я пытаюсь реализовать освещение Орен-Наяра в шейдере фрагмента, как показано здесь.

Однако, я получаю некоторые странные световые эффекты на местности, как показано ниже.

В настоящее время я посылаю шейдеру "вид направления" в качестве "фронтального" вектора камеры. Я не уверен, что это правильно, поскольку перемещение камеры вокруг меняет артефакты.

Умножение вектора "фронт" на матрицу MVP дает лучший результат, но артефакты все еще очень заметны при просмотре местности с некоторых углов. Это особенно заметно в темных областях и вокруг краев экрана.

Что может вызвать этот эффект?

Пример артефакта

введите описание изображения здесь

Как сцена должна выглядеть

введите описание изображения здесь

Vertex Shader

#version 450

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;

out VS_OUT {
    vec3 normal;
} vert_out;

void main() {
    vert_out.normal = normal;
    gl_Position = vec4(position, 1.0);
}

Шейдер управления тесселированием

#version 450

layout(vertices = 3) out;

in VS_OUT {
    vec3 normal;
} tesc_in[];

out TESC_OUT {
    vec3 normal;
} tesc_out[];

void main() { 
    if(gl_InvocationID == 0) {
        gl_TessLevelInner[0] = 1.0;
        gl_TessLevelInner[1] = 1.0;

        gl_TessLevelOuter[0] = 1.0;
        gl_TessLevelOuter[1] = 1.0;
        gl_TessLevelOuter[2] = 1.0;
        gl_TessLevelOuter[3] = 1.0;
    }

    tesc_out[gl_InvocationID].normal = tesc_in[gl_InvocationID].normal;
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

Шейдер оценки Tesselation

#version 450

layout(triangles, equal_spacing) in;

in TESC_OUT {
    vec3 normal;
} tesc_in[];

out TESE_OUT {
    vec3 normal;
    float height;
    vec4 shadow_position;
} tesc_out;

uniform mat4 model_view;
uniform mat4 model_view_perspective;
uniform mat3 normal_matrix;
uniform mat4 depth_matrix;

vec3 lerp(vec3 v0, vec3 v1, vec3 v2) {
    return (
        (vec3(gl_TessCoord.x) * v0) + 
        (vec3(gl_TessCoord.y) * v1) + 
        (vec3(gl_TessCoord.z) * v2)
    );
}

vec4 lerp(vec4 v0, vec4 v1, vec4 v2) {
    return (
        (vec4(gl_TessCoord.x) * v0) + 
        (vec4(gl_TessCoord.y) * v1) + 
        (vec4(gl_TessCoord.z) * v2)
    );
}

void main() {
    gl_Position = lerp(
        gl_in[0].gl_Position,
        gl_in[1].gl_Position,
        gl_in[2].gl_Position
    );

    tesc_out.normal = normal_matrix * lerp(
        tesc_in[0].normal,
        tesc_in[1].normal,
        tesc_in[2].normal
    );

    tesc_out.height = gl_Position.y;

    tesc_out.shadow_position = depth_matrix * gl_Position;
    gl_Position = model_view_perspective * gl_Position;
}

Фрагментный шейдер

#version 450

in TESE_OUT {
    vec3 normal;
    float height;
    vec4 shadow_position;
} frag_in;

out vec4 colour;

uniform vec3 view_direction;
uniform vec3 light_position;

#define PI 3.141592653589793

void main() {
    const vec3 ambient = vec3(0.1, 0.1, 0.1);
    const float roughness = 0.8;

    const vec4 water = vec4(0.0, 0.0, 0.8, 1.0);
    const vec4 sand = vec4(0.93, 0.87, 0.51, 1.0);
    const vec4 grass = vec4(0.0, 0.8, 0.0, 1.0);
    const vec4 ground = vec4(0.49, 0.27, 0.08, 1.0);
    const vec4 snow = vec4(0.9, 0.9, 0.9, 1.0);

    if(frag_in.height == 0.0) {
        colour = water;
    } else if(frag_in.height < 0.2) {
        colour = sand;
    } else if(frag_in.height < 0.575) {
        colour = grass;
    } else if(frag_in.height < 0.8) {
        colour = ground;
    } else {
        colour = snow;
    }

    vec3 normal = normalize(frag_in.normal);
    vec3 view_dir = normalize(view_direction);
    vec3 light_dir = normalize(light_position);

    float NdotL = dot(normal, light_dir);
    float NdotV = dot(normal, view_dir); 

    float angleVN = acos(NdotV);
    float angleLN = acos(NdotL);

    float alpha = max(angleVN, angleLN);
    float beta = min(angleVN, angleLN);
    float gamma = dot(view_dir - normal * dot(view_dir, normal), light_dir - normal * dot(light_dir, normal));

    float roughnessSquared = roughness * roughness;
    float roughnessSquared9 = (roughnessSquared / (roughnessSquared + 0.09));

    // calculate C1, C2 and C3
    float C1 = 1.0 - 0.5 * (roughnessSquared / (roughnessSquared + 0.33));
    float C2 = 0.45 * roughnessSquared9;

    if(gamma >= 0.0) {
        C2 *= sin(alpha);
    } else {
        C2 *= (sin(alpha) - pow((2.0 * beta) / PI, 3.0));
    }

    float powValue = (4.0 * alpha * beta) / (PI * PI);
    float C3  = 0.125 * roughnessSquared9 * powValue * powValue;

    // now calculate both main parts of the formula
    float A = gamma * C2 * tan(beta);
    float B = (1.0 - abs(gamma)) * C3 * tan((alpha + beta) / 2.0);

    // put it all together
    float L1 = max(0.0, NdotL) * (C1 + A + B);

    // also calculate interreflection
    float twoBetaPi = 2.0 * beta / PI;

    float L2 = 0.17 * max(0.0, NdotL) * (roughnessSquared / (roughnessSquared + 0.13)) * (1.0 - gamma * twoBetaPi * twoBetaPi);

    colour = vec4(colour.xyz * (L1 + L2), 1.0);
}

Ответ 1

Сначала я подключил ваш шейдер фрагмента к моему рендерингу с помощью векторов view/normal/light, и он отлично работает. Таким образом, проблема должна быть в том, как вы вычисляете эти векторы.

Затем вы говорите, что вы установили view_dir в вектор переднего фронта камеры. Я предполагаю, что вы имели в виду "вектор фронта камеры в мировом пространстве", который был бы неправильным. Поскольку вы вычисляете точечные продукты с векторами в пространстве камеры, view_dir также должен находиться в пространстве камеры. Это vec3(0,0,1) будет простым способом проверить это. Если это сработает - мы нашли вашу проблему.

Однако использование (0,0,1) для направления просмотра не является строго правильным, если вы выполняете перспективную проекцию, потому что направление от фрагмента к камере зависит от местоположения фрагмента на экране. Тогда правильная формула была бы view_dir = normalize(-pos), где pos - это положение фрагмента в пространстве камеры (то есть с матрицей модельного вида, примененной без проекции). Кроме того, эта величина теперь зависит только от местоположения фрагмента на экране, поэтому вы можете рассчитать ее как:

view_dir = normalize(vec3(-(gl_FragCoord.xy - frame_size/2) / (frame_width/2), flen))

flen - фокусное расстояние вашей камеры, которое вы можете рассчитать как flen = cot(fovx/2).