foddex
foddex

Reputation: 540

OpenGL Shadow Map Mobile Version Doesn't work

Summary

I have a problem porting my OpenGL 3.3 ("desktop") based game engine to OpenGL ES 3.2 ("mobile"). While everything works perfectly on the desktop, on mobile everything works but shadow maps.

My question is very simple: can someone spot a problem in the code I shared, or point me in the right derection?

Context

Screenshots

My very simple light test level is rendered correctly on desktop. There is a yellow (invisible) light to the right of the column in the middle, so the shadow should be cast to the left as shown in the following desktop screenshot:

enter image description here

But it's very wrong on mobile:

Screenshot on Mobile

As you can see the shadow on mobile is "just wrong". It's supposed to be on the left like on the desktop, but in stead it goes in the wrong direction, and looks "chopped off".

My own thoughts and actions

(Hopefully) relevant code

Shadow map setup in C

The code that creates the CUBE MAP textures to hold the shadows:

static GLuint r_createShadowmapTexture() {
    GLuint tex;
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
    for (unsigned int i = 0; i < 6; ++i)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT32F, r_shadowSize, r_shadowSize, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_COMPARE_FUNC, GL_GREATER);
    return tex;
}

The code that sets up the framebuffer for the shadow map:

glGenFramebuffers(1, &r_shadowDepthMapFBO);
glBindFramebuffer(GL_FRAMEBUFFER, r_shadowDepthMapFBO);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

The code that renders the shadow map:

static struct {
    vec3 centerOffset, up;
} const g_shadowMapTransforms[6] = {
    { {1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f} },
    { {-1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f} },
    { {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f} },
    { {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, -1.0f} },
    { {0.0f, 0.0f, 1.0f}, {0.0f, -1.0f, 0.0f} },
    { {0.0f, 0.0f, -1.0f}, {0.0f, -1.0f, 0.0f} },
};

static void r_shadowMapPass(const point_light_t* light, GLuint cubemapTexture) {
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);

    glDepthMask(GL_TRUE);

    glBindFramebuffer(GL_FRAMEBUFFER, r_shadowDepthMapFBO);
    glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, cubemapTexture, 0);

    const float farPlane = min(r_far, light->radius);
    mat4x4 shadowProj;
    mat4x4_perspective(shadowProj, (float)M_PI_2, 1.0f, r_near, farPlane);
    mat4x4 shadowVP[6];
    for (int i = 0; i < 6; ++i) {
        vec3 center;
        vec3_add(center, light->origin, g_shadowMapTransforms[i].centerOffset);
        mat4x4 view;
        mat4x4_look_at(view, (float*)light->origin, center, (float*)g_shadowMapTransforms[i].up);
        mat4x4_mul(shadowVP[i], shadowProj, view);
    }

    rtech_shadowmap_enable();
    rtech_shadowmap_setLightPos((float*)light->origin);
    rtech_shadowmap_setFarPlane(farPlane);
    rtech_shadowmap_setShadowMatrices(shadowVP);
    render_entity_t* re = r_entities;

    glClear(GL_DEPTH_BUFFER_BIT);
    for (uint32_t i = 0; i < r_numEntities; ++i, ++re) {
        if (!re->castsShadow)
            continue;
        rtech_shadowmap_setModelMatrix(re->m);
        if (re->hasSolid) {
            const render_solids_t* const rs = r_solids + re->index;
            for (uint32_t j = 0; j < rs->count; ++j) {
                const uint32_t ao = rs->offset + j;
                r_renderBlock(ao, rs, j);
            }
        } else {
            render_model_t* const rm = &r_models[re->index];
            for (uint32_t j = 0; j < rm->count; ++j)
                r_renderArrayObject(rm->arrayObjects[j], rm->numIndices[j]);
        }
    }

    glDepthMask(GL_FALSE);
}

On desktop I prefix the following to ALL shaders (vertex, geometry and fragment):

#version 330 core

On mobile I prefix the following to ALL shaders:

#version 320 es
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp samplerCubeShadow;

The shadowmap vertex shader:

layout (location = 0) in vec3 aPos;

uniform mat4 gModel;

void main()
{
    gl_Position = gModel * vec4(aPos, 1.0);
}

The shadowmap geometry shader:


layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 gShadowMatrices[6];

out vec4 FragPos; // FragPos from GS (output per emitvertex)

void main()
{
    for(int face = 0; face < 6; ++face)
    {
        gl_Layer = face; // built-in variable that specifies to which face we render.
        for(int i = 0; i < 3; ++i) // for each triangle's vertices
        {
            FragPos = gl_in[i].gl_Position;
            gl_Position = gShadowMatrices[face] * FragPos;
            EmitVertex();
        }
        EndPrimitive();
    }
}

The shadowmap fragment shader:


in vec4 FragPos;

uniform vec3 gLightPos;
uniform float gFarPlane;

void main()
{
    // get distance between fragment and light source
    float lightDistance = length(FragPos.xyz - gLightPos);

    // map to [0;1] range by dividing by gFarPlane
    lightDistance = lightDistance / gFarPlane;

    // write this as modified depth
    gl_FragDepth = lightDistance;
}

An extract from the point light pass that actually uses the shadow map:


...
uniform samplerCubeShadow gShadowCubeMap;
uniform float gFarPlane;
...

float ShadowCalcSinglePass(vec3 fragPos, vec3 lightPos) {
    vec3 fragToLight = fragPos - lightPos;
    float currentDepth = (length(fragToLight) - (0.005 * gFarPlane)) / gFarPlane;
    return texture(gShadowCubeMap, vec4(fragToLight, currentDepth));
}

