[OpenGL] shadow mapping(实时阴影映射)

source:原文地址

code:点击可以直接下载源代码

1978年,Lance Williams在其发表的论文《Casting curved shadows on curved surfaces》中提出了Shadow mapping算法,从那以后,该算法在离线渲染和实时渲染两个领域都得到了广泛的应用。皮尔斯动画工作室的Renderman渲染器、以及一些知名电影如《玩具总动员》都使用了shadow mapping技术。

在众多图形应用的阴影技术中,shadow mapping只是产生阴影的一种方法,它有着自身的优缺点:

优点:

1.不需要了解场景中的物体,因为shadow mapping是图像空间的技术,它会自动地随着GPU上物体的创建和改变发挥效应。

2.对于每个灯光而言,只需要一张纹理来维护阴影信息,而不需要使用模板缓冲区。

3.避免了shadow volumes算法的高填充率的缺陷

缺点:

1.易走样,尤其在使用小的阴影图的情况下。

2.在每个灯光下,场景中的物体必须都进行一次绘制,以产生点光源的阴影贴图,而对于全向点光源,绘制的次数会更多。

该教程只实现了一个点光源的基本阴影映射,也有很多人发表了如何发展改进这一技术的论文。

理论

        考虑一个硬阴影效果下由点光源照亮的简单场景,在给定场景中一个点的情况下,我们如何知道它是被照亮的部分,还是在阴影中?简单的说,如果场景中的一个点和光源的连线中间没有其它遮挡物的话,那么这个点就是被照亮的。理解shadow mapping的关键步骤就是,理解这些点就是以光源为视点下的可见点。

我们已经掌握了在给定视点下判断可见性的技术,并且几乎在任何使用3d硬件绘制场景时都会用到这一技术,这个技术就是z-buffer消隐算法。所以,以光源为视点绘制场景的情况下,可以通过深度测试的点就是那些不在阴影中的点。

如果我们以光源为视点绘制场景,我们可以先保存深度缓冲区的值,然后,我们再以摄像机为视角绘制场景,我们将保存的深度缓冲区的值从光源位置投影为纹理。对于给定的一个点而言,我们比较比较投影到该点的阴影以及该点到光源的距离,来计算该点是否在阴影中。

假设深度纹理中存储的值为D,点到光源的距离为R:

R = D 光源与点的连线中没有物体遮挡,该点不在阴影中
R > D 光源与点的连线中有物体遮挡,该点在阴影中

应用

        我们如何使用OpenGL来实现上述过程?

这个技术要求我们至少绘制两遍场景,为了保证每次绘制更简单,我们绘制了三次。

首先,我们以光源为视点绘制场景。这个可以通过在gluLookAt函数中,设置从光源位置望向场景中心。场景如常绘制,并且读取深度缓存。

所有的阴影计算都是在深度缓存的精度下进行的。使用相等来测试不在阴影中的点,很可能会因为精度误差而产生不正确的结果,这和我们不使用”==“来比较浮点数是一个道理。所以,当我们从光源视角绘制场景时,我们要求OpenGL剔除前向面,因此被写入阴影映射的是物体的背面。这种情况下,存储在阴影映射中的深度值会比光源照射到正面的深度值要大一些。我们使用D>=R来检测不在阴影中的点,所有光源下表面可见的点都不在阴影中。这样我们就利用背面(在光源视角下)把这个问题转化为精确的问题,因为它们仍然符合阴影的定义,所以不会对比较结果产生影响。

这个技术只针对封闭物体,如果场景中存在开放的物体,我们需要使用多边形位移技术来增加深度缓冲区的值。

为了简化这一问题,我们在第一次绘制的时候使用标准的后缓存,这意味着我们的窗口大到足以放下整个阴影纹理,并且不被其他窗体遮挡。这个限制可以通过使用离线缓存来产生纹理以避免。

另外两次绘制都是从相机视角绘制的。首先,我们用一个比较昏暗的灯光绘制整个场景,来表达阴影中显示的效果。这一次仅用环境光来绘制。然而,为了保证阴影中的曲面表面不产生不自然的平坦,我们使用了较暗的漫反射光源。

