2
votes

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

  • All my engine code is written in C
  • Apart from the application layer (I use glfw on desktop and glfm on mobile) the code base is identical: the exact same code is compiled for desktop and for mobile.
  • The shaders - apart from injected headers like #version and precision defaults - are also identical across platforms
  • The shadow map system in my engine is based on the tutorial found here: https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
  • I am using an NVIDIA GTX 1080 for desktop for development, have tested with other NVIDIA PC's as well
  • I am using a brand new ASUS ROG II Phone and an old LG G5 for testing, both run Android 9, both support OpenGL ES 3.2 according to the CPU-Z app, and both show the exact same flawed results.

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

  • I've taken great care to make sure that all my calls are available on OpenGL ES 3.2 just like they are on OpenGL 3.3 Core
  • I've taken great care making sure that there are no problems with the OpenGL calls: I am using glDebugMessageCallback and there are no errors being reported at any time
  • Since it "just works" on desktop, I feel like the math behind it isn't wrong
  • My engine support caching of shadow maps for far away light as a performance optimization, but disabling this had no effect, so it's not causing the problem.

(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

1
Probably not important but what are the sizes and image format/texture compression format that you are using. - Morrison Chang
To narrow this down a bit, the first step I would take is add some debug output that visualizes the contents of the shadow map. Just draw a quad somewhere with a shader that turns the depth values from the shadow map into a color. That way, you can check whether stuff is even rendered into your shadow map and whether sampling the shadow map from a shader works in principle… - Michael Kenzel
@MorrisonChang I'm using 256x256 GL_FLOAT shadow maps - foddex
@MichaelKenzel I added an update as per your suggestion - foddex
Just FYI: I notice a few differences between: arm-software.github.io/opengl-es-sdk-for-android/… and your tutorial link. - Morrison Chang

1 Answers

1
votes

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...