目录[-]



之前我们介绍过简单的把物体压*到投影*面来制造阴影。但这种阴影方式有其局限性(如投影*面须是*面)。在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();
}