第三次绘制的就是我们前面提到的阴影比较。这个比较是shadow mapping中至关重要的一点,它事实上可以直接利用硬件来逐像素的进行比较,使用ARB提供的扩展,ARB_shadow。我们设置纹理单位,这样比较就能影响到alpha值以及颜色成分了。所有的片段如果不能通过比较,将会得到alpha为0的值,而通过比较的则会得到alpha为1的值。利用alpha测试,我们可以丢弃那些本该是阴影的片段。现在使用更亮的管线来允许我们绘制场景中被光照到的部分。

使用深度纹理的线性滤波,可以过滤纹理比较后产生的值,这叫做PCF,它能够得到边缘的软阴影。如果我们允许更小的alpha值通过alpha测试,那么被照亮的片段将和阴影混合,可能会比帧缓存中的像素更暗,这样就产生了阴影边界的黑色边框。所以,在这个样例中,alpha测试被用于丢弃所有不被完全照射的区域。暗边界的产生将会使用另一种不同的、更为复杂的方法来实现这两次绘制。在我们的shadow mapping工程里,使用了max混合来合并结果。为了保证简单,我们没有使用PCF技术。

投影纹理

我们如何将光源下的深度缓存,保存在阴影中,并且在摄影机视角渲染到场景物体中呢?

首先,我们来看一下我们使用的坐标系和矩阵:

这个阴影映射是光源视角的一个快照,是光源裁剪空间的二维投影。为了实现纹理投影,我们使用opengl中的 EYE_LINEAR纹理坐标生成,来产生视点位置下顶点的坐标。我们需要使用纹理矩阵,将纹理坐标映射为一个适合于访问阴影图的坐标。因此纹理矩阵需要完成上述绿色箭头标出操作。

实现这一过程的最好方式是:

T 纹理矩阵
Pl 光源投影矩阵
Vl 光源视点矩阵
Vc 相机视点矩阵

OpenGL将一个矩阵M以MT的方式应用于一个矩阵坐标T,这将通过世界空间和光源视点空间,把相机坐标空间转换到光源裁剪空间。这避免了物体空间以及任何模型矩阵的使用,并且因此不再需要对我们绘制的每个模型重复运算。

当纹理坐标被转换到光源裁剪空间后,我们还要执行一步操作。在透视除法后,裁剪后的x,y,z坐标的范围落在[-1,1]内,而纹理映射的坐标范围在[0,1]中,而深度值也在[0,1]范围内。我们需要产生一个简单的矩阵,来把[-1,1]映射到[0,1]上。对于每个X,Y,Z坐标,我们都要把我们的纹理矩阵上左乘这个矩阵。

在映射的过程中,我们可以避免使用纹理矩阵。这个可以通过在我们打开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);

如果我们同时考虑这四个纹理坐标的观察屏幕,它们形成了4 x 4的单位矩阵。纹理矩阵将这些”texgen“矩阵的基础上产生,然后将使用纹理矩阵来操作。我们可以通过忽略纹理矩阵,而把我们将使用到的纹理矩阵使用的数据直接放置到观察平面上,来实现一个小的加速。

最终,大多数设置投影的复杂运算就是计算Vc的逆矩阵了。OpenGL会为我们完成这一操作。当观察平面被确定后,GL将会自动地将其与当前模型矩阵相乘。我们所需要做的只是保证在这个时候,模型矩阵包括了摄像机视角矩阵,其逆将与我们的texgen矩阵相乘。

所以,最终设置纹理投影,包括优化的代码如下:

//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_texure和ARG_shadow.

代码

这个实例使用的代码量非常小,主要原因如下:

1.我们不需要手动的生成任何特定几何体,因为shadow mapping算法不需要提取轮廓线,也不需要其它附加的顶点性质,如切向量。

所有的几何体都是使用glutSolidSphere和一些简单的指令绘制的。

