OGL(教程23)——阴影映射1

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

背景知识:
阴影的概念和光照的概念是密不可分的,因为你需要一个灯来释放阴影。有很多方式来产生阴影,在接下来的两节中我们将会学习最基础的最简单的一个——阴影映射。

当到达了光栅化和阴影的时候,你会问自己——这个像素是否在阴影中。让我们换个方法——从光源到像素的路径是否经过其他物体?如果是——这个像素则在阴影中。这里假设其他物体不是透明的。如果不是——这个像素即不在阴影中。这种方式,此问题和之前的章节中的问题很类似——怎样确定看到近的物体,当一个物体和另外一个物体重叠时。如果我们把摄像机放在光源的起点,两个问题变为了一个。我们希望距离远的深度测试时失败(如果此像素比另外一个像素要远),那么此像素在阴影中。只有那些深度测试通过的像素,才在光源中。这个是光源直接照射,路径上没有其他物体。这既是阴影映射的实质。

所以看起来像深度测试能帮着我们决定一个像素是否在阴影中,但是存在一个问题。摄像机和光源并不总是处在同一个位置。深度测试通常是用来从摄像机的角度来解决可见性问题。所以当光源处在很远的位置,如何利用摄像机来解决阴影侦测。解决的方案是,渲染场景两次。第一次是从光源的位置。渲染的结果不会到达颜色缓冲,但是最近点的深度值会存储在应用程序创建的深度缓冲中(这个深度缓冲不是由GLUT自动创建的)。第二次绘制,是通常所谓的在摄像机位置渲染场景。我们创建的深度缓冲被绑定到片段着色器以备读取。对于每个像素我们都从深度缓冲中读取深度信息,我们同样计算从光源位置处的深度信息。某些情况下,这两个深度信息是相同的。这种情况下,此像素离光源最近,所以它的深度值写入了深度缓冲。如果是这样的话,那么此像素在光源中,将会被正常着色。如果深度值是不同的,那么说明从光源的位置来看,有其他的像素覆盖了此像素。在这种情况下,我们在颜色计算的时候添加影子系数,来模拟影子效果。看下图:
在这里插入图片描述

我们的场景由两个物体组成——平面和立方体。光源处在左上角的位置,照射着立方体。首先,我们从光源的位置渲染场景。注意点A、B和C。当渲染到B点的时候,它的深度值写入到了深度缓冲。这是因为在光源和B点之间没有其他的东西。所以此时B点就是距离光源最近的点。但是A和C点就需要进行比较,到底谁会写入深度缓冲呢。由于两个点在同一条直线上,所以光栅化之后,两个点在屏幕的同一个位置。深度测试的时候C点战胜了A点,C的深度值写入深度缓冲。

然后,从摄像机的角度渲染表面和立方体。在光照shader中做所有的事情之外,我们也要计算像素到光源的距离,然后和深度缓冲比较。当光栅化点B的时候,这两个值近乎相等(有些略微的不同是因为浮点数插值计算的时候精度问题)。因此,我们侦测B点不在阴影之内。当光栅化A点的时候,从深度缓冲去除的深度值远远小于A点到光源的距离。因此我们侦测A点在阴影之内,所以要加入阴影系数处理,以使它变得暗一些。

这就是阴影映射算法的原理(第一次从光源渲染场景叫做阴影映射)。我们将会分两个步骤学习它。本节我们将学习如何渲染到阴影映射。渲染某个东西,如深度、颜色等,到一个应用创建的贴图,这个过程叫做渲染到纹理。我们将会使用我们已经熟悉的一个简单的纹理映射技术来把这个阴影映射展示在屏幕上。这是一个很好的调试阴影是否正确的过程。下一节,我们将会看到如何使用阴影映射图来判断像素是否在阴影中。

本节的源代码中,包含一个四边形网格,用来展示阴影映射图。四边形由两个三角形组成,纹理坐标也被设置用来覆盖整个贴图空间。当四边形被渲染的时候,纹理坐标被光栅器插值,于是整个纹理会显示在屏幕上。

代码注释:

(shadow_map_fbo.h:50)

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

OpenGL中的3D渲染管线的最终的结果是一个叫做帧缓冲对象的东东,FBO(framebuffer object)。
帧缓冲对象可以用于颜色缓冲、深度缓冲还有其他的额外的缓冲。当glutInitDisplayMode()被调用的时候,他会根据指定的参数创建默认的帧缓冲对象。帧缓冲是由操作系统管理的,而且不能被OpenGL删除。处理默认的帧缓冲外,应用程序可以创建自己的FBO对象。这些对象可以在应用程序控制下,来应用各种各样的技术。ShadowMapFBO 封装了FBO对象相关的简单接口,用来实现阴影映射。在这个类中,包含两个OpenGL的句柄。m_fbo代表事实上FBO。FBO对象包含了帧缓存的所有状态。一旦这个对下被创建,且被正确配置,我们就可以很容易的绑定到一个不同的对象。注意到仅仅只有默认的帧缓存 可以被用来展示东西到屏幕。由应用程序创建的帧缓冲只能用于离屏渲染。这个是中间的渲染通道,比如我们的阴影映射缓冲,然后才会被用于真实渲染。

就本身而言,帧缓冲对象只是一个占位符。为了让其能够有用,我们需要附加贴图到一个或者多个可以用的附着点。贴图包含实际的帧缓冲存储空间。OpenGL定义下面的附着点:

  1. COLOR_ATTACHMENTi——贴图绑定到这个类型,将会接收来自片段着色器的颜色。i表面可以有多个贴图同时绑定到颜色附着点。这个机制可以在片段着色器中开启同时渲染多个颜色缓冲。
  2. DEPTH_ATTACHMENT——贴图绑定到这个类型,将会接收的是深度测试。
  3. STENCIL_ATTACHMENT——贴图绑定到这个类型,将会称当模板缓冲。模板缓冲限制了光栅化的区域,而且可以用于很多其他技术。
  4. DEPTH_STENCIL_ATTACHMENT——这个是深度和模板的结合,因为这两个经常会同时使用。
    对于阴影映射技术,我们只需要深度缓冲。成员m_shadowMap是一个贴图的句柄,他将会被绑定到DEPTH_ATTACHMENT附着点。ShadowMapFBO提供了一些方法,他们将会在主循环中使用。我们将会在渲染阴影映射之前调用BindForWriting(),而在第二次渲染的时候,调用BindForReading()。
(shadow_map_fbo.cpp:43)

glGenFramebuffers(1, &m_fbo);

这里我们创建了FBO。和贴图和缓冲一样,我们指定了GLuints数组的地址和大小。此数组用句柄填充。

shadow_map_fbo.cpp:46)

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_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

接着,我们创建了贴图,称当的是阴影映射图。通常,这个是标准的2D贴图。

  1. 内部格式是GL_DEPTH_COMPONENT。这个和之前用到的函数不同,它的内部颜色类型是GL_RGB。GL_DEPTH_COMPONENT 意味着一个单精度的浮点数,它代表了标准化的深度。
  2. glTexImage2D 最后一个参数是空。这就意味着我们不提供任何数据初始化缓冲。这个意味着,我们想要每帧都包含深度信息值,每帧都有些不同。每当我们想开始新的帧的时候,我们会调用glClear()来清空缓冲。
  3. 我们告诉OpenGL,当贴图坐标超出边界的时候,限制在[0,1]区间。
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);

我们已经创建了一个FBO,还有一个贴图对象,它已经配置好参数以备阴影映射了。仙子我们需要把贴图对象和FBO绑定。第一件事情,我们需要绑定FBO。这个会使当前和将来的操作都应用于FBO对象。这个函数接收FBO句柄,和指定的目标。目标可以是GL_FRAMEBUFFER,GL_DRAW_FRAMEBUFFER ,GL_READ_FRAMEBUFFER。GL_READ_FRAMEBUFFER是在想从FBO中读取像素的时候使用,函数是glReadPixels。GL_DRAW_FRAMEBUFFER是当想渲染到FBO时使用。GL_FRAMEBUFFER 具备读写两个功能,建议使用这个类型初始化FBO对象。当我们会在真正开始渲染的时候使用GL_DRAW_FRAMEBUFFER。

