OpenGL超级宝典学习笔记——深度纹理和阴影
之前我们介绍过简单的把物体压平到投影平面来制造阴影。但这种阴影方式有其局限性(如投影平面须是平面)。在OpenGL1.4引入了一种新的方法阴影贴图来产生阴影。
阴影贴图背后的原理是简单的。我们先把光源的位置当作照相机的位置,我们从这个位置观察物体,我们就知道哪些物体的表面是被照射到(被光源看到)的,哪些是没有被照射到(被遮挡住)的(在某个方向上离光源最近的表面是被照射的,后面的表面则没有被照射到)。我们开启深度测试,这样我们就可以得到一个有用的深度缓冲区数据(每一个像素在深度缓冲区中的结果),然后我们从深度缓冲区中读取数据作为一个阴影纹理,投影回场景中,然后我们在使用照相机的视角,来渲染物体。
光源视角
首先我们把视角移到光源的位置。我们可以通过glu库的辅助函数:
gluLookAt(lightPos[0], lightPos[1], lightPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
把光源的位置设置为观察的位置。
为了以最佳的方式利用空间来产生阴影贴图。从光源的角度看过去的透视可视区域要适应窗口的比例,且透视的最近平面位置是里光源最近的物体的平面,最远的平面位置是离光源最远的物体的平面。这样我们就可以充分的利用场景的信息来填充深度缓冲区,来制造阴影贴图。我们估计恰好包好整个场景的视野。
//场景的半径大小 GLfloat sceneBoundingRadius = 95.0f; //光的距离 lightToSceneDistance = sqrt(lightPos[0] * lightPos[0] + lightPos[1] * lightPos[1] + lightPos[2] * lightPos[2]); //近裁剪平面 nearPlane = lightToSceneDistance - sceneBoundingRadius; //让场景充满整个深度纹理 fieldOfView = (GLfloat)m3dRadToDeg(2.0f * atan(sceneBoundingRadius/lightToSceneDistance)); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(fieldOfView, 1.0f, nearPlane, nearPlane + (2.0f * sceneBoundingRadius));
在上面的代码中,场景的中心位于原点,场景中所有的物体,在以原点为中心,半径为sceneBoundingRadius的圆中。这是我们对场景的粗略估计。大致如下图:
因为我们只需要得到像素经过深度测试后,深度缓冲区的结果。所以我们可以去掉一切不必要的的细节,不往颜色缓冲区中写数据因为不需要显示。
glShadeModel(GL_FLAT); glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); glDisable(GL_NORMALIZE); glColorMask(0,0,0,0); ...
如果我们可以看到深度缓冲区,深度缓冲区的灰度图大概是这样子的。
新型的纹理
我们需要拷贝深度的数据到纹理中作为阴影贴图。在OpenGL1.4之后,glCopyTexImage2D允许我们从深度缓冲区中拷贝数据。纹理数据多了一种深度纹理的类型,其内部格式包括GL_DEPTH_COMPONENT16,GL_DEPTH_COMPONENT24,GL_DEPTH_COMPONENT32,数字代表每个纹理单元包含的位数。一般情况下,我们希望其内部格式与深度缓冲区的精度相匹配。OpenGL允许你指定通用的GL_DEPTH_COMPONENT格式来匹配你的深度缓冲区。在以光源的视角绘制后,我们把深度缓冲区的数据拷贝出来作为深度纹理:
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 0, 0, shadowWidth, shadowHeight, 0);
只有在物体移动或者光源移动时,才需要重新产生深度纹理。如果仅仅是照相机移动,我们并不需要重新产生深度纹理,因为以光源的角度来看,深度纹理没有变化。当窗口的大小改变时,我们也需要产生一个更大的深度纹理。
深度纹理的大小
在OpenGL2.0之前,在不支持非二次幂的纹理(GL_ARB_texture_non_power_of_two)的扩展的情况下,我们需要调整深度纹理的大小,使其恰好为二次幂。例如在1024x768的分辨率下,最大的二次幂纹理大小是1024x512.
void ChangeSize(int w, int h) { windowWidth = shadowWidth = w; windowHeight = shadowHeight = h; //不支持非二次幂纹理大小 if(!nptTextureAvailable) { int i = 0; int j = 0; //获得二次幂的宽度 while((1 << i) <= shadowWidth ) i++; shadowWidth = (1 << (i-1)); //二次幂的高度 while((1 << j) <= shadowHeight ) j++; shadowHeight = (1 << (j-1)); } }
首先绘制阴影
如果阴影被定义为完全没有光照的,那么我们不需要绘制它。例如只有单一的聚光灯作为光源,那让阴影是全黑色的就足以满足我们的要求了。如果我们不希望阴影是全黑的,而且需要阴影区域中的一些细节,那么我们需要在场景中模拟一些环境光。同时,我们还添加一些散射光,帮助传递形状的信息。
GLfloat lowAmbient[4] = {0.1f, 0.1f, 0.1f, 1.0f}; GLfloat lowDiffuse[4] = {0.35f, 0.35f, 0.35f, 1.0f}; glLightfv(GL_LIGHT0, GL_AMBIENT, lowAmbient); glLightfv(GL_LIGHT0, GL_DIFFUSE, lowDiffuse); //在场景中绘制物体 DrawModels()
PS:此时我们并不需要交换缓冲区(swapbuffers).
如果显示出来是这样子的。
有些OpenGL实现支持一种GL_ARB_shadow_ambient扩展,它可以使我们不必进行第一遍的阴影绘图。
然后是光照
目前我们有了一个很昏暗的场景,要制造阴影,我需要一个明亮的光照区域,来与阴影区形成对比。如何决定这个接受更强光照的区域是阴影贴图的关键。在这个明亮的区域,我们用两倍于阴影的光照强度进行绘制。
GLfloat ambientLight[] = { 0.2f, 0.2f, 0.2f, 1.0f}; GLfloat diffuseLight[] = { 0.7f, 0.7f, 0.7f, 1.0f}; ... glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight); glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
这样得到的阴影不全是黑色的。
如果去掉前面的绘制阴影的结果是:
投影阴影贴图
我们的目的是需要把阴影贴图投影到场景中(从照相机的位置看)。投影这些代表着光源到被光照射到的第一个物体的距离的深度值。把纹理坐标重定向到正确的坐标空间需要一些数学知识。之前我们解释了把顶点从物体空间变换到视觉空间,再变换到裁剪空间,然后变换到规格化的设备坐标,最后变换到窗口空间的过程。在这里有两组不同的变换矩阵,一组用于变换到照相机的视觉空间,一组用于变换到光源的视觉空间。通过这两组矩阵变换得到两个从不同角度观察的场景。
上面的箭头表示了我们需要应用到视觉线性纹理坐标的变换过程。纹理的投影通常是从视觉线性坐标的产生开始的。这个过程是自动产生纹理坐标的。不同于物体线性纹理坐标的生成,视觉线性坐标的生成并不固定到任何几何图形之上。反之,它好像是一台投影仪把纹理投影到场景中,想象一下你在投影仪前走动的时候,屏幕上会出现不规则的身体形状。
投影纹理映射:
现在我们获得在照相机的视觉空间下顶点对应的纹理坐标。那我们需要进行一些变换来得到顶点的纹理坐标。当前我们在照相机机的视觉空间,首先我们通过视图矩阵的逆变换回到世界坐标系,然后再变换到光源的视觉空间,然后到光源的裁剪空间。这一系列的变换可以通过下面的矩阵相乘得到:
M = Plight * MVlight * MVcamara-1
裁剪空间规格化后的x,y,z的坐标范围在[-1, 1]之间,然而我们的纹理坐标范围为[0,1],所以我们还需要把[-1,1]变换到[0,1]的范围,这个变换很简单,我们只需要把[-1,1]缩放一半(S),然后偏移0.5就可以得到[0,1]了(B)。
M = B * S * Plight * MVlight * MVcamara-1
所以我们可以得到顶点经过变换后的纹理坐标。T1 = M * T;
图解过程如下:
PS: 当前模型视图矩阵的逆矩阵的乘法操作已经包含在了视觉平面方程式中。
即在OpenGL的纹理自动生成模式GL_EYE_LINEAR中,每一个觉平面方程式(eye plane equation)会自动乘以MVcamara-1
实现上面的步骤一种方式是手动的通过glTranslatef, glScalef, glMultMatrixf 来一步步的实现。另一个方式是在纹理自动生成中,我们可以通过设置一个纹理矩阵来实现上面的变换,把这个纹理矩阵作为视觉线性坐标的视觉平面方程GL_EYE_PLANE即可。
M = B * S * Plight * MVlight 大致代码如下:
M3DMatrix44f tempMatrix; m3dLoadIdentity44(tempMatrix); //偏移0.5 m3dTranslateMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f); //缩放0.5 m3dScaleMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f); //乘以光源投影矩阵 m3dMatrixMultiply44(textureMatrix, tempMatrix, lightProjection); //乘以光源视图矩阵 m3dMatrixMultiply44(tempMatrix, textureMatrix, lightModelView); //矩阵转置,获得平面方程的s,t,r和q行 m3dTransposeMatrix44(textureMatrix, tempMatrix);
应用到视觉平面中:
//因为在当前模型视图矩阵的逆矩阵的乘法操作已经包含在了视觉平面方程式中 //确保在glTexGenfv前已经设置好照相机的模型视图矩阵。 glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); ... //为阴影贴图的投影设置视觉平面 glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); glTexGenfv(GL_S, GL_EYE_PLANE, &textureMatrix[0]); glTexGenfv(GL_T, GL_EYE_PLANE, &textureMatrix[4]); glTexGenfv(GL_R, GL_EYE_PLANE, &textureMatrix[8]); glTexGenfv(GL_Q, GL_EYE_PLANE, &textureMatrix[12]); ... glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
阴影比较
现在我们如何知道从照相机视角看到的点是否在阴影中呢。从上面的那些步骤来看,我们已知顶点的深度纹理坐标,那么这个深度纹理坐标对应的在深度纹理的值我们可以知道即texture[s/q, t/q],这个深度纹理记录了在光的角度看过去离光源最近的点的深度值,我们是设置的深度比较函数是glDepthFunc(GL_LEQUAL);。,同时我们知道(r/q)是顶点在真实光源中深度值,已经通过缩放和偏移变换到了[0,1]的范围。然后我们比较texture[s/q, t/q]和(r/q)如果texture[s/q, t/q] < r/q那么就表示这个点在阴影中。如下图:
深度纹理只包含了一个值代表深度。但在纹理环境的纹理查询中,我们需要返回四个成分的值(RGBA)。OpenGL提供了几种方式把这单个深度值扩展到其他的通道中,其中包含GL_ALPHA(0,0,0,D),GL_LUMINANCE(D,D,D,1)和GL_INTENSITY(D,D,D,D)。在这里我们把深度值扩展到所有的深度通道。
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INSTENSITY);
在OpenGL中开启阴影比较,来产生阴影效果。我们把深度值与纹理坐标的R成分进行比较。
//设置阴影比较
glEnable(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
效果:
书中部分的代码示例:
// Called to regenerate the shadow map void RegenerateShadowMap(void) { GLfloat lightToSceneDistance, nearPlane, fieldOfView; GLfloat lightModelview[16], lightProjection[16]; GLfloat sceneBoundingRadius = 95.0f; // based on objects in scene // Save the depth precision for where it's useful lightToSceneDistance = sqrt(lightPos[0] * lightPos[0] + lightPos[1] * lightPos[1] + lightPos[2] * lightPos[2]); nearPlane = lightToSceneDistance - sceneBoundingRadius; // Keep the scene filling the depth texture fieldOfView = (GLfloat)m3dRadToDeg(2.0f * atan(sceneBoundingRadius / lightToSceneDistance)); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(fieldOfView, 1.0f, nearPlane, nearPlane + (2.0f * sceneBoundingRadius)); glGetFloatv(GL_PROJECTION_MATRIX, lightProjection); // Switch to light's point of view glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(lightPos[0], lightPos[1], lightPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); glGetFloatv(GL_MODELVIEW_MATRIX, lightModelview); glViewport(0, 0, shadowWidth, shadowHeight); // Clear the depth buffer only glClear(GL_DEPTH_BUFFER_BIT); // All we care about here is resulting depth values glShadeModel(GL_FLAT); glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); glDisable(GL_NORMALIZE); glColorMask(0, 0, 0, 0); // Overcome imprecision glEnable(GL_POLYGON_OFFSET_FILL); // Draw objects in the scene except base plane // which never shadows anything DrawModels(GL_FALSE); // Copy depth values into depth texture glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 0, 0, shadowWidth, shadowHeight, 0); // Restore normal drawing state glShadeModel(GL_SMOOTH); glEnable(GL_LIGHTING); glEnable(GL_COLOR_MATERIAL); glEnable(GL_NORMALIZE); glColorMask(1, 1, 1, 1); glDisable(GL_POLYGON_OFFSET_FILL); // Set up texture matrix for shadow map projection, // which will be rolled into the eye linear // texture coordinate generation plane equations M3DMatrix44f tempMatrix; m3dLoadIdentity44(tempMatrix); m3dTranslateMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f); m3dScaleMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f); m3dMatrixMultiply44(textureMatrix, tempMatrix, lightProjection); m3dMatrixMultiply44(tempMatrix, textureMatrix, lightModelview); // transpose to get the s, t, r, and q rows for plane equations m3dTransposeMatrix44(textureMatrix, tempMatrix); } // Called to draw scene void RenderScene(void) { // Track camera angle glMatrixMode(GL_PROJECTION); glLoadIdentity(); if (windowWidth > windowHeight) { GLdouble ar = (GLdouble)windowWidth / (GLdouble)windowHeight; glFrustum(-ar * cameraZoom, ar * cameraZoom, -cameraZoom, cameraZoom, 1.0, 1000.0); } else { GLdouble ar = (GLdouble)windowHeight / (GLdouble)windowWidth; glFrustum(-cameraZoom, cameraZoom, -ar * cameraZoom, ar * cameraZoom, 1.0, 1000.0); } glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); glViewport(0, 0, windowWidth, windowHeight); // Track light position glLightfv(GL_LIGHT0, GL_POSITION, lightPos); // Clear the window with current clearing color glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (showShadowMap) { // Display shadow map for educational purposes glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glEnable(GL_TEXTURE_2D); glDisable(GL_LIGHTING); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); // Show the shadowMap at its actual size relative to window glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex2f(-1.0f, -1.0f); glTexCoord2f(1.0f, 0.0f); glVertex2f(((GLfloat)shadowWidth/(GLfloat)windowWidth)*2.0f-1.0f, -1.0f); glTexCoord2f(1.0f, 1.0f); glVertex2f(((GLfloat)shadowWidth/(GLfloat)windowWidth)*2.0f-1.0f, ((GLfloat)shadowHeight/(GLfloat)windowHeight)*2.0f-1.0f); glTexCoord2f(0.0f, 1.0f); glVertex2f(-1.0f, ((GLfloat)shadowHeight/(GLfloat)windowHeight)*2.0f-1.0f); glEnd(); glDisable(GL_TEXTURE_2D); glEnable(GL_LIGHTING); glPopMatrix(); glMatrixMode(GL_PROJECTION); gluPerspective(45.0f, 1.0f, 1.0f, 1000.0f); glMatrixMode(GL_MODELVIEW); } else if (noShadows) { // Set up some simple lighting glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight); glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight); // Draw objects in the scene including base plane DrawModels(GL_TRUE); } else { if (!ambientShadowAvailable) { GLfloat lowAmbient[4] = {0.1f, 0.1f, 0.1f, 1.0f}; GLfloat lowDiffuse[4] = {0.35f, 0.35f, 0.35f, 1.0f}; // Because there is no support for an "ambient" // shadow compare fail value, we'll have to // draw an ambient pass first... glLightfv(GL_LIGHT0, GL_AMBIENT, lowAmbient); glLightfv(GL_LIGHT0, GL_DIFFUSE, lowDiffuse); // Draw objects in the scene, including base plane DrawModels(GL_TRUE); // Enable alpha test so that shadowed fragments are discarded glAlphaFunc(GL_GREATER, 0.9f); glEnable(GL_ALPHA_TEST); } glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight); glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight); // Set up shadow comparison glEnable(GL_TEXTURE_2D); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); // Set up the eye plane for projecting the shadow map on the scene glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); glTexGenfv(GL_S, GL_EYE_PLANE, &textureMatrix[0]); glTexGenfv(GL_T, GL_EYE_PLANE, &textureMatrix[4]); glTexGenfv(GL_R, GL_EYE_PLANE, &textureMatrix[8]); glTexGenfv(GL_Q, GL_EYE_PLANE, &textureMatrix[12]); // Draw objects in the scene, including base plane DrawModels(GL_TRUE); glDisable(GL_ALPHA_TEST); glDisable(GL_TEXTURE_2D); glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); glDisable(GL_TEXTURE_GEN_R); glDisable(GL_TEXTURE_GEN_Q); } if (glGetError() != GL_NO_ERROR) fprintf(stderr, "GL Error!\n"); // Flush drawing commands glutSwapBuffers(); }
完整代码地址https://github.com/sweetdark/openglex/tree/master/shadowmap
表述能力有限。如果错误,请指正不胜感激。详细的请参考下面的链接。
投影映射纹理GL_EYE_LINEAR的参考:
英文http://www.nvidia.com/object/Projective_Texture_Mapping.html
阴影贴图的参考:
http://www.eng.utah.edu/~cs5610/lectures/ShadowMapping%20OpenGL%202009.pdf
ftp://download.nvidia.com/developer/presentations/2004/GPU_Jackpot/Shadow_Mapping.pdf