2.主要的工作都是由硬件完成的。

shadow mapping比较只需要几行代码来允许硬件操作,然后它就会被自动执行。

首先我们创建3个无符号整数来维护显示列表的编号。一个显示列表用于绘制场景的一部分,由于变量被定义为静态的,我们可以在使用前获得变量的值。

    //Display lists for objects
    static GLuint spheresList=0, torusList=0,baseList=0;

   如果变量sphereList为0,我们使用glGenLists,在spheresList中保存新的显示列表编号。新的显示列表编号不为0,因此代码只会被执行一次。这样就将OpenGL产生4个球体的指令存储在了显示列表中。

 

   //Create spheres list ifnecessary
    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();
    }

现在,我们使用显示列表来绘制随着angles变化而旋转的球,每一次这个函数被调用后,除了第一次调用,这是唯一被执行的部分:

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

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

现在,我们来看看主要的代码文件,这里面包含了所有有趣的代码。

首先我们需要包含必要的头文件,包括glee.h,一个opengl的扩展库。

#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"

现在创建我们的全局对象,计时器和fps。

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

//Frames per second counter
FPS_COUNTER fpsCounter;

之后,我们创建一些全局变量,相机和灯光的位置是在这里给出的固定值。我们同时把shadow map的大小固定设为512x512,并且存储创建shadow map纹理编号的空间。另外,我们还创建空间来维护相机以及光源视角下的投影和视区矩阵。

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

初始化函数也会在代码的开始被调用:

//Called for initiation
bool Init(void)
{

首先我们使用glee库来检查ARB_depth_texture和ARB_shadow扩展是否被支持

    //Check for necessaryextensions
    if(!GLEE_ARB_depth_texture || !GLEE_ARB_shadow)
    {
        printf("I requireARB_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);

下一步就是创建阴影映射纹理。这是一张尺寸为shadowMap大小的方形纹理,并且格式为DEPTH_COMPONENT,我们不希望用任何东西来初始化这个阴影数据,所以我们把它的像素指针指向NULL。

    //Create the shadow maptexture
    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);

    我们想要一个改变场景中物体的漫反射和环境反射材质属性的更简单的方法,所以我们使用了glColorMaterial,所以颜色的改变也会影响材质。

    我们设置所有表面的镜面反射颜色为白色,镜面反射指数为16.

    //Use the color as theambient 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 & savematrices
    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;
}

显示函数在绘制帧的时候被调用。

//Called to draw scene
void Display(void)
{

首先,我们计算球体的旋转角度。使用计时器,旋转率将与帧刷新频率独立开来。

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

第一次绘制的时候,我们从光源视点来绘制场景。清空颜色和深度缓存,并且设置光源的投影矩阵和模型矩阵。使用和shadow map一样大小的作为窗口大小。

    //First pass - from light'spoint 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为我们剔除正面,所以背面被绘制到shadow map中。这个处理方法在前面已经解释过了。我们同时需要禁用写入颜色缓存,并且使用面片光照。因为我们仅对深度缓存的内容感兴趣。

    //Draw back faces into theshadow map
    glCullFace(GL_FRONT);

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

现在,我们可以开始绘制场景了:

    //Draw the scene
    DrawScene(angle);

CopyTexSubImage2D用于把当前帧缓存的内容复制到纹理中。我们首先绑定阴影映射纹理,然后再把视区复制到纹理。因为我们绑定了一个DEPTH_COMPONENT的纹理,数据将会自动从深度缓存中读入。

    //Read the depth buffer intothe 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 fromcamera's point of view
    glClear(GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(cameraProjectionMatrix);

    glMatrixMode(GL_MODELVIEW);
    glLoadMatrixf(cameraViewMatrix);

    glViewport(0, 0, windowWidth, windowHeight);

接下来设置灯光,我们使用一个较暗的漫反射光以及为0的镜面反射亮度。

    //Use dim light to representshadowed 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);

接下来,我们计算texgen矩阵,它将用于把阴影映射投影到场景上,并且允许纹理坐标生成。

    //Calculate texture matrixfor projection
    //This matrix takes us from eye space to thelight's clip space
    //It is postmultiplied by the inverse of thecurrent 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]
    MATRIX4X4textureMatrix=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个颜色通道(一个复制到4个中,即产生一个灰度值)

    //Bind & enable shadowmap 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 inshadow) if r<=texture
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);

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

如果阴影比较通过了,将会产生为1的alpha值。所以,我们使用alpha测试来剔除那些小于0.99的片段。用这种方法,没有通过阴影测试的片段将不被绘制,所以这就允许了前一次绘制的暗场景效果被显示在屏幕上。

    //Set alpha test to discardfalse 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);

为了显示这个样例在你的电脑上运行状态如何,我们将会在屏幕上方显示fps大小。为了做到这一点,我们首先调用FPS_COUNTER::Update来计算fps。

    //Update frames per secondcounter
    fpsCounter.Update();

sprintf被用于把fps从float转换为string

    //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();

我们现在完成了帧的绘制,所以调用glFinish,然后告诉glut库交换前后缓冲区,最终我们调用glutPostRedisplay来要求下一帧尽可能快地绘制。

    glFinish();
    glutSwapBuffers();
    glutPostRedisplay();
}

reshape函数在窗体大小被改变的时候调用(包括窗口创建时),它首先把当前的窗口大小存储到全局变量中,所以视窗可以在第二次绘制的时候被正确的重建。

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

相机投影矩阵在窗体大小被改变后,也会发生变化。因为它被存储在全局变量中,并且仅在必要的时候发送给GL,我们按我们设置这个变量的方式,来更新这个变量。我们先保存当前的模型矩阵,再把当前矩阵设置为单位矩阵,然后新的相机投影矩阵被创建,并且被存储。最后,我们恢复原来的模型矩阵。

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

键盘回调函数在键盘被按下后调用。如果我们按了escape键,程序将退出。如果按了p键,计时器会停止,动画也将暂停。按u键可以重启计时器。.

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

我们的主函数初始化了glut和窗体,然后调用了init函数,检查是否出错,如果返回false,代码退出。窗体创建得足够容纳512X512的阴影映射。之后我们设置了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;
}
时间: 2024-10-07 16:20:46