I am aware this is a pretty open ended question, and a lot of code is involved. I am trying to supply the necessary relevant information, I can share more if required or requested!

Update 1

As per @MichaelKenzel's suggestion, I tried rendering the shadow maps to the screen. This worked perfectly for desktop as can be seen below. On mobile however, this showed an "all red" shadow maps, which (since my shader does 1.0 - sample) that the texture() function is returning 0 for every pixel. However, as can be seen on the "wrong" image above, there IS shadow somewhere, so this seems to be a problem with reading from the depth buffer in this fashion. NOTE: the image below is from a different test level than the ones above, one with more complex shadows enter image description here

in vec2 TexCoord0;

uniform samplerCube gTexture;
uniform int gSide;

out vec4 FragColor;

void main()
{
    vec3 vec;
    switch (gSide) {
    case 0:     vec = vec3(1.0, TexCoord0.xy); break;
    case 1:     vec = vec3(-1.0, TexCoord0.xy); break;
    case 2:     vec = vec3(TexCoord0.x, 1.0, TexCoord0.y); break;
    case 3:     vec = vec3(TexCoord0.x, -1.0, TexCoord0.y); break;
    case 4:     vec = vec3(TexCoord0.xy, 1.0); break;
    case 5:     vec = vec3(TexCoord0.xy, -1.0); break;
    default:    vec = vec3(1.0, 0.0, 0.0); break;
    }

    FragColor = vec4(vec3(1.0, 0.0, 0.0) * (1.0 - texture(gTexture, vec).r), /*alpha*/ 1.0);
}

Code that renders the shadow maps debug quads:

void r_debugPassShadowMap2() {
    if (!r_shadowmapDebug.initialized) {
        r_shadowmapDebug.initialized = true;

        const float margin = 16;
        const float mapWidth = (r_windowWidth - (margin * 7.f)) / 6.f;
        mat4x4 t, s;
        mat4x4_translate(t, 1, 1, 0); // quad verts range from -1x-1x0 to 1x1x0
        mat4x4_identity(s);
        s[0][0] = s[1][1] = s[2][2] = mapWidth / 2;
        mat4x4 m;
        mat4x4_mul(m, s, t);

        mat4x4 p;
        mat4x4_ortho(p, 0, (float)r_windowWidth, (float)r_windowHeight, 0, -1, 1);

        for (uint32_t i = 0; i < 6; ++i) {
            mat4x4 v;
            mat4x4_translate(v, margin + (margin + mapWidth) * i, margin, 0);
            mat4x4 mv;
            mat4x4_mul(mv, v, m);
            mat4x4_mul(r_shadowmapDebug.sideWVP[i], p, mv);
        }
    }

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);
    glDisable(GL_BLEND);
    rtech_shadowmapdebug_enable();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, r_shadowmapDebug.texture);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_COMPARE_MODE, GL_NONE);
    for (uint32_t i = 0; i < 6; ++i) {
        rtech_shadowmapdebug_setWVP(r_shadowmapDebug.sideWVP[i]);
        rtech_shadowmapdebug_setSide(i);
        glBindVertexArray(r_quadAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);
    }
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
}

Update 2 - First partial fix

Thanks to @MorrisonChang's link, I tried something different and it made a huge difference: I had set GL_TEXTURE_MAG_FILTER and GL_TEXTURE_MIN_FILTER to GL_LINEAR for the shadow map texture. When I changed it to GL_NEAREST I suddenly got some more results on mobile! However, as the image below clearly shows, for some reason it renders only to ONE face, instead of all 6! But at least I am a step further!

enter image description here

Upvotes: 2

Views: 708

Answers (1)

foddex
foddex

Reputation: 540

I found the answer, and (to me) it is baffling, but I found out it is according to specs. I made it work instantly after I changed my geometry shader from this:

void main()
{
    for(int face = 0; face < 6; ++face)
    {
        gl_Layer = face; // built-in variable that specifies to which face we render.
        for(int i = 0; i < 3; ++i) // for each triangle's vertices
        {
            FragPos = gl_in[i].gl_Position;
            gl_Position = gShadowMatrices[face] * FragPos;
            EmitVertex();
        }
        EndPrimitive();
    }
}

to this:

void emitFace(mat4 m) {
    for(int i = 0; i < 3; ++i)
    {
        FragPos = gl_in[i].gl_Position;
        gl_Position = m * FragPos;
        EmitVertex();
    }
    EndPrimitive();
}

void main()
{
    gl_Layer = 0;
    emitFace(gShadowMatrices[0]);

    gl_Layer = 1;
    emitFace(gShadowMatrices[1]);

    gl_Layer = 2;
    emitFace(gShadowMatrices[2]);

    gl_Layer = 3;
    emitFace(gShadowMatrices[3]);

    gl_Layer = 4;
    emitFace(gShadowMatrices[4]);

    gl_Layer = 5;
    emitFace(gShadowMatrices[5]);
}

Apparently, assigning to gl_Layer through a for loop variable is something that doesn't work in OpenGL ES!

On this page https://www.khronos.org/registry/OpenGL-Refpages/es3/html/gl_Layer.xhtml it says

If a shader statically assigns a value to gl_Layer, layered rendering mode is enabled.

It also says:

If the geometry stage makes no static assignment to gl_Layer, the input gl_Layer in the fragment stage will be zero.

But it doesn't say "you are not supposed to assign a non static value to gl_Layer ever", and the compiler also didn't warn at all!

In OpenGL Core the spec says the exact same thing actually: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/gl_Layer.xhtml

However, my NVIDIA driver supports dynamic assignments to gl_Layer anyway, and because it does that, I never realized it was wrong...

Upvotes: 1

Related Questions