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