[OpenGL] shadow mapping(实时阴影映射)的相关文章

OpenGL 阴影之Shadow Mapping和Shadow Volumes

先说下开发环境.VS2013,C++空项目,引用glut,glew.glut包含基本窗口操作,免去我们自己新建win32窗口一些操作.glew使我们能使用最新opengl的API,因winodw本身只包含opengl 1.1版本的API,根本是不能用的. 其中矩阵计算采用gitHub项目openvr中的三份文件, Vectors.h ,Matrices.h, Matrices.cpp,分别是矢量与点类,矩阵类,我们需要的一些操作,矢量的叉乘和点乘,矩阵转置,矩阵的逆,矩阵与矢量相剩等. 这里主要

OpenGL阴影,Shadow Mapping(附源程序)

实验平台:Win7,VS2010 先上结果截图(文章最后下载程序,解压后直接运行BIN文件夹下的EXE程序): 本文描述图形学的两个最常用的阴影技术之一,Shadow Mapping方法(另一种是Shadow Volumes方法).在讲解Shadow Mapping基本原理及其基本算法的OpenGL实现之后,将继续深入分析解决几个实际问题,包括如何处理全方向点光源.多个光源.平行光.最近还有可能写一篇Shadow Volumes的博文(目前已经将基本理论弄清楚了),在那里,将对Shadow Ma

阴影映射(Shadow Map)的研究(三)

阴影映射(Shadow Map)的研究(三) 最近为了自己制作的项目可是吃了不少苦头,这其中关键的一点就是想要实现阴影映射(Shadow Map).为了实现目标,我参考了网络上很多相关的资料,也看了一些案例,最终花了我一个月的时间将这个效果实现了. 阴影映射这样的效果,其实在即将发布的Qt 3D中已经有相关的介绍,KDAB中有一篇文章<Shadow Mappingin Qt3D 2.0>就在Qt 3D的框架上实现了阴影映射.不过当时这个效果是假定目标机器支持OpenGL 3.0规范的,目前大部

