ShadowMap教程

本文来自http://www.paulsprojects.net/tutorials/smt/smt.html感觉此片文章非常好,经本人翻译,有些地方翻译的不明不白,还忘读者能给给出宝贵建议改正。

阴影贴图:

阴影贴图首先是被Lance Williams在1978年在题目为“Casting curved shadows on curved surfaces”引入。在此之后被后人进行了很大的扩展,不论是在离线渲染还是在实时渲染当中。Pixar的RenderMan以及电影“玩具总动员”中都使用了阴影贴图技术。

  阴影贴图只不过是计算机图形学当中众多生成阴影的算法之一。每一种方法都有它自己的优缺点。本文讲述了阴影贴图的方法。

优点:

 1、不用预先处理场景中的几何物体(ShadowVolume需要对场景中的物体预处理,需要大量的运算)

2、仅仅使用一张贴图用于存储场景中对于每一个光源的阴影信息。蒙版缓存不需要。

3、避免了高填充性(shadowVolume具有高填充性)。

缺点:

1、  走样非常严重。

2、  对于每一个聚光灯的光源,如果要生成阴影,场景中的物体都要被渲染一次,而对于全向电光源则需要渲染多次。

本教程侧重于在一个聚光灯下的简单阴影贴,但也有很多如何改善和扩展这项技术的文章。

理论-阴影贴图中的深度测试:

     在一个被一个简单的点光源照射的简单场景中,场景的一点,如何知道这点事接受光照还是在阴影中呢?简单的说,场景中的一点如果在这点与光源之间没有任何阻碍,那么这一点将会被光源照亮。理解阴影贴图的关键就是对于一个位置在光源的观察者而言,那些点是可见的。

     对于一个观察者(Viewer)而言,z-buffer可以确定在一个场景中那些点是可见的。所以那些没有在阴影的区域都是可以通过深度测试的。

     我们将深度纹理中的值设为D,与光源的距离设为R,那么:

     R=D,此点与光源之间没有任何遮挡物,所以这点是没有阴影的。

     R>D,从光源看向此点时,中间必有一个物体遮挡,所以此点时在阴影之中的。

应用程序:

我们如何用OpenGL实现呢?

     这项技术至少需要两个过程,为了简单,我们采取三个过程。

     首先,我们用光源作为视点来绘制场景,可以使用gluLookAt函数从光源的位置看向场景的中心,然后正常绘制场景,并读取深度缓冲区。

     阴影的所有计算取决于深度缓存的精确度(应该是这么理解)。由于低精度,在对没有阴影的区域做相等运算时可能会产生很多不精确的结果。这个与对浮点数的相等运算时候采用“==”是相同的原因。所以我们以光源的位置作为视点绘制场景的时候,让Opengl剔除前向表面。程序会根据物体的后向表面绘制阴影贴图,此时存储在阴影贴图中的深度值会比由光源一点看向场景的时候的可见面的深度值要大。对于没有阴影的点标记为D>=R,对于光源的所有可见面都没有阴影。相对于光源后向面存在一定的精度问题,但是此时后相面都将会在阴影中,所以比较结果无关紧要。

     这项技术只能处理物体是封闭的场景,对于物体不是封闭的场景,你应该使用多边形偏移替代深度测设。

     为了简单,我们将将第一阶段绘制到后缓冲区。这意味着我们的窗口必须足够大,以适应它的阴影贴图,并且窗口必须不被其他物体阻挡。这些约束我们可以通过采用一个离线的pbuffer来生成阴影贴图。

     两外的两个过程是以照相机的设点来绘制的。首先,我们将在一个昏暗的灯光下绘制整个场景,将显示是否有阴影。理论上,这个过程我们紧紧用一个环境光光源来绘制整个场景。但是,为了使有阴影的曲面显示不会不正常,我们采用一个昏暗的漫反射光源来代替。

      第三个过程就是上面提到的比较。这一步中的比较对于shadowmap而言至关重要,硬件对于每一个像素都会使用opengl的扩展机制进行比较。我们通过比较可以影响到我们创建的贴图的alpha以及颜色值。对于测试失败(R>D)的片段,像素的alpha的值将被设为0,通过的片段的alpha的值被设为1。通过alpha测试,我们可以忽略那些在阴影中的片段。这个时候,使用一个明亮的镜面光光源绘制场景。

     对阴影测试后的深度贴图的值通过线性过滤器进行进一步的过滤。采用的线性过滤器叫做“Percentage Closer Filtering(PCF)”,经过过滤后会产生较为柔和的阴影边界。如果我们允许alpha值较小的片段通过测试,那么,被光照射到的通过阴影贴图修改的片段可以比那些在帧缓存中的阴影区的像素更黑一些。这样就在阴影的边界产生一个黑边,所以,在这个Demo当中,alpha测试时用来忽略所有并完全照亮的区域。黑色边框可以通过对第二个步骤做进一步复杂的处理来得到。在我的阴影贴图工程中,Max blending用来合并最后的结果。为了使这个教程尽量简单,PCF没有被采用。(翻译不准确)

