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

  • 相关阅读:
    开源项目中标准文件命名和实践
    linux远程拷贝命令-scp
    Linux访问Windows共享目录的方法——smbclient
    ADB Fix error : insufficient permissions for device
    APT典型应用示例
    我的参考书籍列表
    GCC Reference
    GNU make简介
    Windows下搭建Android NDK开发环境及命令行编译
    Git命令行基本操作
  • 原文地址:https://www.cnblogs.com/mikewolf2002/p/2954575.html
Copyright © 2011-2022 走看看