阴影映射(Shadow Map)的研究(五)

阴影映射(Shadow Map)的研究(五) 我成功地将别人的例子加以改进,使用QOpenGLWidget作为渲染窗口,将阴影映射渲染了出来.目前可以确定的是,使用OpenGL ES 2.0作为渲染的接口要求,能够让目前绝大多数机器都能够顺利兼容,但是囿于渲染窗口,可能在某些平台上表现不好.如果移植到Qt Quick 2,这样能够支持的平台就更多了.现在我将这些接口统统使用Qt的方式实现了,移植到Qt Quick 2也很简单. 这里主要参考的是OpenGLUnderQML这个例子,自定义了一个Q

阴影映射(Shadow Map)的研究(四)

阴影映射(Shadow Map)的研究(四) 上一篇文章粗略地介绍了要实现OpenGL ES 2.0的阴影映射所需的知识难点,现在简略地说明一下:1.FBO:2.着色器:3.float的分拆以及组合.上篇文章虽然说已经成功地移植了来自Java编写的Android下阴影映射的效果,但这边采用的很大程度上是OpenGL原生代码编写的内容,接下来的目标是采用自Qt 5起就逐渐采用的Qt对OpenGL的封装类,用面向对象的思维来处理OpenGL对象,这样让代码更加优雅. 1.FBO 首先说一下FBO.在

阴影映射(Shadow Map)的研究(二)

阴影映射(Shadow Map)的研究(二) 上一篇文章介绍了我对Z缓存的较为详细的研究.这里之所以对Ze求导函数,是因为的我们需要寻找它的变化曲线,从而找到极值点,这样就能够确定Ze相对于zw的疏密分布情况.幸运的是,我们找到的导函数是双曲函数,并且我们关心的的右侧是单调递增的. 蒋彩阳原创文章,首发地址:http://blog.csdn.net/gamesdev/article/details/44946763.欢迎同行前来探讨. 引出上一篇文章的结论,当 时,导函数取得最大值.但是在Zw∈

阴影映射(Shadow Map)的研究(六)

阴影映射(Shadow Map)的研究(六) 成功地将阴影映射与Qt Quick 2整合之后,接下来可以将阴影映射的效果变得更漂亮一些.如果你成功地运行过我制作的演示程序,那么就会发现,阴影映射的效果并不是那么理想,可能有噪点(粉刺)的出现.这个是和阴影的产生相关,主要还是由于阴影映射这个算法它要求产生的阴影精度是有限的.很多改进的算法都是围绕着如何让阴影更加自然进行研究的.这里我也尝试模仿了一个稍微简单的算法:PCF算法. PCF算法的理念也比较简单,简言之就是让产生的阴影更加模糊.它主要在阴

【学习笔记】3D图形核心基础精炼版-12:stage3D实战-动态阴影 shadow mapping 和范例工程4

目的: 物体投影在另一个物体身上,而另一个物体可能是平的,但大多数都是不平的多边形物体,这里考虑的是后者,这样可以适用于大多数场合的投影. 预览效果: 原理: 这里使用的是shadow mapping方式,其原理如下: 1.将场景的深度值预先渲染到 以光源位置为原点.光线发射方向为观察方向的投影坐标系中,形成深度纹理. 2.再次渲染场景的过程中,将每个片断(像素)变换到前述眼坐标系中,并缩放到[0,1]的范围内以便查询纹理. 3.以较暗的光照绘制场景 4.以当前片断在眼坐标中的S.T坐标查询深度

opengl 教程(23) shadow mapping (2)

原帖地址:http://ogldev.atspace.co.uk/www/tutorial24/tutorial24.html Background In the previous tutorial we learned the basic principle behind the shadow mapping technique and saw how to render the depth into a texture and later display it on the screen b