投射纹理:

在以摄像机作为视点渲染场景时,如何将使用光源作为视点时的深度缓存的值当做一个纹理使用在场景中的物体上呢?

首先,我们来看一下这个demo中包含的坐标与矩阵变换:

 2011032816102690.jpg

阴影图是对于光源裁剪空间的2d投影的快照。为了执行纹理投影,我们将使用OpenGl的EYE_LINEAR 纹理坐标生成,可以生成一个基于Eye-Space的纹理坐标。我们需要通过纹理矩阵将这些生成的纹理坐标映射到阴影图的适当的地址。纹理矩阵因此执行上面的绿色箭头标志的操作。

做好的办法就是使用:

 2011032818534510.jpg

其中:

2011032818503359.jpg

需要注意Opengl对于纹理坐标T应用矩阵的时候是用矩阵做成纹理坐标MT,通过这个变换可以将相机空间转换到光源的裁剪空间(中间先转换到世界空间然后再到光源空间)。这些变换不经过物体空间不使用模型坐标系,所以对模型不需要重新计算。

当贴图的纹理坐标在光源的裁剪空间内后还需要执行最后一步操作。通过透视除法以后,裁剪空间的X、Y于Z的坐标都在-1至1范围之内。纹理映射将X,Y的坐标映射到【0,1】,存储的深度值也是【0,1】。我们需要使用一个矩阵将这些值映射到【-1,1】。然后做成纹理矩阵。

事实上,在做变换的时候可以完全避开纹理矩阵。我们可以在使用EYE_LINEAR生成纹理坐标的时候指定一个矩阵。生成纹理坐标的代码如下:

glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGenfv(GL_S, GL_EYE_PLANE, VECTOR4D(1.0f, 0.0f, 0.0f, 0.0f)); glEnable(GL_TEXTURE_GEN_S);

如果我们看向视平面的纹理坐标,他们正好形成了一个4X4的单位矩阵。纹理坐标的生成是基于一个纹理生成矩阵,通过纹理矩阵控制。我们可以忽略纹理矩阵使用,直接将其放入视平面,从而达到小小的提速。

最后一步,投影中运算耗费最多的是如何计算相机视图矩阵的逆矩阵。这些操作Opengl也会帮我们完成(多么幸运!!)。当视平面确定以后,GL会自动的后乘当前模型视图矩阵的逆矩阵。我们需要做的就是确保这个时候模型视图矩阵包含相机视图矩阵。此时相机的视图矩阵的逆矩阵就会被乘到我们的纹理生成矩阵之上。

所以最终的设置纹理投影的代码如下:

//Calculate texture matrix for projection
//This matrix takes us from eye space to the light's clip space
//It is postmultiplied by the inverse of the current view matrix when specifying texgen

static MATRIX4X4 biasMatrix
(0.5f, 0.0f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 0.5f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f);

MATRIX4X4 textureMatrix=biasMatrix*lightProjectionMatrix*lightViewMatrix;

//Set up texture coordinate generation.
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGenfv(GL_S, GL_EYE_PLANE, textureMatrix.GetRow(0));
glEnable(GL_TEXTURE_GEN_S);

glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGenfv(GL_T, GL_EYE_PLANE, textureMatrix.GetRow(1));
glEnable(GL_TEXTURE_GEN_T);

glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGenfv(GL_R, GL_EYE_PLANE, textureMatrix.GetRow(2));
glEnable(GL_TEXTURE_GEN_R);

glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGenfv(GL_Q, GL_EYE_PLANE, textureMatrix.GetRow(3));
glEnable(GL_TEXTURE_GEN_Q);

使用扩展:

在这个工程中只用到了两个扩展ARB_depth_texture与ARB_shadow。

ARB_depth_texture提供了一个新的纹理格式用来存储深度缓存。使用DEPTH_COMPONENT格式用来存储一个纹理,我们将得到一个与深度缓存相同精度的单通道纹理。举例来说,如果我们有一个24位的深度缓存,一个拥有DEPTH_COMPONENT的纹理也具有24位,存储的值域深度缓存的值是一样的。

对于RGB纹理,我们可以调用CopyTex【Sub】Image2D用来拷贝帧缓存中的数据到一张纹理。当我们使用深度纹理时,这些信息会被自动的从深度缓存中拷贝出而不是颜色缓存。显卡自己的拷贝将会将深度缓存的数据读取到内存,然后存储为一张纹理。

ARB_shadow提供了自动的纹理比较。

相对于自己写大量的代码用来初始化扩展的Opengl函数指针,我们采用一个Ben Woodhouse写的扩展在入库“Glee”,他将会为我们完成我们需要做的工作,你可以从http://elf-stone.com/glee.php下载,本教程也提供最新的版本。

代码:

本教程需要的代码非常少,原因如下:

没有必要在场景中添加一些人工的模型,因为shadowmap并不需要获得模型的轮廓以及一些额外的顶点信息比如切线等,此教程中的模型全部用gluSolidSphere或者相似的命令绘制。

最主要的工作由硬件来完成,阴影图的比较只需要几行代码就可以自动的执行。

在我的Opengl  shadow mapping Demo中阴影贴图使用8位的精确度以使那些不支持Opengl扩展的硬件也可以运行程序。这可能需要一些一些额外的工作。8位的精确度也能使很多效果能够展现但是意味着最小的聚光灯区域。现在硬件阴影贴图很多都支持,使用非常方便,精度也很高,只需要很少的调用。

因为代码非常简洁易懂,我在原本的Opengl程序之上添加了几个简单的类。TIMER类使用timeGetTime实现了一个简单的定时器。时间可以暂停,并可以返回离上次设置的时间(毫秒级),我们使用这个让场景以一个恒定的速度运行而并不是依赖于帧率。

FPS_COUNTER实现了一个简单的帧计数器,可以让你看到这个demo正在运行。他会记录每一秒的帧数,并且每一秒都会更新。

在scene.cpp中的DrawScene函数,绘制了我们想要绘制的场景。它以一个浮点数作为参数,用来旋转场景中的球体。

Void DrawScene(float angle)

