OpenGL 阴影贴图移动版不起作用

Posted

技术标签:

【中文标题】OpenGL 阴影贴图移动版不起作用【英文标题】:OpenGL Shadow Map Mobile Version Doesn't work 【发布时间】:2019-11-28 13:56:22 【问题描述】:

总结

我有一个问题移植我的基于 OpenGL 3.3(“桌面”)的游戏引擎到 OpenGL ES 3.2(“移动”)。虽然一切都在桌面上完美运行,但在移动设备上一切正常除了阴影贴图

我的问题很简单:有人能在我分享的代码中发现问题,或者指出正确的方向吗?

上下文

我所有的引擎代码都是用 C 编写的 除了应用层(我在桌面上使用 glfw 和在移动设备上使用 glfm)之外,代码库是相同的:为桌面和移动编译完全相同的代码着色器 - 除了注入的标头(如 #version 和精度默认值)外 - 跨平台也是相同的 我的引擎中的阴影贴图系统基于此处找到的教程:https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows 我正在使用 NVIDIA GTX 1080 进行桌面开发,也已在其他 NVIDIA PC 上进行过测试 我正在使用全新的华硕 ROG II 手机和旧的 LG G5 进行测试,都运行 android 9,根据 CPU-Z 应用程序都支持 OpenGL ES 3.2,并且 都显示完全相同的缺陷结果

截图

我非常简单的光照测试关卡在桌面上正确渲染。中间的柱子右侧有一盏黄色(不可见)灯,所以阴影应该投射到左侧,如下面的桌面截图所示:

但是在手机上就大错特错了:

如您所见,移动设备上的阴影“完全错误”。它应该像在桌面上一样在左侧,但它却走错了方向,看起来“被切断了”。

我自己的想法和行为

我非常注意确保我的所有调用都可以在 OpenGL ES 3.2 上使用,就像它们在 OpenGL 3.3 Core 上一样 我非常注意确保 OpenGL 调用没有问题:我正在使用 glDebugMessageCallback 并且任何时候都不会报告错误 由于它在桌面上“正常工作”,我觉得它背后的数学没有错 我的引擎支持缓存远光的阴影贴图作为性能优化,但禁用此功能没有任何效果,因此不会导致问题。

(希望)相关代码

C 中的阴影贴图设置

创建 CUBE MAP 纹理以保存阴影的代码:

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;

为阴影贴图设置帧缓冲区的代码:

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

渲染阴影贴图的代码:

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);

在桌面上,我为所有着色器(顶点、几何和片段)添加以下前缀:

#version 330 core

在移动设备上,我为所有着色器添加以下前缀:

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

阴影贴图顶点着色器:

layout (location = 0) in vec3 aPos;

uniform mat4 gModel;

void main()

    gl_Position = gModel * vec4(aPos, 1.0);

阴影贴图几何着色器:


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();
    

阴影贴图片段着色器:


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;

实际使用阴影贴图的点光通道的摘录:


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

我知道这是一个非常开放的问题,涉及很多代码。我正在尝试提供必要的相关信息,如果需要或要求,我可以分享更多信息!

更新 1

按照@MichaelKenzel 的建议,我尝试将阴影贴图渲染到屏幕上。这非常适合桌面,如下所示。然而,在移动设备上,这显示了一个“全红色”阴影贴图,(因为我的着色器执行 1.0 - 采样)纹理()函数为每个像素返回 0。但是,从上面的“错误”图像中可以看出,某处存在阴影,因此以这种方式从深度缓冲区读取似乎是一个问题。 注意:下面的图片来自与上面不同的测试级别,一个具有更复杂的阴影

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);

渲染阴影贴图调试四边形的代码:

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);

更新 2 - 第一个部分修复

感谢@MorrisonChang 的链接,我尝试了一些不同的方法并产生了巨大的不同:我将阴影贴图纹理的 GL_TEXTURE_MAG_FILTER 和 GL_TEXTURE_MIN_FILTER 设置为 GL_LINEAR。当我将其更改为 GL_NEAREST 时,我突然在移动设备上获得了更多结果!然而,正如下图清楚地显示的那样,由于某种原因,它只渲染到一张脸,而不是全部 6 张!但至少我更进一步!

【问题讨论】:

可能并不重要,但您使用的尺寸和图像格式/纹理压缩格式是什么。 为了缩小这个范围,我要做的第一步是添加一些调试输出,以可视化阴影贴图的内容。只需使用着色器在某处绘制一个四边形,将阴影贴图中的深度值转换为颜色。这样,你可以检查是否有东西被渲染到你的阴影贴图中,以及从着色器中采样阴影贴图是否在原则上有效…… @MorrisonChang 我正在使用 256x256 GL_FLOAT 阴影贴图 @MichaelKenzel 我根据您的建议添加了更新 仅供参考:我注意到arm-software.github.io/opengl-es-sdk-for-android/… 和您的教程链接之间存在一些差异。 【参考方案1】:

我找到了答案,而且(对我来说)这令人费解,但我发现它符合规范。在我改变了我的几何着色器之后,我立即让它工作了:

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();
    

到这里:

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]);

显然,通过 for 循环变量分配给 gl_Layer 在 OpenGL ES 中是行不通的!

在这个页面https://www.khronos.org/registry/OpenGL-Refpages/es3/html/gl_Layer.xhtml上面写着

如果着色器静态地为 gl_Layer 赋值,则启用分层渲染模式。

它还说:

如果几何阶段没有对 gl_Layer 进行静态分配,则片段阶段的输入 gl_Layer 将为零。

但它并没有说“你不应该为 gl_Layer 分配非静态值”,编译器也根本没有警告!

在 OpenGL Core 中,规范实际上说了同样的话:https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/gl_Layer.xhtml

但是,无论如何,我的 NVIDIA 驱动程序都支持对 gl_Layer 的动态分配,并且因为它这样做,所以我从未意识到这是错误的......

【讨论】:

但是您的原始代码确实 statically assign 到 gl_Layer。这意味着,或多或少“如果您的代码看起来像为 gl_Layer 分配了任何东西”而不是“如果您的代码为 gl_Layer 分配了一个常量值” 嗯,我没有那样解释它......然后我真的很茫然为什么这个改变让它立即生效......! 很高兴你得到它的工作,但对于任何严重的事情不要使用几何着色器或分层渲染。性能几乎可以保证在每个移动平台上都很糟糕...... 你的两部手机都有 Adreno。我想知道这是否是他们驱动程序中长期存在的错误?我不知道这是否真的是错误的,但也许你应该联系高通。 关于“静态分配”的定义,GL ES 3.2 规范 (khronos.org/registry/OpenGL/specs/es/3.2/es_spec_3.2.pdf) 中有几个地方使用了该短语,其中一个使用以下定义:“静态着色器如果在预处理之后,它包含将写入变量的语句,则为变量分配一个值,无论运行时控制流是否会导致该语句被执行。听起来您的着色器按照该规则是正确的。

以上是关于OpenGL 阴影贴图移动版不起作用的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL移动对象不起作用

OpenGL VBO球体纹理加载不起作用

OpenGL相机扫射不起作用

确定级联阴影贴图分割范围

计算lookAt矩阵不起作用?

UINavigationbar 通过外观设置阴影不起作用