现代OpenGL阴影立方体贴图的指针?

18

背景

我正在使用C++和现代OpenGL(3.3)开发一个3D游戏。目前,我正在处理光照和阴影渲染,并且已经成功地实现了定向阴影映射。在阅读游戏要求后,我决定需要点光源阴影映射。经过一些研究,我发现为了进行全向阴影映射,我将执行类似于定向阴影映射的操作,但是将使用立方体贴图。

我之前没有关于立方体贴图的知识,但我的理解是它是六个无缝连接的纹理。

我寻找了一些相关的资料,但很遗憾我很难找到一份完整的现代OpenGL教程。我首先寻找能够从头到尾解释的教程,因为我很难从代码片段或概念中学习,但我尝试过了。

目前的理解

以下是我的一般理解,排除了技术细节。请纠正我。

  • 对于每个点光源,设置一个帧缓冲区,就像定向阴影映射一样。
  • 然后生成单个立方体贴图纹理,并使用glBindTexture(GL_TEXTURE_CUBE_MAP, shadowmap)进行绑定。
  • 设置立方体贴图的以下属性:

    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    

(这也类似于方向阴影贴图)

  • 现在glTexImage2D()会迭代六次,每个面一次。我是这样做的:

 for (int face = 0; face < 6; face++) // Fill each face of the shadow cubemap
     glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_DEPTH_COMPONENT32F , 1024, 1024, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