{

首先我们创建三个无符号整形用来存储三个显示列表,每一个显示列表都用来绘制场景的一个部分。因为每个变量都被声明为静态的,所以在程序运行之后,他们会保持他们自己的额值。

//Display lists for objects

Static Gluint sphersLists =0, torusList =0, baseList =0;

如果变量“SphereList”的值为0,那么我将调用glGenLists函数生成一个新的显示列表存储到sphereList。这是变量的值将不为0,所以以下的代码只有在程序的第一次运行的时候才会被调用。显示列表中共绘制了四个球体。

    //Create spheres list if necessary
    if(!spheresList)
    {
        spheresList=glGenLists(1);
        glNewList(spheresList, GL_COMPILE);
        {
            glColor3f(0.0f, 1.0f, 0.0f);
            glPushMatrix();

            glTranslatef(0.45f, 1.0f, 0.45f);
            glutSolidSphere(0.2, 24, 24);

            glTranslatef(-0.9f, 0.0f, 0.0f);
            glutSolidSphere(0.2, 24, 24);

            glTranslatef(0.0f, 0.0f,-0.9f);
            glutSolidSphere(0.2, 24, 24);

            glTranslatef(0.9f, 0.0f, 0.0f);
            glutSolidSphere(0.2, 24, 24);

            glPopMatrix();
        }
        glEndList();
    }

同样的方法我们生成一个圆环以及平面的显示列表:

//Create torus if necessary
    if(!torusList)
    {
        torusList=glGenLists(1);
        glNewList(torusList, GL_COMPILE);
        {
            glColor3f(1.0f, 0.0f, 0.0f);
            glPushMatrix();

            glTranslatef(0.0f, 0.5f, 0.0f);
            glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
            glutSolidTorus(0.2, 0.5, 24, 48);

            glPopMatrix();
        }
        glEndList();
    }

    //Create base if necessary
    if(!baseList)
    {
        baseList=glGenLists(1);
        glNewList(baseList, GL_COMPILE);
        {
            glColor3f(0.0f, 0.0f, 1.0f);
            glPushMatrix();

            glScalef(1.0f, 0.05f, 1.0f);
            glutSolidCube(3.0f);

            glPopMatrix();
        }
        glEndList();
    }

这个时候我们就可以通过调用显示列表来绘制场景,根据“angle“旋转球体。每一刻都调用这个函数,这是程序中唯一(时时刻刻)执行的一部分。

//Draw objects
    glCallList(baseList);
    glCallList(torusList);

    glPushMatrix();
    glRotatef(angle, 0.0f, 1.0f, 0.0f);
    glCallList(spheresList);
    glPopMatrix();
}

现在我们来看看主要代码存在的源文件。

首先我要需要包含相关的头文件,其中有“Glee.h“扩展库的头文件:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include "GLee/GLee.h" //GL header file, including extensions
#include <GL/glut.h>
#include "Maths/Maths.h"
#include "TIMER.h"
#include "FPS_COUNTER.h"
#include "scene.h"
#include "main.h"

现在我们来创建全局对象,计时器以及帧率计时器:

//Timer used for frame rate independent movement
TIMER timer;

//Frames per second counter
FPS_COUNTER fpsCounter;

接下来我们创建按一些全局变量,相机与光源的位置,同时设置阴影贴图的打下为512X512,,并且创建存储阴影贴图纹理的标志器,还有存储相机与光源的投影以及视图的矩阵

//Camera & light positions
VECTOR3D cameraPosition(-2.5f, 3.5f,-2.5f);
VECTOR3D lightPosition(2.0f, 3.0f,-2.0f);

//Size of shadow map
const int shadowMapSize=512;

//Textures
GLuint shadowMapTexture;

//window size
int windowWidth, windowHeight;

//Matrices
MATRIX4X4 lightProjectionMatrix, lightViewMatrix;
MATRIX4X4 cameraProjectionMatrix, cameraViewMatrix;

Demo用Init函数来做一些初始化动作。

//Called for initiation
bool Init(void)
{

首先我们使用Glee库检查ARB_depth_texture与ARB_Shadow是否支持

 //Check for necessary extensions
    if(!GLEE_ARB_depth_texture || !GLEE_ARB_shadow)
    {
        printf("I require ARB_depth_texture and ARB_shadow extensionsn\n");
        return false;
    }

    接下来初始化视图模型矩阵,以及阴影与深度测试。我们同时启用了后相面剔除用来提升速度。因为我们使用了glScale,所以启用GL_NORMALIZE.

    //Load identity modelview
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    //Shading states
    glShadeModel(GL_SMOOTH);
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    //Depth states
    glClearDepth(1.0f);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_DEPTH_TEST);

    glEnable(GL_CULL_FACE);

    //We use glScale when drawing the scene
    glEnable(GL_NORMALIZE);

接下来的步骤就是创建阴影贴图纹理。它的格式是“DEPTH_COMPONENT”,大小是上面设置的大小的二次方。我们并不想对纹理的数据进行任何设置,所以可以设为NULL。

   //Create the shadow map texture
    glGenTextures(1, &shadowMapTexture);
    glBindTexture(GL_TEXTURE_2D, shadowMapTexture);
    glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowMapSize, shadowMapSize, 0,
        GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

