zoukankan      html  css  js  c++  java
  • opengl 教程(23) shadow mapping (1)

       原帖地址: http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html

     

           当光投射到物体上时,会在地面或者墙壁等物体上产生阴影。在计算机图形学中,有很多种技术可以产生阴影,本篇教程中,我们学习一种最常用的阴影技术—shadow mapping

          对于OpenGL程序中的阴影问题,可以归结为:如何判定一个像素是否在阴影区域。简单的说,我们可以把像素的位置和光源的位置连接起来(如下图所示),如果连接线通过物体(假设该物体不透明),则该像素可能在阴影内,否则不在阴影内,如下图中A像素和物体交于C点,所以它应该在阴影内,而B像素和物体没有交点,所以不在阴影内。我们可以把摄像机放在光源的位置,则在阴影区域的点深度测试会失败,而不在阴影区域的点则不会,shadow mapping就是基于这种思路。

          上面我们得到结论,深度测试可以帮我们判定一个像素点是否在阴影区域,但前提条件是光源和摄像机位置相同,在大多数情况下,光源和摄像机都不在一个位置,此时该怎么做呢?

          解决方法就是我们渲染场景两次: 第一次渲染时候,我们把摄像机放在光源的位置,此时我们并不写颜色缓冲,只是输出深度缓冲,通常是输出到一个纹理缓冲中。第二次渲染时候,摄像机在其原始的位置,在第二次渲染的片元shader中,我们会读入第一次渲染的深度缓冲(通常是一个纹理)。对于第二次渲染的每个像素,我们会把这个像素的深度值转化到光源作为视点的空间坐标中[就是通过公式计算得到光源到这点个像素点的距离],然后和第一次渲染保存的深度值进行比较,如果这两个深度是一样的,则表示该像素不在阴影之内,我们输入其正常的颜色即可,如果深度缓冲不同,则表示从光源位置看时,其它的像素遮盖住了它,该像素应该在阴影之内,此时,我们可以输入一个阴影的颜色,比如灰色,作为该像素的输出颜色。

          注意深度比较判断等于和不等于时候,要考虑到精度的问题,如果两个浮点表示的深度值足够接近,就认为它们相等。

    shadow

           我们的场景由两个物体组成,立方体和地面,光源位于左上角,指向立方体。第一次渲染中,摄像机在光源的位置,此时B被渲染,它的深度值进入深度缓冲,而A点和C点在同一条线上,此时C的深度更小,所以C的深度被写入深度缓冲。在第二次渲染中,摄像机在其原始的位置,此时对于B点来说,它的光源视点深度和第一次渲染的深度是一样的(注意,可能不是完全一致,有浮点精度问题存在),所以它不在阴影内,而对于A点,现在的光源视点深度值和第一次渲染的深度不一样,所以A在阴影内。

           我们第一次渲染生成的深度图就称作: shadow map,对于基于shadow map的阴影算法,我们分两篇教程来学习,本篇教程中,我们只学习如何生成shadow map,就是通过纹理映射技术,把第一次渲染的深度图输出到一张纹理中去,最后我们会在屏幕上显示生成的shadow map图,这也是一个很好的调试方法,可以观察到shadow map是否正确,有时候阴影不正确,就是因为shadow map图不对。

           在源代码中,包括一个简单的四边形,该四边形用来显示shadow map。该四边形由2个三角形组成,纹理坐标设置成(0,0),(1,0),(0,1)(1,1),以便使得它覆盖整个纹理空间。

    主要源代码:

    shadow_map_fbo.h

    class ShadowMapFBO
    {
        public:
            ShadowMapFBO();
            ~ShadowMapFBO();
            bool Init(unsigned int WindowWidth, unsigned int WindowHeight);
            void BindForWriting();
            void BindForReading(GLenum TextureUnit);
        private:
            GLuint m_fbo;
            GLuint m_shadowMap;
    };

           在OpengGL中,3D管线最终的输出缓冲称作framebuffer对象或者说FBO,FBO的概念涉及颜色缓冲,深度缓冲以及其它的一些缓冲,比如模板缓冲等等。当函数glutInitDisplayMode()被调用时候,会创建一个缺省的framebuffer对象,这个framebuffer对象由窗口系统管理,OpenGL不能删除它,除了缺省的framebuffer,每个应用程序还能创建自己的FBO,这些对象由应用程序控制,可以用来实现一些特效。

          本篇教程中的ShadowMapFBO类提供一种很方便管理FBO的方法,该类包含2个OpenGL句柄,句柄m_fbo表示真实的FBO(输出到屏幕上的FBO),句柄m_shadowMap表示深度缓冲。注意:只有缺省的framebuffer才能显示在屏幕上,应用程序创建的FBO只能用来离线渲染,比如把该FBO保存在文件中

    framebuffer本身只是一个句柄,我们需要把它和纹理关联起来,纹理中的数据才是framebuffer中真正的内容。

    下面是OpenGL中纹理和FBO关联的一些设置:

    1. COLOR_ATTACHMENTi - 片元shader的输出图像将放到该纹理中。后缀i意味着可能有多个纹理和颜色缓冲相关联,在片元shader中,我们可以同时渲染多个颜色缓冲。
    2. DEPTH_ATTACHMENT - 纹理和深度缓冲相关联。
    3. STENCIL_ATTACHMENT - 纹理和模板缓冲相关联。
    4. DEPTH_STENCIL_ATTACHMENT - 纹理和深度模板缓冲相关联。

          在shadow mapping阴影实现中,我们只需要深度缓冲,m_shadowMap就是和深度缓冲相关联的纹理句柄。ShadowMapFBO类也提供了一些在main cpp中调用的函数,比如渲染shadow map前要调用BindForWriting(),在第二次渲染前要调用BindForReading()。

    shadow_map_fbo.cpp

    glGenFramebuffers(1, &m_fbo);

    我们开始创建FBO,创建方法和纹理类似,首先我们指定一个GLuints数组地址和大小,数组中为fbo句柄。

    glGenTextures(1, &m_shadowMap);

    glBindTexture(GL_TEXTURE_2D, m_shadowMap);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

    接下来,我们创建shadow map纹理,它是一个标准的2D纹理。

    1. 纹理的内部格式是GL_DEPTH_COMPONENT,这和普通的纹理设置不同,通常的纹理一般是GL_RGB,GL_DEPTH_COMPONENT表示纹理数据是一个单浮点格式,该浮点数表示归一化的深度值
    2. 最后一个参数glTexImage2D是空的,这意味着我们不提供任何数据来初始化该缓冲。
    3. GL_CLAMP使得纹理坐标限制在[0,1]内。

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);

          上面的代码把纹理对象和FBO关联起来。GL_DRAW_FRAMEBUFFER表示写framebuffer,而GL_READ_FRAMEBUFFER表示读framebuffer,此时我们可以用glReadPixels得到framebuffer的内容。

    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);

          我们把shadow map纹理和深度FBO关联起来,最后一个参数是mipmap层,因为我们没有使用mipmap层,所以这儿为0,第四个参数为纹理句柄,如果为0的话,则会取消深度FBO的纹理关联。

    glDrawBuffer(GL_NONE);

    由于第一次渲染并不输出color缓冲,所以我们使用GL_NONE参数。缺省情况下,颜色缓冲target是 GL_COLOR_ATTACHMENT0。有效的参数包括:GL_NONE以及GL_COLOR_ATTACHMENT0到GL_COLOR_ATTACHMENTm,这儿m是GL_MAX_COLOR_ATTACHMENTS - 1,注意,这些参数仅对FBO有效。

    GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (Status != GL_FRAMEBUFFER_COMPLETE) {
        printf("FB error, status: 0x%x\n", Status);
        return false;
    }

    FBO配置完成,我们需要验证它是否有效,保证程序不会出错。

    void ShadowMapFBO::BindForWriting()
    {
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
    }

    void ShadowMapFBO::BindForReading(GLenum TextureUnit)
    {
        glActiveTexture(TextureUnit);
        glBindTexture(GL_TEXTURE_2D, m_shadowMap);
    }

          我们会在shadow map和缺省的framebuffer之间进行切换,第一次输出到shadow map,第二次输出到framebuffer,上面的两个函数就是执行该功能。

    下面是第一趟渲染时候,也就是渲染shadow map时的vs和fs(ps)的代码:

    shadowmap.vs

    #version 400                                                                       
                                                                                       
    layout (location = 0) in vec3 Position;                                            
    layout (location = 1) in vec2 TexCoord;                                            
    layout (location = 2) in vec3 Normal;                                              
                                                                                       
    uniform mat4 gWVP;                                                                 
                                                                                       
    out vec2 TexCoordOut;                                                              
                                                                                       
    void main()                                                                        
    {                                                                                  
        gl_Position = gWVP * vec4(Position, 1.0);                                      
        TexCoordOut = TexCoord;                                                        
    }

    shadowmap.ps

    #version 400
    in vec2 TexCoordOut;
    uniform sampler2D gShadowMap;
    out vec4 FragColor;
    void main()
    {
        float Depth = texture(gShadowMap, TexCoordOut).x;
        Depth = 1.0 - (1.0 - Depth) * 25.0;
        FragColor = vec4(Depth);
    }

        在第二次执行渲染中,我们会执行片元shader,输出shadow map纹理。由于shadow map创建时候使用了GL_DEPTH_COMPONENTU做为格式,它是单浮点数,并不是颜色,所以我们用纹理坐标的x分量,来采样纹理值。透视投影有个问题,它把一个顶点向量z值归一化时候,它会保留更多接近视点的位置,靠近视点位置的的深度比较小,用图像显示出来,可能不太清晰,我们用一个变化显示深度,并把深度扩展为vec4表示的颜色。

    tutorial23.cpp

    virtual void RenderSceneCB()
    {
        m_pGameCamera->OnRender();
        m_scale += 0.05f;
        ShadowMapPass();
        RenderPass();

        glutSwapBuffers();
    }

    主函数的渲染很简单,它调用两个渲染函数,先渲染shadow map,第二趟渲染把shadow map输出到屏幕上。

    virtual void ShadowMapPass()
    {
        m_shadowMapFBO.BindForWriting();
        glClear(GL_DEPTH_BUFFER_BIT);
        Pipeline p;
        p.Scale(0.1f, 0.1f, 0.1f);
        p.Rotate(0.0f, m_scale, 0.0f);
        p.WorldPos(0.0f, 0.0f, 5.0f);
        p.SetCamera(m_spotLight.Position, m_spotLight.Direction, Vector3f(0.0f, 1.0f, 0.0f));
        p.SetPerspectiveProj(20.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);
        m_pShadowMapTech->SetWVP(p.GetWVPTrans());
        m_pMesh->Render();
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    第一次渲染中,会把摄像机放在光源的位置,用来得到shadow map。

    virtual void RenderPass()
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        m_pShadowMapTech->SetTextureUnit(0);
        m_shadowMapFBO.BindForReading(GL_TEXTURE0);
        Pipeline p;
        p.Scale(5.0f, 5.0f, 5.0f);
        p.WorldPos(0.0f, 0.0f, 10.0f);
        p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
        p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);
        m_pShadowMapTech->SetWVP(p.GetWVPTrans());
        m_pQuad->Render();
    }

    第二次渲染中,把输入的纹理shadow map,渲染在一个quad中,注意:需要装入quad模型quad.obj。

    程序执行后效果如下:

    clipboard

  • 相关阅读:
    vim复制
    嵌入式Linux学习(二)
    (Java实现) 洛谷 P1042 乒乓球
    (Java实现) 洛谷 P1042 乒乓球
    (Java实现) 洛谷 P1071 潜伏者
    (Java实现) 洛谷 P1071 潜伏者
    (Java实现) 洛谷 P1025 数的划分
    (Java实现)洛谷 P1093 奖学金
    (Java实现)洛谷 P1093 奖学金
    Java实现 洛谷 P1064 金明的预算方案
  • 原文地址:https://www.cnblogs.com/mikewolf2002/p/2954575.html
Copyright © 2011-2022 走看看