(shadow_map_fbo.cpp:55)

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

这里我们绑定阴影映射图到FBO的深度缓冲附着点。最后一个参数是揭示了没有mipmap层级被使用。mipmapping是贴图映射特性,它代表不同的分辨率,从高分辨率(其mipmap=0),到其他低分辨率(1-N)。这里我们仅仅使用高分辨率即可。第四个参数是阴影映射图的句柄,如果这里是0,那么当前的贴图就会和指定的附着点解耦(比如上面的深度缓冲)。

(shadow_map_fbo.cpp:58)

glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);

由于我们不想渲染颜色缓冲,只是深度信息。我们只需要调用上面两句代码即可。默认情况,颜色缓冲目标是被置为GL_COLOR_ATTACHMENT0,但是我们FBO不包含颜色缓冲。因此,最好告诉OpenGL我们真正的目的。有效的参数还可以是GL_NONE、GL_COLOR_ATTACHMENT0 到GL_COLOR_ATTACHMENTm ,这里m是GL_MAX_COLOR_ATTACHMENTS -1。这些参数只对FBO对象有效。如果默认的帧缓冲使用参数GL_NONE,GL_FRONT_LEFT,GL_FRONT_RIGHT,GL_BACK_LEFT ,GL_BACK_RIGHT。这些允许你绘制前后左右到缓冲。我们把读取缓冲设置为GL_NONE,记住我们不打算调用glReadPixel函数。这个主要是避免支持OpenGL3.x和OpenGL4.x的GPU会出现的问题。

(shadow_map_fbo.cpp:61)

GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);

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

当配置FBO之后,最重要的事情是验证OpenGL的状态是否为完成。如果有错误要查看。

(shadow_map_fbo.cpp:72)

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

我们要渲染到阴影映射和渲染到默认缓冲之间做切换。第二次渲染的时候,我们将会绑定阴影映射。上面的函数在第一次渲染之前调用。

(shadow_map_fbo.cpp:78)

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

上面的函数在第二次渲染之前调用。注意到,我们绑定了贴图对象,而不是FBO本身。这个函数接收了贴图单元。贴图单元必须和shader中的sampler2D统一变量保持一致。很重要的一点是,当glActiveTexture 接收的参数是索引枚举的时候,如GL_TEXTURE0, GL_TEXTURE1等等。shader需要的仅仅是0和1,等等。

(shadow_map.vs)

#version 330

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

我们将会对两个渲染通道使用同样的shader。顶点着色器两个通道都会使用,但是片段着色器只有第二个通道使用。由于我们第一次渲染的时候,关闭了颜色缓冲,所以片段着色器将不会被使用。上面的顶点着色器很容易。它产生了裁剪空间坐标。第一次渲染,贴图坐标是冗余的,因为没有片段着色器。但是,但是这个不影响,因为要公用顶点着色器。正如你看到的,从shader的角度,这里的z通过或者不通过不再重要。两次调用的不同之处在于,第一次渲染,传递的是光源视角的wvp矩阵,而第二次渲染是摄像机视角的wvp矩阵。第一次渲染,z被距离光源最近的深度值填充。而第二次渲染是被距离摄像机最近的点的深度值填充。第二次渲染,我们需要纹理坐标,因为片段着色器需要从阴影映射图采样。

(shadow_map.fs)

#version 330

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

这个是片段着色器,用来展示渲染通道的阴影映射图。2D纹理坐标用来从阴影映射图中采样。阴影映射图是通过GL_DEPTH_COMPONENT 类型创建的内部格式。这意味着基本纹理单元是单精度浮点数,而不是颜色。这就是为什么在采样的时候使用.x的原因。透视矩阵有一个知道的行为,就是它把距离摄像机更近的点映射到[0,1]范围。理论上z要高精度,因为距离摄像机越近,误差越明显。

猜你喜欢

转载自blog.csdn.net/wodownload2/article/details/83072831
今日推荐