我们想在场景中可以方便的更改物体的漫反射与环境光的材质,所以我们使用glColorMatrial函数当改变颜色时,可以改变物体的材质。

我们假定所有的表main都有一个白色的镜面反射属性以及镜面反射系数是16。

 //Use the color as the ambient and diffuse material
    glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
    glEnable(GL_COLOR_MATERIAL);

    //White specular material color, shininess 16
    glMaterialfv(GL_FRONT, GL_SPECULAR, white);
    glMaterialf(GL_FRONT, GL_SHININESS, 16.0f);

接下来将设置相机与光源的矩阵并存在全局变量中。

首先我们存储当前的模型视图矩阵,对于每一个我们想设置的矩阵,我们首先载入单位矩阵,然后再在模型视图矩阵堆栈的栈顶使用相关的opengl函数创建相关的矩阵。然后将这些值读入相关的全局矩阵变量中。最后,恢复模型视图矩阵。

注意我们在模型视图矩阵堆栈上面创建了所有的矩阵,包括投影矩阵。这就是为什么GetFloatv总是返回模型视图矩阵。

光源与相机拥有不同的投影矩阵。

为了能够得到更高的精度,光源的的近平面与远平面应该尽可能得考进。光源拥有的角度为1,使他的视锥体是一个被截断的方形金字塔。

//Calculate & save matrices
    glPushMatrix();

    glLoadIdentity();
    gluPerspective(45.0f, (float)windowWidth/windowHeight, 1.0f, 100.0f);
    glGetFloatv(GL_MODELVIEW_MATRIX, cameraProjectionMatrix);

    glLoadIdentity();
    gluLookAt(cameraPosition.x, cameraPosition.y, cameraPosition.z,
    0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f);
    glGetFloatv(GL_MODELVIEW_MATRIX, cameraViewMatrix);

    glLoadIdentity();
    gluPerspective(45.0f, 1.0f, 2.0f, 8.0f);
    glGetFloatv(GL_MODELVIEW_MATRIX, lightProjectionMatrix);

    glLoadIdentity();
    gluLookAt( lightPosition.x, lightPosition.y, lightPosition.z,
    0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f);
    glGetFloatv(GL_MODELVIEW_MATRIX, lightViewMatrix);

    glPopMatrix();

最后我们设置计时器并返回真值

    //Reset timer
    timer.Reset();

    return true;
}

Display函数用来绘制一帧

/Called to draw scene
void Display(void)
{

首先我们计算求得角度。球转动的速度依赖于时间,并不依赖于帧率。

    //angle of spheres in scene. Calculate from time
    float angle=timer.GetTime()/10;

第一个过程,我们从光源的视图绘制场景,清空颜色与深度缓存,并且设置哪些相机的矩阵。使用一个与阴影贴图相同大小的视口。

//First pass - from light's point of view
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(lightProjectionMatrix);

    glMatrixMode(GL_MODELVIEW);
    glLoadMatrixf(lightViewMatrix);

    //Use viewport the same size as the shadow map
    glViewport(0, 0, shadowMapSize, shadowMapSize);

然后使Opengl启用前向面剔除,所以后向面将会被绘如阴影贴图。这用来解决上面提到的精度问题。因为我只对深度缓存感兴趣,所以禁用了颜色缓存,并使用flat shading。

  //Draw back faces into the shadow map
    glCullFace(GL_FRONT);

    //Disable color writes, and use flat shading for speed
    glShadeModel(GL_FLAT);
    glColorMask(0, 0, 0, 0);

We are now ready to draw the scene.

    //Draw the scene
    DrawScene(angle);

CopyTexSubImage2D用来拷贝帧缓存中的内容到纹理,首先我们绑定阴影贴图纹理,然后拷贝视口的内容到这张纹理。因为这张纹理的格式是DEPTH_COMPONNET,所以数据会自动从深度缓存中读取。

  //Read the depth buffer into the shadow map texture
    glBindTexture(GL_TEXTURE_2D, shadowMapTexture);
    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, shadowMapSize, shadowMapSize);

