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

posted on 2013-03-11 20:15  迈克老狼2012  阅读(2823)  评论(0编辑  收藏  举报

导航