纹理通过调用以下函数附加到帧缓冲区:
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadowmap, 0);
  • 渲染场景时,会像方向阴影映射一样进行两次渲染。

  • 首先,绑定阴影帧缓冲区,将视口调整为阴影图的大小(在此情况下为1024×1024)。
  • 设置剔除正面的面,使用 glCullFace(GL_FRONT)
  • 切换活动着色器程序到后面我会提供源代码的顶点和片元阴影着色器。
  • 计算所有六个视角的光照视图矩阵。我是通过创建一个 glm::mat4 的向量并使用 push_back() 函数将矩阵添加到其中实现的,如下所示:

  • // Create the six view matrices for all six sides
    for (int i = 0; i < renderedObjects.size(); i++) // Iterate through all rendered objects
    {
        renderedObjects[i]->bindBuffers(); // Bind buffers for rendering with it
    
        glm::mat4 depthModelMatrix = renderedObjects[i]->getModelMatrix(); // Set up model matrix
    
        for (int i = 0; i < 6; i++) // Draw for each side of the light
        {
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, shadowmap, 0);
            glClear(GL_DEPTH_BUFFER_BIT); // Clear depth buffer
    
            // Send MVP for shadow map
            glm::mat4 depthMVP = depthProjectionMatrix * depthViewMatrices[i] * depthModelMatrix;
            glUniformMatrix4fv(glGetUniformLocation(shadowMappingProgram, "depthMVP"), 1, GL_FALSE, glm::value_ptr(depthMVP));
    
            glUniformMatrix4fv(glGetUniformLocation(shadowMappingProgram, "lightViewMatrix"), 1, GL_FALSE, glm::value_ptr(depthViewMatrices[i]));
            glUniformMatrix4fv(glGetUniformLocation(shadowMappingProgram, "lightProjectionMatrix"), 1, GL_FALSE, glm::value_ptr(depthProjectionMatrix));
            glDrawElements(renderedObjects[i]->getDrawType(), renderedObjects[i]->getElementSize(), GL_UNSIGNED_INT, 0);
        }
    }
    
  • 默认的帧缓冲区已绑定,场景正常绘制。

  • 问题

    现在来看着色器。这就是我理解不足的地方。我完全不确定该做什么,我的研究似乎彼此矛盾,因为它们是针对不同版本的。最终,我只能简单地从随机来源中复制和粘贴代码,并希望它能实现除黑屏以外的其他东西。我知道这很糟糕,但似乎没有清晰的定义要做什么。我应该在哪些空间里工作?我是否需要单独的阴影着色器,就像我在指向点光源时使用的那样?做为遮罩立方体图像的类型到底用什么?samplerCube?samplerCubeShadow?如何正确地采样这个立方体贴图?我希望有人能澄清一下,并提供一个好的解释。 我目前对着色器的理解是: - 当场景被渲染到立方体贴图中时,顶点着色器只需使用我在C++代码中计算的depthMVP uniform并通过它们转换输入顶点。 - 立方体贴图传递的片段着色器仅将单个输出值分配给gl_FragCoord.z。(这一部分与我实现定向阴影映射时没有改变。我认为在立方体图形映射中会是相同的,因为着色器甚至不与立方体贴图交互-OpenGL只是将它们的输出呈现到立方体贴图中,对吧?因为它是一个帧缓冲区?)

    • 正常渲染的顶点着色器未更改。
    • 在正常渲染的片段着色器中,使用光照的投影和视图矩阵将顶点位置转换为光照空间。
    • 这样以某种方式用于立方体贴图纹理查找。???
    • 一旦使用神奇的方法检索了深度,就将其与距离顶点的光线进行比较,就像定向阴影映射一样。如果小于,则该点必须被遮挡,反之亦然。

    这并不是很理解。我不知道顶点是如何被转换和用于查找立方体贴图的,所以我要粘贴我的着色器源代码,希望人们能澄清这一点。请注意,这些代码大部分都是盲目地复制和粘贴的,我没有改变任何东西以免危及任何理解。

    阴影顶点着色器:

    #version 150
    
    in vec3 position;
    
    uniform mat4 depthMVP;
    
    void main()
    {
        gl_Position = depthMVP * vec4(position, 1);
    }
    

    阴影片段着色器:

    #version 150
    
    out float fragmentDepth;
    
    void main()
    {
        fragmentDepth = gl_FragCoord.z;
    }
    

    标准顶点着色器:

    #version 150
    
    in vec3 position;
    in vec3 normal;
    in vec2 texcoord;
    
    uniform mat3 modelInverseTranspose;
    uniform mat4 modelMatrix;
    uniform mat4 viewMatrix;
    uniform mat4 projectionMatrix;
    
    out vec3 fragnormal;
    out vec3 fragnormaldirection;
    out vec2 fragtexcoord;
    out vec4 fragposition;
    out vec4 fragshadowcoord;
    
    void main()
    {
        fragposition = modelMatrix * vec4(position, 1.0);
        fragtexcoord = texcoord;
        fragnormaldirection = normalize(modelInverseTranspose * normal);
        fragnormal = normalize(normal);
        fragshadowcoord = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    
    
        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    }
    

    标准片段着色器:

    #version 150
    
    out vec4 outColour;
    
    in vec3 fragnormaldirection;
    in vec2 fragtexcoord;
    in vec3 fragnormal;
    in vec4 fragposition;
    in vec4 fragshadowcoord;
    
    uniform mat4 modelMatrix;
    uniform mat4 viewMatrix;
    uniform mat4 projectionMatrix;
    uniform mat4 viewMatrixInversed;
    
    uniform mat4 lightViewMatrix;
    uniform mat4 lightProjectionMatrix;
    
    uniform sampler2D tex;
    uniform samplerCubeShadow shadowmap;
    
    float VectorToDepthValue(vec3 Vec)
    {
        vec3 AbsVec = abs(Vec);
        float LocalZcomp = max(AbsVec.x, max(AbsVec.y, AbsVec.z));
    
        const float f = 2048.0;
        const float n = 1.0;
        float NormZComp = (f+n) / (f-n) - (2*f*n)/(f-n)/LocalZcomp;
        return (NormZComp + 1.0) * 0.5;
    }
    
    float ComputeShadowFactor(samplerCubeShadow ShadowCubeMap, vec3 VertToLightWS)
    {   
        float ShadowVec = texture(ShadowCubeMap, vec4(VertToLightWS, 1.0));
        if (ShadowVec + 0.0001 > VectorToDepthValue(VertToLightWS)) // To avoid self shadowing, I guess
            return 1.0;
    
        return 0.7;
    }
    
    void main()
    {
        vec3 light_position = vec3(0.0, 0.0, 0.0);
        vec3 VertToLightWS = light_position - fragposition.xyz;
        outColour = texture(tex, fragtexcoord) * ComputeShadowFactor(shadowmap, VertToLightWS);
    }
    

    我无法记得ComputerShadowFactor和VectorToDepthValue函数代码来源,因为我是在我的笔记本电脑上进行研究的,而现在我无法接触到它,但这些着色器的结果如下:

    这些着色器的结果

    这是一个未被阴影覆盖的小正方形,周围都是有阴影的空间。

    很明显我在这里做错了很多事情,可能集中在我的着色器上,因为我对这个主题的知识缺乏,而且我发现除了教程之外很难学习,对此我非常抱歉。我迷茫了,如果有人能提供清晰的解释,说明我做错了什么,为什么错了,如何修复以及一些代码,那将是太棒了。我认为问题可能是因为我在错误的空间中工作。


    你为什么删除了那个,genpfault? - toficofi
    3
    理论上来说,因为你不需要解释你在遇到这个问题方面有困难,或者你希望得到帮助。但实际上,是因为他想要一个编辑徽章。 - Brett Hale
    2
    谢谢你的解释,@BrettHale,但我还是回滚了,因为我认为我的困难解释很重要,因为回答问题的人可能能够以一种更容易理解的方式调整他们的答案,这样可以更快地解决问题。 - toficofi
    1个回答

    11

    我希望能解答您的一些问题,但首先需要一些定义:

    什么是立方体贴图(cubemap)?

    它是从一个方向向量到一对[面,该面上的2D坐标]的映射,通过将方向向量投影到一个假想的立方体来获得。

    什么是OpenGL立方体贴图纹理?

    它是一组六个“图像”。

    什么是GLSL立方体贴图采样器?

    它是一个采样器原语,可以进行立方体贴图采样。这意味着它使用方向向量而不是通常的纹理坐标进行采样。硬件将方向向量投影到虚拟立方体上,并在正确的2D位置处使用所得到的[面,2D纹理坐标]对采样正确的“图像”。

    什么是GLSL阴影采样器?

    它是一个采样器原语,绑定到包含NDC空间深度值的纹理上。当使用特定于阴影的采样函数进行采样时,它会返回NDC空间深度(显然在相同的阴影图空间中)和绑定的纹理中存储的NDC空间深度之间的“比较”。在调用采样函数时,要指定与之比较的深度作为纹理坐标的附加元素。请注意,阴影采样器提供了使用和速度方面的便利性,但总是可以在着色器中手动进行比较。


    现在,回答您的问题:

    OpenGL只是向立方体贴图渲染,对吗?

    不是,OpenGL将渲染到当前绑定帧缓冲区中的一组目标。

    对于立方体贴图,通常的渲染方式为:

    • 创建它们并将它们的六个“图像”附加到同一个帧缓冲区中(显然在不同的附件点)
    • 每次只启用一个目标(因此,您逐个渲染每个立方体贴图面)
    • 在立方体贴图面中渲染所需内容(可能使用面特定的“视图”和“投影”矩阵)

    点光源阴影映射

    除了关于立方体贴图的所有内容之外,在实现点光源阴影映射时存在许多问题,因此很少使用硬件深度比较。

    相反,常见的做法如下:

    • 不要写入NDC空间深度,而是写入距离点光源的径向距离
    • 在查询阴影图时(请参见下文的示例代码):
      • 不要使用硬件深度比较(使用samplerCube代替samplerCubeShadow)
      • 将要测试的点变换到“立方体空间”中(根本不包括投影)
      • 使用“立方体空间”向量作为查找方向来采样立方体贴图
      • 将从立方体贴图中采样的径向距离与测试点的径
        // sample radial distance from the cubemap
        float radial_dist = texture(my_cubemap, cube_space_vector).x;
        
        // compare against test point radial distance
        bool shadowed = length(cube_space_vector) > radial_dist;
        

    很抱歉回复这么晚,但我一直没有时间完全研究你的答案并尝试应用它。首先,感谢您清晰明了的解释,但我需要进一步澄清以确保理解正确。首先,您说我必须编写从点光源到阴影贴图的径向距离,而不是深度。这是否意味着我不必通过depthMVP转换我的阴影着色器,有效地从光的角度渲染场景?我是否需要使用正常的MVP渲染场景,并仅计算cont呢? - toficofi
    请问如何计算每个给定点到光源位置的径向距离?另外,如果已知片元和光源的三维坐标,如何计算“径向距离”?这篇维基百科文章http://en.wikipedia.org/wiki/Radial_distance_(geometry)讲解了二维向量的情况,但如果您能进一步澄清,那就太好了。谢谢! - toficofi
    @Jishaxe 我的意思是,在从点光源渲染立方体贴图中的场景几何体时(对于每个立方体面),您只需输出到点光源的距离(在光源的立方体空间中变换的向量长度),而不是剪辑空间距离(gl_FragCoord.z)。我使用了“径向距离”这个术语,但我真正意思是“距离”。简化一下:对于立方体贴图中的每个点,您必须编写该方向上场景中最近物体的距离。 - Gigi
    抱歉 @Gigi,我需要更进一步的澄清。我尝试了你建议的方法,但是随着继续进行,我开始意识到很多都是猜测,我并不知道自己在做什么。首先,我尝试实现你所说的距离计算。在阴影通道的片段着色器中,我添加了这个:distanceToLight = length(cubeSpacePosition - gl_FragCoord.xyz);其中distanceToLight是该着色器的单个输出值,cubeSpacePosition是在顶点着色器中计算的输入,如下:cubeSpacePosition = (depthMVP * vec4(position, 1.0)).xyz; - toficofi
    在标准片段着色器中,这是代码:http://pastebin.com/Ki49NrFq 我认为那部分相当容易理解,但运行时整个场景都处于阴影之中。显然我又犯了多个错误(再次)源于我的知识和经验的缺乏。谢谢Gigi的耐心,对于我来说这很困难,我正在努力。 - toficofi
    显示剩余3条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接