最后,恢复我们启用的哪些状态

    //restore states
    glCullFace(GL_BACK);
    glShadeModel(GL_SMOOTH);
    glColorMask(1, 1, 1, 1);

第二个过程,我们以相机为视点绘制整个场景。可以设置明亮的光源与阴影区域。首先清除深度缓冲区,没有必要清除颜色缓冲区,以为它尚未被写入。设置相机的相应的矩阵,然后使用一个视口覆盖整个窗口。

    //2nd pass - Draw from camera's point of view
    glClear(GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(cameraProjectionMatrix);

    glMatrixMode(GL_MODELVIEW);
    glLoadMatrixf(cameraViewMatrix);

    glViewport(0, 0, windowWidth, windowHeight);

光源此时应给为没有阴影的区域设置,我们使用跟一个昏暗的漫散射光源。

    //Use dim light to represent shadowed areas
    glLightfv(GL_LIGHT1, GL_POSITION, VECTOR4D(lightPosition));
    glLightfv(GL_LIGHT1, GL_AMBIENT, white*0.2f);
    glLightfv(GL_LIGHT1, GL_DIFFUSE, white*0.2f);
    glLightfv(GL_LIGHT1, GL_SPECULAR, black);
    glEnable(GL_LIGHT1);
    glEnable(GL_LIGHTING);

DrawScene(angle);

第三个过程是计算阴影的部分。如果一个片段通过了阴影测试(没有阴影的部分)此时我们希望照亮他,此时对昏暗的像素重新照亮,启用镜面光。打到明亮的效果。

  //3rd pass
    //Draw with bright light
    glLightfv(GL_LIGHT1, GL_DIFFUSE, white);
    glLightfv(GL_LIGHT1, GL_SPECULAR, white);

接下来,我们计算阴影生成矩阵。这个矩阵将要用来将阴影映射到场景,并启用纹理坐标生成。

    //Calculate texture matrix for projection
    //This matrix takes us from eye space to the light's clip space
    //It is postmultiplied by the inverse of the current view matrix when specifying texgen
    static MATRIX4X4 biasMatrix(0.5f, 0.0f, 0.0f, 0.0f,
    0.0f, 0.5f, 0.0f, 0.0f,
    0.0f, 0.0f, 0.5f, 0.0f,
    0.5f, 0.5f, 0.5f, 1.0f); //bias from [-1, 1] to [0, 1]
    MATRIX4X4 textureMatrix=biasMatrix*lightProjectionMatrix*lightViewMatrix;

    //Set up texture coordinate generation.
    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
    glTexGenfv(GL_S, GL_EYE_PLANE, textureMatrix.GetRow(0));
    glEnable(GL_TEXTURE_GEN_S);

    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
    glTexGenfv(GL_T, GL_EYE_PLANE, textureMatrix.GetRow(1));
    glEnable(GL_TEXTURE_GEN_T);

    glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
    glTexGenfv(GL_R, GL_EYE_PLANE, textureMatrix.GetRow(2));
    glEnable(GL_TEXTURE_GEN_R);

    glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
    glTexGenfv(GL_Q, GL_EYE_PLANE, textureMatrix.GetRow(3));
    glEnable(GL_TEXTURE_GEN_Q);

现在我们绑定并且启用纹理贴图,并且设置自动比较。首先我们启用比较,告诉GL如果r小于或者等于纹理中存储的值则产生一个真值,阴影比较对每一个片段产生一个非0即1的值。我们然GL复制其中的全部4个通道,产生强度的效果

 //Bind & enable shadow map texture
    glBindTexture(GL_TEXTURE_2D, shadowMapTexture);
    glEnable(GL_TEXTURE_2D);

    //Enable shadow comparison
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE_ARB, GL_COMPARE_R_TO_TEXTURE);

    //Shadow comparison should be true (ie not in shadow) if r<=texture
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);

    //Shadow comparison should generate an INTENSITY result
    glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE_ARB, GL_INTENSITY);

如果阴影测试通过,将会产生一个值为1的值,所以我们忽略那些alpha值小于0.99的片段。用这种方法,那些阴影测试失败的片段将会不被显示,上一阶段的结果将显示很暗。

  //Set alpha test to discard false comparisons
    glAlphaFunc(GL_GEQUAL, 0.99f);
    glEnable(GL_ALPHA_TEST);

场景最后一次绘制,一些状态被重置。

    DrawScene(angle);

    //Disable textures and texgen
    glDisable(GL_TEXTURE_2D);

    glDisable(GL_TEXTURE_GEN_S);
    glDisable(GL_TEXTURE_GEN_T);
    glDisable(GL_TEXTURE_GEN_R);
    glDisable(GL_TEXTURE_GEN_Q);

    //Restore other states
    glDisable(GL_LIGHTING);
    glDisable(GL_ALPHA_TEST);

为了能够看到这个demo在你的计算机上运行的流畅程度,我们在窗口的左上角湿湿的为你显示当前的帧率。为了实现这个功能,我们调用FPS_COUNTER::Update用来计算每秒的帧率。

//Update frames per second counter
    fpsCounter.Update();

Sprint用来将帧率得到的数字转换成字符串。

   //Print fps
    static char fpsString[32];
    sprintf(fpsString, "%.2f", fpsCounter.GetFps());

接下来,投影与模型矩阵将被设置为一个正交投影。原来的矩阵将被保存入矩阵堆栈。

    //Set matrices for ortho
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(-1.0f, 1.0f, -1.0f, 1.0f);

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

现在我们可以使用glut提供的函数打印帧率的文本。

  //Print text
    glRasterPos2f(-1.0f, 0.9f);
    for(unsigned int i=0; i<strlen(fpsString); ++i)
        glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, fpsString[i]);

原来的投影与模型矩阵现在可以被恢复。

    //reset matrices
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();

我们需要结束这一帧得得绘制,调用glFinesh(),并且调用glut的函数来交换前后缓存。最后调用glutPostRedisplay用来要求尽快的绘制下一帧。

当窗口的大小变化时Reshap函数被调用(包括窗口创建的时候),首先保存窗口的大小到全局变量,这样视口在刚开始调用第二次绘制过程的时候的时候就可以被重新确立。

//Called on window resize
void Reshape(int w, int h)
{
    //Save new window size
    windowWidth=w, windowHeight=h;

当窗口大小变化的时候相机的投影矩阵也跟着变化,因为这个矩阵存储在一个全局变量当中,所以我们向创建他时那样更新它。我们保存当前的模型视图矩阵,然后载入单位矩阵,这样新的相机投影矩阵机会被创建并且保存,然后恢复旧的模型视图矩阵。

    //Update the camera's projection matrix
    glPushMatrix();
    glLoadIdentity();
    gluPerspective(45.0f, (float)windowWidth/windowHeight, 1.0f, 100.0f);
    glGetFloatv(GL_MODELVIEW_MATRIX, cameraProjectionMatrix);
    glPopMatrix();
}

当按键按下时KeyBoard函数被调用

//Called when a key is pressed
void Keyboard(unsigned char key, int x, int y)
{
    //If escape is pressed, exit
    if(key==27)
    exit(0);

    //Use P to pause the animation and U to unpause
    if(key=='P' || key=='p')
    timer.Pause();

    if(key=='U' || key=='u')
    timer.Unpause();
}

Main函数初始化glut与窗口,然后调用初始化函数。

int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowSize(640, 512);
    glutCreateWindow("Shadow Mapping");

    if(!Init())
        return 0;

    glutDisplayFunc(Display);
    glutReshapeFunc(Reshape);
    glutKeyboardFunc(Keyboard);
    glutMainLoop();
    return 0;
}

结论:

我希望这篇教程能够引领你进入shadow map的大门。还有很多方式可以改进这种方法。包括透视阴影贴图用来降低走样Dual paraboloid shadow用来减少场景对于点光源生成深度图的渲染过程次数。虽然模板阴影shadowVolume存在很多缺点,但是是当下主流的技术,但是随着场景复杂度的提高,这种局面可能会改变。

下载:

你可以从这里下载exe与源码:

http://www.paulsprojects.net/tutorials/smt/smt.zip         

转载于:https://www.cnblogs.com/cg_ghost/archive/2011/03/28/1997818.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值