浏览量:4,563

实时阴影技术

转载请注明原文章链接:http://www.twinklingstar.cn/2015/1717/tech-of-shadows/

示例代码下载地址:https://github.com/twinklingstar20/twinklingstar_cn_tech_of_shadows/

1. 阴影介绍

在现实生活中,阴影随处可见,如图1中所示的两个例子,一个温暖窝心,一个浪漫感动。光与物的结合,形成阴影,使得场景更加的真实。如果没有阴影的存在, 这两个场景将变得十分的不自然。

2015-3-18 22-02-56

图1. 现实中的阴影

想起高中时学过的一首由马致远创作的小令《天净沙•秋思》

枯藤老树昏鸦
小桥流水人家
古道西风瘦马
夕阳西下

断肠人在天涯

顿时在脑海中闪现一个画面:在深秋村野上,出现了一位漂泊天涯的游子,在残阳夕照的荒凉古道上,牵着一匹瘦马,迎着凄苦的秋风,信步满游,背后拖着长长的身影

在图形学领域,给出一个阴影的定义:

Shadow [is] the region of space for which at least one point of the light source is occluded.[1]

(由于光源上存在点被障碍物阻挡而产生的区域,就形成了阴影)

这个定义存在两个局限:(1)只考虑到直接来自于光源的光照,不考虑由平面反射出的光照;(2)默认障碍物是不透明。本篇文章讨论的阴影就基于这两个 “不符合”实际的假设来进行的。

首先,介绍几个与阴影相关的重要概念。阴影渲染中的三个关键元素是:(1)光源,(2)遮挡物(Occluders, Blockers, Shadows Casters),(3)接受物(Receiver)。如果光源是一块区域,由于部分区域被遮挡物遮挡,就会形成半影(Penumbra),完全被遮挡的区域,就会形成全影(Umbra),如图2所示,即阴影 = 半影 + 全影。如果光源是一个点,那么就不会存在半影,称这样的阴影为硬阴影(Hard Shadow);如果采用面光源或者体光源,就产生了软阴影(Soft Shadow)。硬阴影和软阴影的几何示意图如图3所示,硬阴影和软阴影的渲染效果图如图4所示。

2015-3-18 22-06-13

图2. 阴影相关的概念[1]

2015-3-18 22-06-31

图3. 硬阴影和软阴影的几何示意图

2015-3-18 22-06-42

图4. 硬阴影和软阴影的渲染效果图,(a)硬阴影,(b)软阴影[2]

2. 平面阴影

平面阴影(Planar Shadows)技术,处理的问题是:在光照作用下,遮挡物在平面上的生成的阴影。

2.1. Blinn’s平面阴影

本小节将介绍由Blinn[5](1988)提出的平面投影阴影(Planar Projected Shadows)技术[1,4,5]

2.1.1. 原理

平面阴影技术的几何示意图,如图5所示,给定光源位置为l,接受物是一个平面,平面可以用参数方程\vec n \cdot x + d = 0来表示,遮挡物上任意一个点的坐标为v,它可以通过与一个矩阵M的乘积,计算出投影到接受物平面上的点v' = M \cdot v,那么矩阵M又是什么呢?

点光源的位置是l,光源和物体上的点v可以确定一条射线,用下面的等式表示:

r(t) = l + t\vec d,\vec d = v - l(1)

射线上存在一点v'v'在平面上,则把等式(1)代入平面的参数方程中,得到:

    \[\vec n \cdot (l + t\vec d) + d = 0 \Rightarrow \hat t = - \frac{{\vec n \cdot l + d}}{{\vec n \cdot \vec d}}\]

把计算出来的\hat t代入到等式(1)中,可以计算得到:

v' = r(\hat t) = l - \frac{{\vec n \cdot l + d}}{{\vec n \cdot (v - l)}}(v - l)

= \frac{{\left( {\vec n \cdot (v - l)} \right)l - (\vec n \cdot l + d)(v - l)}}{{\vec n \cdot (v - l)}}

= \frac{{\left( {\vec n \cdot l + d} \right)v - (\vec n \cdot v + d)l}}{{\vec n \cdot l - \vec n \cdot v}}(2)

其中,等式中的l表示光源的位置,v表示物体上的点坐标,\vec nd构成了平面的表示公式,v'表示遮挡们上任意一个点v在平面上的投影点。

如果等式(2)中的坐标vv'采用齐次坐标的形式,可以消除分母,并把结果表示成矩阵的形式,如下面的等式所示:

{v_x}' = (\vec n \cdot l + d){v_x} - ({\vec n_x}{v_x} + {\vec n_y}{v_y} + {\vec n_z}{v_z} + d){l_x}

{v_y}' = (\vec n \cdot l + d){v_y} - ({\vec n_x}{v_x} + {\vec n_y}{v_y} + {\vec n_z}{v_z} + d){l_y}

{v_z}' = (\vec n \cdot l + d){v_z} - ({\vec n_x}{v_x} + {\vec n_y}{v_y} + {\vec n_z}{v_z} + d){l_z}

{v_w}' = \vec n \cdot l - ({\vec n_x}{v_x} + {\vec n_y}{v_y} + {\vec n_z}{v_z})

对于齐次坐标来说,\left( {x,y,z,w} \right),表示的坐标点是\left( {x/w,y/w,z/w,1} \right),推导出的矩阵形式是:

M = \left( {\begin{array}{*{20}{c}}{\vec n \cdot l + d - {{\vec n}_x}{l_x}}&{ - {{\vec n}_y}{l_x}}&{ - {{\vec n}_z}{l_x}}&{ - d{l_x}}\\{ - {{\vec n}_x}{l_y}}&{\vec n \cdot l + d - {{\vec n}_y}{l_y}}&{ - {{\vec n}_z}{l_y}}&{ - d{l_y}}\\{ - {{\vec n}_x}{l_z}}&{ - {{\vec n}_y}{l_z}}&{\vec n \cdot l + d - {{\vec n}_z}{l_z}}&{ - d{l_z}}\\{ - {{\vec n}_x}}&{ - {{\vec n}_y}}&{ - {{\vec n}_z}}&{\vec n \cdot l}\end{array}} \right)(3)

即有

    \[v'M = Mv \Rightarrow \left( {\begin{array}{*{20}{c}}{{v_x}'}\\{{v_y}'}\\{{v_z}'}\\{{v_w}'}\end{array}} \right) = M\left( {\begin{array}{*{20}{c}}{{v_x}}\\{{v_y}}\\{{v_z}}\\{{v_w}}\end{array}} \right)\]

2.1.2. z-fighting问题

平面投影阴影渲染,只能应用在阴影的接受物是平面的情况,这是这种技术的局限,它还存在一些其它方面的问题。

采用这种技术基本包括两个渲染过程:(1)渲染场景,(2)渲染阴影。阴影在接受平面上,平面与阴影渲染出来的颜色是不同的,正确的效果是阴影区域渲染出来的是阴影效果而不是平面的效果。那么,现在问题就出来了,图形学的中深度缓冲区(z-Buffer)只会存储距离观察者最近的一个像素值,接受平面与阴影重叠的区域有相同的深度值,深度缓冲区是选择阴影颜色还是平面的颜色呢?计算机不是人脑,没有那么的智能。实际上,由于精度问题,还会导致GPU随机的选择阴影和平面的像素值,效果如图5所示。

2015-3-18 22-20-11

图5. 阴影用蓝色表示,平面用绿色表示,z-fighting就会产生图中箭头所示的效果

解决这个问题,有两种解决方案,得到图6所示的正确渲染结果:

(1)       给阴影在平面法向量方向上,增加一定的偏移,使得阴影所在的平面在接受平面略上方的位置,而这个偏移值比较难指定,但是OpenGL中提供了glPolygonOffset()函数,可以帮我们解决这个问题;

(2)       先渲染接受物平面;然后,关闭深度测试,渲染阴影;最后,开启深度测试,渲染整个场景。这种渲染顺序,可以避免z-fighting,但是又引起了一个新的问题:就是必须把遮挡物和接受物的渲染分离开。

2015-3-18 22-20-23

图6. 采用方案(2)得到的正确的平面阴影

2.1.3. 混合阴影

所谓的混合阴影是指渲染出的阴影效果是阴影指定颜色与平面颜色混合而成。这样子,采用前面的技术就又可能产生一个新的问题:某些像素点,会混合多次阴影指定颜色。如图7所示,阴影指定颜色是灰色,但是图中箭头所示区域多次混合了阴影指定颜色。这个问题产生的原因就是,遮挡物上的多个点,都投影到平面上的相同位置,结果在渲染时多次混合,造成图中阴影中部分区域颜色更深的效果。

2015-3-18 22-20-32

图7. 阴影的多次混合[3]

       这个问题可以采用模板缓冲区来解决,基本的步骤如下所示:

(1) 初始化模板缓冲区,全部清零;

(2) 把接受平面渲染进模板缓冲区中,把覆盖的像素都设置成1;

(3) 激活深度测试和模板缓冲操作,设置模板缓冲区:

           i.  glStencilFunc(GL EQUAL, 1, 0xffff);
          ii.  glStencilOp(GL KEEP, GL INCR, GL INCR);

         iii.  drawShadow()

(4) 渲染场景

上面步骤产生的一个结果是,第一个阴影像素的颜色输出,与阴影接受平面的颜色混合,然后对应像素点的模板缓冲区的值加1,如果再有相同位置的阴影像素,由于模板缓冲区的值不等于1,不再能通过测试。

2.1.4. 错误的阴影效果

平面投影阴影技术主要干的一件事儿就是给定一个光源点和一个遮挡物,计算出该物体在某个平面上的投影,如图8所示的效果是正确的。

2015-3-18 22-23-52

但是有也可能发生下面两种情况,是错误的:

2015-3-18 22-24-02

图9. 反阴影(Anti-Shadow)和假影(False Shadow)[2]

2015-3-18 22-24-13

图10. 平面阴影中正确的阴影效果和假影

由于不同的显卡对OpenGL的实现不同,如果齐次坐标值w<0,不同的显卡渲染出来的效果可能会不同[8]。在有些显卡上,OpenGL会裁剪掉齐次坐标w<0的像素点,因此不会产生反影。如图10所示,存在三个圆环,平面上方有两个,平面下方也存在一个,白色的圆点表示光源,它介于平面上方的两个圆环间,由于显卡会自动把齐次坐标w<0的点裁剪掉,所以光源上方的圆环不会产生反影;在平面下方还有一个圆环(由于被平面遮挡了,所以显示不出来),但是上面的平面阴影算法会产生假影;介于光源和平面之间的圆环,能产生正确的阴影。

为了避免假影的错误,需要在创建投影阴影之前,使用接受面对遮挡物进行裁减。接下来介绍一种平面软阴影技术,可以避免平面阴影的反影和假影的出现。

2.1.5. 代码

实现的效果下图所示,后面提供了DEMO的代码。

2015-3-18 22-24-24

图11. Blin平面阴影技术效果图

/************************************************************************       
\link   www.twinklingstar.cn
\author twinklingstar
\date   2015/03/15
\file   planarshadow.cpp
****************************************************************************/
#include <stdlib.h>
#include <gl/glut.h>
#include <string.h>
#include <stdio.h>

//Camera & light positions
float cameraPosition[]={-4.0f, 6.0f,-4.0f};
float lightPosition[]={1.0f,2.0f,-1.0f};

//window size
int		windowWidth;
int		windowHeight;

float white[]={1.0f,1.0f,1.0f,1.0f};
float black[]={0.0f,0.0f,0.0f,0.0f};
float plane[]={0.0f,1.0f,0.0f,0.0f};

float dot3x3(float* v1,float* v2)
{
	return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2];
}

void cross3x3(float* v1,float* v2,float* result)
{
	result[0] = v1[1]*v2[2] - v1[2]*v2[1];
	result[1] = v1[2]*v2[0] - v1[0]*v2[2];
	result[2] = v1[0]*v2[2] - v1[2]*v2[0];
}

void setMatrix(float* plane,float* light,float *result)
{
	float delta = dot3x3(plane,light) + plane[3];
	result[0] = delta - plane[0]*light[0];
	result[4] = -plane[1]*light[0];
	result[8] = -plane[2]*light[0];
	result[12] = -plane[3]*light[0];

	result[1] = -plane[0]*light[1];
	result[5] = delta - plane[1]*light[1];
	result[9] = -plane[2]*light[1];
	result[13] = -plane[3]*light[1];

	result[2] = -plane[0]*light[2];
	result[6] = -plane[1]*light[2];
	result[10] = delta - plane[2]*light[2];
	result[14] = -plane[3]*light[2];

	result[3] = -plane[0];
	result[7] = -plane[1];
	result[11] = -plane[2];
	result[15] = dot3x3(plane,light);
}

void drawPlane()
{
	int size = 4;
	float height = 0.0;

	glColor3f(0.0f,1.0f,0.0f);
	glPushMatrix();
	glBegin(GL_QUADS);
	glVertex3f(size,height,size);
	glVertex3f(-size,height,size);
	glVertex3f(-size,height,-size);
	glVertex3f(size,height,-size);
	glEnd();
	glPopMatrix();
}

void drawLight()
{
	glColor3f(1.0f,1.0f,1.0f);
	glPushMatrix();
	glTranslatef(lightPosition[0],lightPosition[1],lightPosition[2]);
	glutSolidSphere(0.05,32,32);
	glPopMatrix();
}

void drawScene( bool isShadow = false)
{
	if( isShadow )
		glColor3f(1.0f, 0.0f, 0.0f);
	else
		glColor3f(0.0f,0.0f,1.0f);

	static float a = 0.0f;
	glPushMatrix();
	glTranslatef(0.0f, 0.5f, 0.0f);
	glRotatef(a, 1.0f, 1.0f, 0.0f);
	glutSolidTorus(0.2, 0.5, 24, 48);
	glPopMatrix();

	//draw false shadow
	glPushMatrix();
	glTranslatef(0.0f, -4.0f, 0.0f);
	glRotatef(a, 1.0f, 0.0f, 1.0f);
	glutSolidTorus(0.2, 0.5, 24, 48);
	glPopMatrix();

	//draw anti-shadow
	glPushMatrix();
	glTranslatef(1.0f, 3.0f, -1.0f);
	glRotatef(a, 0.0f, 1.0f, 1.0f);
	glutSolidTorus(0.2, 0.5, 24, 48);
	glPopMatrix();

	a += 0.05f;
}

bool init()
{
	//Shading states
	glShadeModel(GL_SMOOTH);
	//Clearing color of the color buffer.
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	//Clearing depth of the depth buffer
	glClearDepth(1.0f);
	//Depth test.
	glDepthFunc(GL_LEQUAL);
	glEnable(GL_DEPTH_TEST);
	//We use glScale when drawing the scene
	glEnable(GL_NORMALIZE);
	//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);
	return true;
}

void myDisplay(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(cameraPosition[0], cameraPosition[1], cameraPosition[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
	glViewport(0, 0, windowWidth, windowHeight);

	//draw the plane.
	//Use dim light to represent shadowed areas
	float lightColor[]={0.2f,0.2f,0.2f,1.0f};
	glLightfv(GL_LIGHT1, GL_POSITION, lightPosition);
	glLightfv(GL_LIGHT1, GL_AMBIENT, lightColor);
	glLightfv(GL_LIGHT1, GL_DIFFUSE, white);
	glLightfv(GL_LIGHT1, GL_SPECULAR, white);
	glEnable(GL_LIGHT1);
	glEnable(GL_LIGHTING);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_COLOR_MATERIAL);
	drawPlane();

	//draw the shadows.
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_LIGHTING);
	glDisable(GL_COLOR_MATERIAL);
	float matrix[16];
	setMatrix(plane,lightPosition,matrix);
	glPushMatrix();
	glMultMatrixf(matrix);
	drawScene(true);
	glPopMatrix();

	//draw the scene.
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_LIGHTING);
	glEnable(GL_COLOR_MATERIAL);
	drawScene(false);

	//draw the light
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_LIGHTING);
	glDisable(GL_COLOR_MATERIAL);
	drawLight();

	glutSwapBuffers();
	glutPostRedisplay();
}

void myReshape(GLsizei w,GLsizei h)
{
	windowWidth	=	w;
	windowHeight=	h;
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(45.0f, (float)windowWidth/windowHeight, 1.0f, 1000.0f);

}

int main(int argc,char ** argv)
{
	glutInit(&argc,argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glutInitWindowSize(400,400);
	glutCreateWindow("Planar Projection Shadows");
	if( !init() )
		return 0;
	glutReshapeFunc(myReshape);
	glutDisplayFunc(myDisplay);

	glutMainLoop();
	return(0);
}

2.2. Heckbert&Herf平面阴影

Heckbert&Herf[4,6,7]提出了一种基于图形硬件渲染软阴影的技术,该技术也可以用于渲染平面阴影,而且能有效的避免反影和假影的出现,本小节介绍它在平面硬阴影上的渲染原理。

2.2.1. 原理

2015-3-18 22-30-47

图12. 由光源点a和接受平面上的平行四边形基底(向量ex和ey,点b)构成的锥体[1,6]

如图12所示,现在的目的就是通过透视变换,把由光源点a和接受平面上的平行四边形基底(向量ex和ey,点b)构成的锥体变换为一个平行六面体,这个平行六面体位于单位屏幕空间(Unit Screen Space)中,即形成了一个新的坐标系统。假设原先锥体内的坐标点(x0, y0, z0),通过变换,在新的坐标系统下的坐标为(xu, yu, zu)。从锥体顶点往接受平面观察,锥体的左边和右边,分别映射到平面xu=0和xu=1,上面和下面分别映射到平面yu=1和yu=0,椎体顶点所在的平面映射到面{z_u} = \infty,锥体底面映射到面zu=1,如图13所示。x和y方向上是单位长度,光源与平面之间的z值变换后,在

    \left[ {1,\infty } \right)\]之间;光源后面的点,变换后的值在z=0到负无穷大之间;平面后面的点,变换后的值在z=0与z=1之间。所以,在透视变换的时候,将近平面的距离要设置为1。 <img class="aligncenter size-full wp-image-1742" src="http://www.twinklingstar.cn/wp-content/uploads/2015/03/2015-3-18-22-31-01.png" alt="2015-3-18 22-31-01" width="800" height="441" /> 图13. 通过矩阵M<sub>t</sub>变换,映射到一个长方体上<sup> [4]</sup> 可以通过一个齐次矩阵进行变换,齐次矩阵的形式如下所示: <span class="ql-right-eqno">   </span><span class="ql-left-eqno">   </span><img src="http://www.twinklingstar.cn/wp-content/ql-cache/quicklatex.com-f9a976a1cec0f295d5a8875b7ef6b0fc_l3.png" height="86" width="259" class="ql-img-displayed-equation quicklatex-auto-format" alt="\[{M_t} = \left( {\begin{array}{*{20}{c}}{{m_{00}}}&{{m_{01}}}&{{m_{02}}}&{{m_{03}}}\\{{m_{10}}}&{{m_{11}}}&{{m_{12}}}&{{m_{13}}}\\0&0&0&1\\{{m_{30}}}&{{m_{31}}}&{{m_{32}}}&{{m_{33}}}\end{array}} \right)\]" title="Rendered by QuickLaTeX.com"/> 通过该齐次矩阵变换,并进行透视除法得到变换后的坐标(x<sub>u</sub>,y<sub>u</sub>,z<sub>u</sub>): \(\left( {\begin{array}{*{20}{c}}x\\y\\1\\w\end{array}} \right) = {M_t}\left( {\begin{array}{*{20}{c}}{{x_0}}\\{{y_0}}\\{{z_0}}\\1\end{array}} \right)

\left( {\begin{array}{*{20}{c}}{{x_u}}\\{{y_u}}\\{{z_u}}\end{array}} \right) = \left( {\begin{array}{*{20}{c}}{x/w}\\{y/w}\\{1/w}\end{array}} \right)

我们先来求矩阵的最后一行\left( {{m_{30}},{m_{31}},{m_{32}},{m_{33}}} \right)的值,通过矩阵变换后,可以求得

    \[w = {m_{30}}{x_0} + {m_{31}}{y_0} + {m_{32}}{z_0} + {m_{33}},{z_u} = 1/w\]

所以有

    \[{z_u} = 1/({m_{30}}{x_0} + {m_{31}}{y_0} + {m_{32}}{z_0} + {m_{33}})\]

把 (x0,y0,z0)=a代入(5)式,得到{z_u} = \infty,即分母为0;把(x0,y0,z0)=b或者b+ex或者b+ey代入(5),得到zu=1。总共可以得到4个等式,4个等式联立,如下所示

\left( {{m_{30}},{m_{31}},{m_{32}}} \right) \cdot a + {m_{33}} = 0

\left( {{m_{30}},{m_{31}},{m_{32}}} \right) \cdot b + {m_{33}} = 1

\left( {{m_{30}},{m_{31}},{m_{32}}} \right) \cdot (b + {e_x}) + {m_{33}} = 1

\left( {{m_{30}},{m_{31}},{m_{32}}} \right) \cdot (b + {e_y}) + {m_{33}} = 1

可以得到一个解

    \[\left( {{m_{30}},{m_{31}},{m_{32}},{m_{33}}} \right) = ({a_w}{\vec n_{wx}},{a_w}{\vec n_{wy}},{a_w}{\vec n_{wz}}, - {a_w}{\vec n_w} \cdot a)\]

其中,{\vec n_w} = {e_y} \times {e_x},{a_w} = 1/{\vec n_w} \cdot {e_w},{e_w} = b - a

用类似的方法,可以求得矩阵的第一行和第二行的值,最终推导得到一个矩阵是:

{M_t} = \left( {\begin{array}{*{20}{c}}{{a_x}{{\vec n}_{xx}}}&{{a_x}{{\vec n}_{xy}}}&{{a_x}{{\vec n}_{xz}}}&{ - {a_x}{{\vec n}_x} \cdot b}\\{{a_y}{{\vec n}_{yx}}}&{{a_y}{{\vec n}_{yy}}}&{{a_y}{{\vec n}_{yz}}}&{ - {a_y}{{\vec n}_y} \cdot b}\\0&0&0&1\\{{a_w}{{\vec n}_{wx}}}&{{a_w}{{\vec n}_{wy}}}&{{a_w}{{\vec n}_{wz}}}&{ - {a_w}{{\vec n}_w} \cdot a}\end{array}} \right)(6)

其中,

{e_w} = b - a

{\vec n_x} = {e_w} \times {e_y},{a_x} = 1/{\vec n_x} \cdot {e_x}

{\vec n_y} = {e_x} \times {e_w},{a_y} = 1/{\vec n_y} \cdot {e_y}

{\vec n_w} = {e_y} \times {e_x},{a_w} = 1/{\vec n_w} \cdot {e_w}

得到的矩阵是一个3D到3D的变换。那好,说到这里,可能大家还是很晕乎,这个矩阵到底哪里来的?它往哪里去?完整的渲染算法流程又是什么样的?且听我娓娓叙来。

2015-3-18 22-31-14

图14. 创建一个sx \times sy像素的纹理

假设一种最简单的情况,场景中只有一个光照,设它的坐标为a,只有一个接受平面且它是一个矩形,它的四个坐标为\left( {{p_0},{p_1},{p_2},{p_3}} \right),首先我们需要创建一个sx \times sy像素的纹理,观察点为 ,观察的区域正好是接受平面的矩形,如图14所示,把该矩形渲染进sx \times sy像素的纹理中,伪码如下所示:

create_shadow_texture()
1.    glViewport(x, y, sx, sy);               //指定屏幕坐标为(x, y),尺寸为(sx, sy)为渲染区域
2.    glCullFace(GL_BACK);               //开启背面裁减
3.    {n_x} = {p_2} - {p_0};
4.    {n_y} = {p_1} - {p_0};
5.    {n_z} = {n_y} \times {n_x};
6.    {n_x} = {n_x}/\left\| {{n_x}} \right\|,{n_y} = {n_y}/\left\| {{n_y}} \right\|,{n_z} = {n_z}/\left\| {{n_z}} \right\|;
7.    c = a + {n_z};
8.    glPushMatrix();
9.    glLoadIdentity();
10.   gluLookAt({a_x},{a_y},{a_z},{c_x},{c_y},{c_z},{n_{x,x}},{n_{x,y}},{n_{x,z}});
11.   left = ({p_0} - a) \cdot {n_x},right = ({p_2} - a) \cdot {n_x};
12.   bott = ({p_0} - a) \cdot {n_y},top = ({p_1} - a) \cdot {n_y};
13.   d = \left| {\left( {{p_0} - a} \right) \cdot {n_z}} \right|;
14.   left * = znear/d,rightt * = znear/d,bott * = znear/d,top * = znear/d;
15.   glMatrixMode(GL_PROJECTION);
16.   glPushMatrix();
17.   glLoadIdentity();
18.   glFrustum(left, right, bottom, top, znear, zfar);
19.   glMatrixMode(GL_MODELVIEW);
20.   glDisable(GL_LIGHTING);
21.   glDisable(GL_TEXTURE_2D);
22.   glColor3f(1.0f, 1.0f, 1.0f);
23.   渲染矩形\left( {{p_0},{p_1},{p_2},{p_3}} \right);
24.   glColor3f(0.0f, 0.0f, 0.0f);
25.   渲染所有的遮挡物;
26.   glPopMatrix();
27.   glMatrixMode(GL_PROJECTION);
28.   glPopMatrix();
29.   glMatrixMode(GL_MODELVIEW);
30.   glBindTexture(GL_TEXTURE_2D, shadow);      //shadow表示纹理的名字
31.   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
32.   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
33.   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
34.   format = GL_LUMINANCE;

35.   glCopyTexImage2D(GL_TEXTURE_2D, 0, format, x, y, sx, sy, 0);

代码3~18行,起到了前面所述的裁减矩阵的作用。完成纹理的创建后,渲染场景中的遮挡物和接受平面,如下所示:

       draw_scene()
1.    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
2.    glLoadIdentity();
3.    glEnable(GL_TEXTURE_2D);            //开启纹理渲染
4.    glEnable(GL_LIGHTING);                 //开启光照
5.    glEnable(GL_LIGHT0);
6.    glBindTexture(GL_TEXTURE_2D, shadow); //shadow表示前面创建的纹理名
7.    glBegin(GL_TRIANGLE_STRIP);      
8.            glTexCoord2f(0,0); glVertex3fv( );
9.            glTexCoord2f(0,1); glVertex3fv( );
10.          glTexCoord2f(1,0); glVertex3fv( );
11.          glTexCoord2f(1,1); glVertex3fv( );
12.   glEnd();

13.   渲染遮挡物

前面的两段代码介绍了Heckbert&Herf提出的方法渲染的基本流程,但阴影的渲染还不精确,因为前面的默认阴影是黑色的,实际上存在环境光的影响,参考后面的代码。

2.2.2. 代码

网上可以找到Heckbert&Herf算法的实现代码[9],例如渲染出图15(a)所示的带有阴影的场景,场景中的每一个接受平面都要创建一个sx \times sy纹理对象,如图(b)所示。

2015-3-18 22-32-21

图15.渲染的场景和创建的9个接受平面的纹理

具体的代码实现可以参考网上提供的代码[9],我将该代码进行了修改,使它只保留了平面硬阴影的渲染,如下所示:

/* Soft shadows using a shadow texture per polygon.  Based on an algorithm   */
/* described by Paul Heckbert and Michael Herf of CMU; see their web site    */
/* http://www.cs.cmu.edu/ph/shadow.html for details.                         */
/*                                                                           */
#include <stdlib.h>
#include <gl/glut.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include <stdio.h>

/* list of polygons that have shadow textures */
static GLfloat gPts[][4][3] = {
	/* floor */
	-100.f, -100.f, -320.f,
	-100.f, -100.f, -520.f,
	100.f, -100.f, -320.f,
	100.f, -100.f, -520.f,

	/* left wall */
	-100.f, -100.f, -320.f,
	-100.f,  100.f, -320.f,
	-100.f, -100.f, -520.f,
	-100.f,  100.f, -520.f,

	/* back wall */
	-100.f, -100.f, -520.f,
	-100.f,  100.f, -520.f,
	100.f, -100.f, -520.f,
	100.f,  100.f, -520.f,

	/* right wall */
	100.f, -100.f, -520.f,
	100.f,  100.f, -520.f,
	100.f, -100.f, -320.f,
	100.f,  100.f, -320.f,

	/* ceiling */
	-100.f,  100.f, -520.f,
	-100.f,  100.f, -320.f,
	100.f,  100.f, -520.f,
	100.f,  100.f, -320.f,

	/* blue panel */
	-60.f,  -40.f, -400.f,
	-60.f,   70.f, -400.f,
	-30.f,  -40.f, -480.f,
	-30.f,   70.f, -480.f,

	/* yellow panel */
	-40.f,  -50.f, -400.f,
	-40.f,   50.f, -400.f,
	-10.f,  -50.f, -450.f,
	-10.f,   50.f, -450.f,

	/* red panel */
	-20.f,  -60.f, -400.f,
	-20.f,   30.f, -400.f,
	10.f,  -60.f, -420.f,
	10.f,   30.f, -420.f,

	/* green panel */
	0.f,  -70.f, -400.f,
	0.f,   10.f, -400.f,
	30.f,  -70.f, -395.f,
	30.f,   10.f, -395.f,
};

static GLfloat gMaterials[][4] = {
	1.0f, 1.0f, 1.0f, 1.0f, /* floor        */
	1.0f, 1.0f, 1.0f, 1.0f, /* left wall    */
	1.0f, 1.0f, 1.0f, 1.0f, /* back wall    */
	1.0f, 1.0f, 1.0f, 1.0f, /* right wall   */
	1.0f, 1.0f, 1.0f, 1.0f, /* ceiling      */
	0.2f, 0.5f, 1.0f, 1.0f, /* blue panel   */
	1.0f, 0.6f, 0.0f, 1.0f, /* yellow panel */
	1.0f, 0.2f, 0.2f, 1.0f, /* red panel    */
	0.3f, 0.9f, 0.6f, 1.0f, /* green panel  */
};

/* number of shadow textures to make */
static int gNumShadowTex = sizeof(gPts) / sizeof(gPts[0]);
//window size
static GLuint		gWindowWidth  = 400;
static GLuint		gWindowHeight = 400;

static GLint gTexXSize = 128;
static GLint gTexYSize = 128;

/* texture object names */
const GLuint gFloorTex	 = 1;
const GLuint gShadowTexs = 2;

static GLfloat gOrigin[4]	= { 0.f, 0.f, 0.f, 1.f };
static GLfloat gBlack[4]	= { 0.f, 0.f, 0.f, 1.f };
static GLfloat gAmbient[4]	= { 0.2f, 0.2f, 0.2f, 1.f };

static GLfloat gLightPos[4] = { 70.f, 70.f, -320.f, 1.f };

/* some simple vector utility routines */
void
vcopy(GLfloat a[3], GLfloat b[3])
{
	b[0] = a[0];
	b[1] = a[1];
	b[2] = a[2];
}

void
vnormalize(GLfloat v[3])
{
	float m = sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
	v[0] /= m;
	v[1] /= m;
	v[2] /= m;
}

void
vadd(GLfloat a[3], GLfloat b[3], GLfloat c[3])
{
	c[0] = a[0] + b[0];
	c[1] = a[1] + b[1];
	c[2] = a[2] + b[2];
}

void
vsub(GLfloat a[3], GLfloat b[3], GLfloat c[3])
{
	c[0] = a[0] - b[0];
	c[1] = a[1] - b[1];
	c[2] = a[2] - b[2];
}

void
vcross(GLfloat a[3], GLfloat b[3], GLfloat c[3])
{
	c[0] = a[1] * b[2] - a[2] * b[1];
	c[1] = -(a[0] * b[2] - a[2] * b[0]);
	c[2] = a[0] * b[1] - a[1] * b[0];
}

float
vdot(GLfloat a[3], GLfloat b[3])
{
	return (a[0]*b[0] + a[1]*b[1] + a[2]*b[2]);
}

void
findNormal(GLfloat pts[][3], GLfloat normal[3]) {
	GLfloat a[3], b[3];

	vsub(pts[1], pts[0], a);
	vsub(pts[2], pts[0], b);
	vcross(b, a, normal);
	vnormalize(normal);
}

/* Make a checkerboard texture for the floor. */
GLfloat *
make_texture(int maxs, int maxt)
{
	GLint s, t;
	static GLfloat *texture;

	texture = (GLfloat *) malloc(maxs * maxt * sizeof(GLfloat));
	for (t = 0; t < maxt; t++) {
		for (s = 0; s < maxs; s++) {
			texture[s + maxs * t] = ((s >> 4) & 0x1) ^ ((t >> 4) & 0x1);
		}
	}
	return texture;
}

void
keyboard(unsigned char key, int x, int y)
{
	if (key == 27)  /* ESC */
		exit(0);
}

void
make_shadow_texture(int index, GLfloat eyept[3])
{
	GLfloat xaxis[3], yaxis[3], zaxis[3];
	GLfloat cov[3]; /* center of view */
	GLfloat pte[3]; /* plane to eye */
	GLfloat eye[3];
	GLfloat tmp[3], dist;
	GLfloat (*qpts)[3] = gPts[index];
	GLfloat left, right, bottom, top;
	GLfloat znear = 10.f, zfar = 600.f;
	GLint n;

	/* For simplicity, we don't compute the transformation matrix described */
	/* in Heckbert and Herf's paper.  The transformation and frustum used   */
	/* here is much simpler.                                                */
	vcopy(eyept, eye);
	vsub(qpts[1], qpts[0], yaxis);
	vsub(qpts[2], qpts[0], xaxis);
	vcross(yaxis, xaxis, zaxis);

	vnormalize(zaxis);
	vnormalize(xaxis); /* x-axis of eye coord system, in object space */
	vnormalize(yaxis); /* y-axis of eye coord system, in object space */

	/* center of view is just eyepoint offset in direction of normal */ 
	vadd(eye, zaxis, cov);

	/* set up viewing matrix */
	glPushMatrix();
	glLoadIdentity();
	gluLookAt(eye[0], eye[1], eye[2],
			  cov[0], cov[1], cov[2],
			  yaxis[0], yaxis[1], yaxis[2]);

	/* compute a frustum that just encloses the polygon */
	vsub(qpts[0], eye, tmp); /* from eye to 0th vertex */
	left = vdot(tmp, xaxis);
	vsub(qpts[2], eye, tmp); /* from eye to 2nd vertex */
	right = vdot(tmp, xaxis);
	vsub(qpts[0], eye, tmp); /* from eye to 0th vertex */
	bottom = vdot(tmp, yaxis);
	vsub(qpts[1], eye, tmp); /* from eye to 1st vertex */
	top = vdot(tmp, yaxis);

	/* scale the frustum values based on the distance to the polygon */
	vsub(qpts[0], eye, pte);
	dist	=  fabs(vdot(zaxis, pte));
	left	*= (znear / dist);
	right	*= (znear / dist);
	bottom	*= (znear / dist);
	top		*= (znear / dist);

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glFrustum(left, right, bottom, top, znear, zfar);
	glMatrixMode(GL_MODELVIEW);

	glDisable(GL_LIGHTING);

	glDisable(GL_TEXTURE_2D);

	for (n=0; n < gNumShadowTex; n++) {
		qpts = gPts[n];

		if (n == index) {
			/* this poly has full intensity, no occlusion */
			glColor3f(1.f, 1.f, 1.f);
		} else {
			/* all other polys just occlude the light */
			glColor3f(0.f, 0.f, 0.f);
		}

		glBegin(GL_TRIANGLE_STRIP);
		glVertex3fv(qpts[0]);
		glVertex3fv(qpts[1]);
		glVertex3fv(qpts[2]);
		glVertex3fv(qpts[3]);
		glEnd();
	}
	glPopMatrix();
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
}

void make_all_shadow_textures(float eye[3]) {
	GLint texPerRow;
	GLint n;
	GLfloat x, y;

	texPerRow = (gWindowWidth / gTexXSize);
	for (n=0; n < gNumShadowTex; n++) {
		y = (n / texPerRow) * gTexYSize;
		x = (n % texPerRow) * gTexXSize;
		glViewport(x, y, gTexXSize, gTexYSize);
		make_shadow_texture(n, eye);
	}
	glViewport(0, 0, gWindowWidth, gWindowHeight);
}

void store_all_shadow_textures(void) {
	GLint texPerRow;
	GLint n, x, y;
	GLubyte *texbuf;

	texbuf = (GLubyte *) malloc(gWindowWidth * gTexYSize * sizeof(int));

	/* how many shadow textures can fit in the window */
	texPerRow = (gWindowWidth / gTexXSize);

	for (n=0; n < gNumShadowTex; n++) {
		GLenum format;

		x = (n % texPerRow) * gTexXSize;
		y = (n / texPerRow) * gTexYSize;

		glBindTexture(GL_TEXTURE_2D, gShadowTexs + n);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

		format = GL_LUMINANCE;

		glCopyTexImage2D(GL_TEXTURE_2D, 0, format, x, y, gTexXSize, gTexYSize, 0);
	}
	free(texbuf);
}

bool
init()
{
	/* turn on features */
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glCullFace(GL_BACK);
	glLightfv(GL_LIGHT0, GL_AMBIENT, gBlack);
	//Create the floor texture.
	float* tex = make_texture(gTexXSize, gTexYSize);
	glBindTexture(GL_TEXTURE_2D, gFloorTex);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, 1, gTexXSize, gTexYSize, 0, GL_RED, GL_FLOAT, tex);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	free(tex);
	return true;
}

void 
draw(void)
{
	GLint n;
	GLfloat normal[3];
	GLfloat (*qpts)[3];

	glPushMatrix();
	glLoadIdentity();

	glLightfv(GL_LIGHT0, GL_POSITION, gLightPos);

	glPopMatrix();

	/* make shadow textures from just one frame */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	make_all_shadow_textures(gLightPos);
	store_all_shadow_textures();

	glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
	glLoadIdentity();

	glColor3f(1.f, 1.f, 1.f);

	glEnable(GL_TEXTURE_2D);

	/* Unfortunately, using the texture as an occlusion map requires two */
	/* passes: one in which the occlusion map modulates the diffuse      */
	/* lighting, and one in which the ambient lighting is added in. It's */
	/* incorrect to modulate the ambient lighting, but if the result is  */
	/* acceptable to you, you can include it in the first pass and       */
	/* omit the second pass. */

	/* draw only with diffuse light, modulating it with the texture */
	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gBlack);
	for (n=0; n < gNumShadowTex; n++) {
		qpts = gPts[n];
		findNormal(qpts, normal);
		glNormal3fv(normal);
		glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, gMaterials[n]);
		glBindTexture(GL_TEXTURE_2D, gShadowTexs + n);
		glBegin(GL_TRIANGLE_STRIP);
		glTexCoord2f(0,0); glVertex3fv(qpts[0]);
		glTexCoord2f(0,1); glVertex3fv(qpts[1]);
		glTexCoord2f(1,0); glVertex3fv(qpts[2]);
		glTexCoord2f(1,1); glVertex3fv(qpts[3]);
		glEnd();
	}

	/* add in the ambient lighting */
	glDisable(GL_LIGHTING);
	glDisable(GL_TEXTURE_2D);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);
	glDepthFunc(GL_LEQUAL);
	for (n=0; n < gNumShadowTex; n++) {
		qpts = gPts[n];
		glColor4f(gAmbient[0] * gMaterials[n][0],
				  gAmbient[1] * gMaterials[n][1],
				  gAmbient[2] * gMaterials[n][2],
				  gAmbient[3] * gMaterials[n][3]);
		glBegin(GL_TRIANGLE_STRIP);
		glTexCoord2f(0,0); glVertex3fv(qpts[0]);
		glTexCoord2f(0,1); glVertex3fv(qpts[1]);
		glTexCoord2f(1,0); glVertex3fv(qpts[2]);
		glTexCoord2f(1,1); glVertex3fv(qpts[3]);
		glEnd();
	}
	/* restore the ambient colors to their defaults */
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gAmbient);

	/* blend in the checkerboard floor */
	glEnable(GL_BLEND);
	glBlendFunc(GL_ZERO, GL_SRC_COLOR);
	glDepthFunc(GL_LEQUAL);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, gFloorTex);
	glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, gMaterials[0]);
	glTranslatef(0.0f, 0.05f, 0.0f);
	glColor3f(1.f, 1.f, 1.f);
	glBegin(GL_TRIANGLE_STRIP);
	glNormal3f(0.f, 1.f, 0.f);
	glTexCoord2f(0.f, 0.f); glVertex3fv(gPts[0][0]);
	glTexCoord2f(0.f, 1.f); glVertex3fv(gPts[0][1]);
	glTexCoord2f(1.f, 0.f); glVertex3fv(gPts[0][2]);
	glTexCoord2f(1.f, 1.f); glVertex3fv(gPts[0][3]);
	glEnd();

	/* undo some state settings that we did above */
	glDisable(GL_BLEND);
	glDisable(GL_TEXTURE_2D);
	glDepthFunc(GL_LESS);
	glTranslatef(0.0f, -0.05f, 0.0f);

	glutSwapBuffers();
}

void 
reshape(GLsizei w,GLsizei h)
{
	gWindowWidth	=	w;
	gWindowHeight	=	h;
	/* set up perspective projection */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glFrustum(-30.0f, 30.0f, -30.0f, 30.0f, 100.0f, 640.0f);
	glMatrixMode(GL_MODELVIEW);
}

int main(int argc,char ** argv)
{
	glutInit(&argc,argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
	glutInitWindowSize(gWindowWidth, gWindowHeight);
	glutCreateWindow("Heckbert&Herf's Shadows");

	glutReshapeFunc(reshape);
	glutDisplayFunc(draw);
	glutKeyboardFunc(keyboard);

	assert(init());

	glutMainLoop();

	return(0);
}

3. 阴影体

3.1. 原理

在1977年,Crow[10]提出了阴影体算法(Shadow Volume),来实现阴影效果。阴影体跟图形学中的视锥(View Volume)类似,表示一个三维空间区域,该区域中的物体会渲染出阴影。如图16所示,S表示光源,T表示要渲染的三角形面片,\pi表示接受面(这里为了演示方便,采用平面,实际上可以是任意形状的多边形)。那么,连接点S\Delta T的三个顶点,并延长线段,使得它们与平面相交,得到投影三角形T'。那么,渲染介于\Delta T\Delta T'之间的空间区域内的物体,由于\Delta T对光源的阻挡,渲染出来的物体会带有阴影的效果,称介于\Delta T\Delta T'之间的空间区域为阴影体

2015-3-19 11-54-55

图16. 阴影体

如图17所示,阴影体算法来实现阴影算法的基本思想。上面发光的点,表示光源,渲染三角形时,后面会形成一个阴影体;下面一个点,表示照相机的位置,它会形成一个视锥;视锥和阴影体相交的三维空间就是照相机可见的阴影区域,即如果照相机可见的点,在阴影体内,就会有三角形阴影的效果。

2015-3-19 11-55-41

图17. 阴影体确定阴影区域[10]

NVIDIA提供的图片可以非常形象的解释阴影体渲染阴影的效果,如图18所示,显示出了光源和阻挡物形成的阴影体,以及后面生成的阴影效果。

2015-3-19 11-55-52

图18. 阴影体渲染的阴影[11]

3.2. 朴素算法[1]

简单的设想下,要怎么判断一个像素点是否在阴影体中呢?有一种朴素算法是:(1)首先渲染不带阴影的场景,产生整个屏幕的视点样本(View Samples);(2)对于每个阴影体的面,通过一种特殊的片断着色器给它着色;(3)对于每个已绘制在屏幕上的像素,读取它的视点样本,判断是否在刚在渲染的阴影体内,如果在就输出黑色,否则就不变。

在现代的图形硬件中,这种算法是非常难实现的,但是在以前的提供有专有硬件的机器中——像素平面机[1](Pixel-Plane Machines),则比较容易实现。可如果屏幕的分辨率很高,即使是专有化的硬件也会不堪重负,因此急需探索新的算法。

在阴影体算法推出几年后,出现了硬件加速的模板缓冲区(Stencil Buffer),于是在十几年后,Heidmann描绘了一种基于硬件加速的模板缓冲区的阴影体的算法。

3.3. 模板阴影体

3.3.1. 原理

先介绍些专有名词,用二维图表示,如图19所示,称阻挡物面向光源的一侧为向光盖(Light Cap),称阴影体后面无穷远处或者接受面为背光盖(Dark Cap),光源、阻挡物可以形成一个阴影体,在阴影体的后面加上一个盖,就形成了一个密闭的模型了。阴影体算法中渲染的面包括:

(1)阻挡物的面

(2)阴影体侧面

(3)阴影接受物的面

(4)向光盖

(5)背光盖

面按照不同的参照物,有不同的分类。相对于视点而言,可以分为正面和背面;相对于光源来说,可以分为向光面和背光面。特别说明下正面和背面的概念,以图19为例,阴影体的侧面的正方向是垂直于侧面指向阴影体外,阴影体左侧面就称为正面,因为视点在左侧面的正方向上;阴影体右侧面就称为背面,因为图中视点在右侧面的。

2015-3-19 11-56-04

图19. 阴影体的二维几何示意图[1]

为了方便后面的介绍,这里简单的描述下模板缓冲区的概念和基本用法。OpenGL和DirectX都支持模板缓冲区,每个像素占的位数通常是1、4、8等,模板缓冲区是在图形渲染管线的片断操作阶段起作用的(OpenGL的图形渲染管线,参见《OpenGL原理介绍》图1)。片断操作,又可以细分为裁剪测试、透明度测试、模板测试、深度测试等,基本的测试过程如图20所示,模板测试发生在深度测试之间前。

2015-3-19 12-02-06

图20. 片断操作的几个处理步骤

只有存在模板缓冲区的情况下,才会进行模板测试,模板测试过程是:将存储在模板缓冲区中的值与参考值进行对比,通过测试的像素才可能在屏幕上显示,同时根据测试的结果,可能对模板缓冲区中的值进行修改。模板缓冲区用到的几个主要的函数,如下所示,函数完整的介绍,参考OpenGL接口文档[13]

glEnable/glDisable(GL_STENCIL_TEST);
glClearStencil(0);
glClear(… | GL_STENCIL_BUFFER_BIT);
glStencilFunc(function, ref, mask);
glStencilOp(stencil_fail, depth_fail, depth_pass);
glStencilMask(mask);

阴影体的朴素算法非常费时,也不适合现代的图形硬件,在1991年,Heidmann[12]提出了利用模板缓冲区来实现阴影体算法,称他提出的算法为z-pass算法。z-pass基于一个数学理论基础:以二维的几何形为例,判断一个点是否在几何形内的做法是,以这个点为原点,沿任意一个方向画一个射线,计算射线与几何形的交点个数,如果交点个数是偶数,则点在几何形外;否则的话,点在几何形内(暂不考虑相切这种特殊情况)。如图21所示,点{Q_1}在几何形外,点{Q_2}在几何形内。该数学原理也适用于三维的情况。

2015-3-19 12-02-20

图21. 二维空间上,点在几何形内的判定

Heidmann(1991)[12]提出的z-pass算法的简化过程如下所示:

1. 清除颜色缓冲区、深度缓冲区和模板缓冲区;
2. 开启深度测试,只使用环境光渲染场景;
3. 设置颜色缓冲区、深度缓冲区只可读;
4. 渲染两次阴影体的正面和背面
4.1 第一次渲染:渲染阴影体的正面,当深度测试成功的时候,模板缓冲区的值+1
4.2 第二次渲染:渲染阴影体的背面,当深度测试成功的时候,模板缓冲区的值-1

5. 模板缓冲区数值不等于0的像素,在阴影体内;数值等于0的像素,不在阴影体内;

以如图22为例,经过第1~2步,完成了场景中两个矩形的渲染,深度缓冲区中保存的是整个场景最靠近观察者的深度值。因为第3步设置颜色缓冲区和深度缓冲区只可读,所以在第4步的渲染,不会更新颜色缓冲区和深度缓冲区的值,但还可以进行深度测试,并根据结果,更新模板缓冲区的值。先考虑物体P上箭头所示的点,渲染阴影体的正面,即侧面a和b,相对于该像素,两个面深度测试都成功,那么该点对应的模板缓冲区的数值+2;接着,渲染阴影体的背面,即侧面c和d,深度测试失败,模板缓冲区的值不变。最终该像素在模板缓冲区中的值为+2,可以判定该像素在阴影中。同理,可以得到,物体Q上箭头所示的点在模板缓冲区中的值为0,即不在阴影中。

2015-3-19 12-02-34

图22. z-pass算法的二维几何示意图[1]

Heidmann提出的阴影体算法显著提高了阴影体技术的性能,使它在硬件上的实现变成可能,但是它却会引起错误,主要是当阴影体的侧面被视锥的近平面裁剪或者视点在阴影体内时,就会发生。易知,在顶点处理管线线,由视点可以引出一个视锥,由较近的位置切一刀,由较远的位置切一刀,形成两个平面,分别称为近平面和远平面,由视点只能观察到近平面与远平面之间包围的区域。如图23所示,(a)光源形成一个阴影体,阴影体的一个侧边恰好与近平面相交,由于固定管线处理中将位于视点与近平面之间的阴影体侧边裁减了,那么由绿色箭头指向的点的阴影渲染就会发生错误,因为在该箭头方向上阴影体的正面没有进行任何片断操作,而阴影体的背面,由于物体P的阻挡,深度测试无法通过,最后模板缓冲区在该点的值为0,错误的判断该点不在阴影体内。(b)视点在阴影体内,那么产生的计数值就会出错,因为根本没有阴影体正面的深度测试,其实可以看成是近平面裁剪的极端情况。

2015-3-19 12-02-48

图23. z-pass算法无法正确的渲染阴影的情况,(a)近平面裁剪引起的错误,(b)视点在阴影体内

针对视点在阴影体内的解决方法是通过计算包含眼睛的阴影体的个数,作出调整,但是这就增大了对CPU的负担。也有很多学者[14,15,16]尝试改进该方法,主要的目的,就是在近平面合适的位置加个向光盖,保证当阴影体的侧面被裁剪后,还能保证模板缓冲区中得到的计数值。有的方法需要CPU进行复杂的几何计算,几何计算又可能经历浮点数计算的误差,有的方法经验证,是错误的。直到2005年,Hornus et al[17]通过对视锥近平面的有效测试,解决了z-pass方法近平面裁剪的问题,Hornus et al称该方法为ZP+。本文不对ZP+方法进行详细介绍,有兴趣的人可以自行阅读该篇论文。

在2000年的时候,Carmack提出了z-fail方法,并将它应用到了DOOM3中,由于Carmack把这种算法推广给了更多的开发者,这种方法也称为Carmack’s Reverse。这个名字听起来就像是与z-pass对着干,z-fail方法的算法思想如下所示:

1. 清除颜色缓冲区、深度缓冲区和模板缓冲区;
2. 开启深度测试,只使用环境光渲染场景;             
3. 设置颜色缓冲区、深度缓冲区只可读;
4. 渲染两次阴影体的正面和背面
4.1 第一次渲染:渲染阴影体的背面,当深度测试失败的时候,模板缓冲区的值+1
4.2 第二次渲染:渲染阴影体的正面,当深度测试失败的时候,模板缓冲区的值-1

5. 模板缓冲区数值不等于0的像素,在阴影体内;数值等于0的像素,不在阴影体内;

显然,与z-pass算法不同的是第4.1~4.2步,通过这两步简单的变换,就能有效的避免z-pass算法引起的近平面裁剪的问题,如图24所示。视点在物体P的阴影体内,由于阴影体的一个侧面a在视锥外,被裁剪掉了。先考虑最左边的绿色箭头所指的点,算法第4.1步,渲染阴影体背面,即b和d,平面b深度测试成功,模板缓冲区的值不变,平面d深度测试失败,模板缓冲区的值+1;算法第4.2步,渲染阴影体正面,即c,平面c深度测试成功,模板缓冲区的值不变;第5步,可以判断模板缓冲区在该点的值为1,处于阴影内。同理,可以考虑中间和右侧的绿色箭头所示的点的阴影判定。

2015-3-19 12-03-03

图24. z-fail算法二维几何示意图

由上面的示例可以看出,z-fail算法解决了近平面裁剪和视点在阴影体内的问题,但是它把近平面裁剪的问题引到了远平面。如图25所示,现在考虑绿色箭头所示的点,理想的情况是绿色箭头与阴影体的一个侧边相交的,因此算法第4.1步,渲染背面,深度测试失败,模板缓冲区中对应的数值+1,第4.2步,渲染正面,深度测试成功,模板缓冲区中的值不变。实际上,阴影体的一个侧边被视锥的远平面裁剪掉,导致第4.1步,深度测试成功,模板缓冲区中的数值不变,错误就出现了。为了能够解决远平面裁剪的问题,就是在远平面的位置加上一个背光盖,就可以解决这个错误。Everitt&Kilgard[19]于2003年提出了一种解决方案,可以实现健壮的z-fail阴影体算法。

对于z-pass和z-fail两个算法来说,z-pass算法可能会引起近平面裁剪问题,基本的做法就是加上一个向光盖,但是过去很多人提出的方案都不能解决问题,直到2005年Hornus et al[17]提出zp+算法;z-fail算法,可能引起远平面裁剪问题,基本的做法就是加上一个背光盖,在2003年Everitt&Kilgard[19]提出了一种较为健壮的算法实现,而且NVIDIA还为了支持该算法,提供了硬件层的支持:GL_ARB_depth_clamp[20],后面会详细阐述这个算法。

2015-3-19 12-03-14

图25. z-fail算法的远平面裁剪问题

3.3.2. 无穷远的表示

在阴影体侧面渲染中,由轮廓边和光源构成一个平面,沿着光源的照射方向,延伸到无穷远处。怎么表示无穷远呢?如果简单的指定一个非常大的定长的四边形,以为它就“应该”就是包括阴影体内的所有物体了,这种方法是错误的。如图26所示(a),当我们指定一个非常大的定长的四边形,以为它就可以包含整个世界了,但是却出现了(b)所示的情况,当光源与几何体靠的非常的近,固定一个很大的四边形是无法覆盖整个阴影体的,也许我们只看包含住一只小小的蚂蚁。

2015-3-19 12-03-25

图26. 指定一个很大的四边形是否可以覆盖整个阴影体?

这时候的一个想法就是采用齐次坐标,三维空间的齐次坐标表示为\left( {x,y,z,w} \right),它实际表示的坐标为\left( {x/w,y/w,z/w} \right),OpenGL也指定了glVertex4f()方法来渲染指定齐次坐标点。当齐次坐标的w=0时,它并不是表示一个点,而是表示一个方向。如图27所示,(a)表示标准的三角形,(b)三角形的一个顶点表示方向,两条边沿着该齐次坐标所示的方向无穷延伸,还有一条边是定长的,(c)三角形的两个顶点表示方向,两条边向无穷延伸。

2015-3-19 12-14-05

图27. 三角形的三种齐次表示,(a)三角形三个顶点第4个分量都是1,(b)一个顶点的第4个分量为0,(c)两个顶点的第4个分量为0

顶点的齐次表示可以表示无穷远的平面。现在给定光源L = \left( {Lx,{\rm{ }}Ly,{\rm{ }}Lz,{\rm{ }}Lw} \right)和顶点A = \left( {Ax,{\rm{ }}Ay,{\rm{ }}Az,{\rm{ }}Aw} \right)B = \left( {Bx,{\rm{ }}By,{\rm{ }}Bz,{\rm{ }}Bw} \right),指定延伸的无穷远方向为A - LB - L,那么需要渲染的无穷远的平面如下所示:

\left( {Bx,{\rm{ }}By,{\rm{ }}Bz,{\rm{ }}Bw} \right)

\left( {Ax,{\rm{ }}Ay,{\rm{ }}Az,{\rm{ }}Aw} \right)

\left( {AxLw - LxAw,AyLw - LyAw,AzLw - LzAw,0} \right)

\left( {BxLw - LxBw,ByLw - LyBw,BzLw - LzBw,0} \right)

3.3.3. 轮廓确定算法

这里,以三角形面片作为三维模型的基本构造图形,即整个三维模型可以分解为一个个三角形面片。这又可分为两种情况:(1)每个三角形的顶点可以是任意顺序的,另外还需要特别指定面的法向量;(2)把面的法向量方向隐藏在三角形的顶点顺序中。如图28所示,规定的是逆时针方向是平面的正方向,也可以通过gl接口void glFrontFace(GLenum mode)改为顺时针是平面的正方向。

2015-3-19 12-14-20

图28. 三角形面片的存储,(a)三角形顶点的顺序,(b)三角形面的法向量确定方法

这里只讨论密闭的三维模型(Closed Model),即模型的每条边有且只有两个相邻面。给定一个光源和模型,获得该模型的可能的轮廓边(Possible Silhouette)。所谓的可能的轮廓边是指什么呢?如图29所示,一个光源的光线射向一个物体,那么图中{p_0},{p_5},{p_6},{p_4}四个点就构成了该物体的轮廓边,连接光源p与轮廓边\overline {{p_0}{p_5}} ,\overline {{p_5}{p_2}} ,\overline {{p_2}{p_4}} ,\overline {{p_4}{p_0}},并延长,就够成了该物体形成的阴影体了。可能的轮廓边,指的是那些两个三角形共有,且一个三角形是向光面而另一个三角形是背光面。为什么这里说是可能的轮廓边,而不是确定就是轮廓边呢,原因就在于模型可能是凹多面体,那些边不完成是阴影和非阴影区域的分界线,它是轮廓边的一个超集。

2015-3-19 12-14-29

图29. 光源和轮廓边,点p表示光源所在的位置

3.3.4. 健壮的z-fail算法

接着,详细介绍Everitt&Kilgard(2003)[19]提出的健壮的z-fail算法实现。

视点坐标变换裁剪坐标,需要一个透视变换,透视变换的矩阵等式(7)所示,对透视变换的介绍,可以参考文章《OpenGL原理介绍》。如果一个齐次坐标(x, y, z, w),经过这个透视变换,由于超出了远平面的深度值,所以可能会被裁剪掉。通过采用NVIDIA提出的GL_ARB_depth_clamp[20]扩展,取消远平面,即在远平面后面也可以渲染出来,也可以采用矩阵Pinf,跟矩阵P相比,就只有第三行发生了变换,使用矩阵Pinf,无穷远处的坐标也不会被裁剪掉。

P = \left( {\begin{array}{*{20}{c}}{\frac{{2 \times Near}}{{Right - Left}}}&0&{\frac{{Right + Left}}{{Right - Left}}}&0\\0&{\frac{{2 \times Near}}{{Top - Bottom}}}&{\frac{{Top + Bottom}}{{Top - Bottom}}}&0\\0&0&{ - \frac{{Far + Near}}{{Far - Near}}}&{ - \frac{{2 \times Far \times Near}}{{Far - Near}}}\\0&0&{ - 1}&0\end{array}} \right)(7)

\mathop {\lim }\limits_{Far \to \infty } P = {P_{\inf }} = \left( {\begin{array}{*{20}{c}}{\frac{{2 \times Near}}{{Right - Left}}}&0&{\frac{{Right + Left}}{{Right - Left}}}&0\\0&{\frac{{2 \times Near}}{{Top - Bottom}}}&{\frac{{Top + Bottom}}{{Top - Bottom}}}&0\\0&0&{ - 1}&{ - 2 \times Near}\\0&0&{ - 1}&0\end{array}} \right)(8)

       采用GL_ARB_depth_clamp[20]扩展确实可以有效的解决图25所示的箭头无法与阴影体侧边相交的问题,但是这样就解决了远平面裁剪的问题了吗?显然是没有的。依旧以图25为例,将绿色箭头绕着它的端点顺时针旋转,使得它与阴影体的右侧面平行,如图30所示。

2015-3-19 13-11-55

图30. 取消远平面后,还存在的错误

现在的解决方案就是给阴影体加盖,包括向光盖和背光盖。由于采用了矩阵Pinf,可以渲染到无穷远,对于w=0的齐次坐标不会被裁剪,渲染向光盖的方法和背光盖的方法如下所示:

对于每个阻挡物的三角形面片\Delta ABC,如果它是向光面,则渲染背光盖,即三角形面片\Delta ABC

\left( {Bx,{\rm{ }}By,{\rm{ }}Bz,{\rm{ }}Bw} \right)

\left( {Ax,{\rm{ }}Ay,{\rm{ }}Az,{\rm{ }}Aw} \right)

\left( {Cx,{\rm{ }}Cy,{\rm{ }}Cz,{\rm{ }}Cw} \right)

对于每个阻挡物的三角形面片\Delta ABC,如果是背光面,则渲染背光盖,即无穷大的三角形面片A - L,B - L,C - L

\left( {AxLw - LxAw,AyLw - LyAw,AzLw - LzAw,0} \right)

\left( {BxLw - LxBw,ByLw - LyBw,BzLw - LzBw,0} \right)

\left( {CxLw - LxCw,CyLw - LyCw,CzLw - LzCw,0} \right)

健壮的z-fail算法的伪码如下所示:

1. 清除深度缓冲区为1.0,清除颜色缓冲区
    Clear(DEPTH_BUFFER_BIT | COLOR_BUFFER_BIT);
2. 把Pinf矩阵,作为视点坐标向裁剪坐标的变换矩阵
           pinf[0][1] = pinf[0][2] = pinf[0][3] = pinf[1][0] = 0;
           pinf[1][2] = pinf[1][3] = pinf[3][0] = pinf[3][1] = 0;
           pinf[3][3] = 0;
           pinf[0][0] = 2*near / (right – left);
           pinf[1][1] = 2*near / (top – bott);
           pinf[2][0] = (right + left) / (right – left);
           pinf[2][1] = (top + bott) / (top – bott);
           pinf[2][2] = pinf[2][3] = -1;
           pinf[3][2] = -2*near;
           MatrixMode(PROJECTION);
           LoadMatrixf(&pinf[0][0]);
3. 装载模型视图矩阵,设置场景的观察点
           MatrixMode(MODELVIEW);
           loadCurrentViewTransform();
4. 用环境光渲染场景,打开深度测试,背面裁剪。
    Enable(DEPTH_TEST);DepthFunc(LESS);
    Enable(CULL_FACE);CullFace(BACK);
    SetAmbientLight();
    drawScene();
5. 设置深度缓冲区只可读,打开颜色混合,光闭环境光
    DepthMask(0);
    Enable(BLEND);BlendFunc(ONE,ONE);
    StopAmbientLight();
6. 对于每一个光源:
A. 清除模板缓冲区
    Clear(STENCIL_BUFFER_BIT);
B. 屏蔽颜色缓冲区可写,打开模板测试,打开模板缓冲区可写
    ColorMask(0,0,0,0);
    Enable(STENCIL_TEST);
    StencilFun(ALWAYS,0,~0);StencilMask(~0);
C. 对于每一个阻挡物:
a. 根据光源的相对位置,确定构成阻挡物的三角形面片的向光性或背光性
b. 配置zfail模板缓冲区的参数设置
    CullFace(FRONT);StencilOp(KEEP,INCR,KEEP);
c. 根据轮廓边和光源的相对位置,渲染阴影体的侧面。对于每个轮廓边A,B
    Vertex4f(B.x,B.y,B.z,B.w);
    Vertex4f(A.x,A.y,A.z,A.w);
    Vertex4f(A.x*L.w-L.x*A.w, A.y*L.w-L.y*A.w, A.z*L.w-L.z*A.w, 0);      // 无穷远
    Vertex4f(B.x*L.w-L.x*B.w, B.y*L.w-L.y*B.w, B.z*L.w-L.z*B.w, 0);        // 无穷远
d. 渲染阻挡物的三角形面片,换句话说就是渲染向光盖和背光盖。
    Begin(TRIANGLES);
for (int i=0; i<numTris; i++){  // 遍历每个三角形面片 
// 如果三角形面片是背光面(相对于当前光源来说)
if (triList[i].backFacing) {
for (int j=0; j<3; j++)               //遍历三角形的每个顶点
Vertex4f(V.x*L.w-L.x*V.w, V.y*L.w-L.y*V.w, V.z*L.w-L.z*V.w, 0);
}
else{
for (int j=0; j<3; j++)             //遍历三角形的每个顶点
Vertex4f(V.x,V.y,V.z,V.w);
}
        }
    End();
e. 配置zfail模板缓冲区的参数,
    CullFace(BACK);StencilOp(KEEP,DECR,KEEP);
f. 重复(c)到(d)的操作,这次渲染的是正面,而不是背面了。
D. 开启光源,把它放置到固定位置
    SetNormalLight(position);
E. 设置模板测试,渲染模板缓冲区中值为0的像素。对于测试通过的像素,将它在模板缓冲区中的值加1,避免重复混合(Double Blending),开启颜色缓冲区可写,使用equal进行深度测试更新可视的像素’
F. 重新渲染整个场景
    drawScene()
G. 恢复深度测试为less
     DepthFunc(LESS);
7. 屏蔽颜色混合和模板测试,重新开启深度缓冲区可写

    Disable(BLEND);Disable(STENCIL_TEST);DepthMask(1);

OpenGL中提供了正面和背面的模板测试操作glStencilOpSeparate(),这就使得原先需要两次渲染才能实现的效果变为一次渲染。

3.3.5. 总结

对比z-pass算法和z-fail算法,两个算法都会引起错误,z-pass算法是近平面裁剪问题,z-fail是远平面裁剪问题。由Hornus[17](2005)提出了解决z-pass问题的方法,称为zp+算法,Everitt&Kilgard[19](2003)提出了解决z-fail问题的方法,实现了健壮的z-fail算法。由于本人对zp+算法没有进行深入理解,无法对两个算法的效率进行对比。单独分析z-fail算法,由于它取消了远平面,所以更多的几何图形没有被裁剪掉,而且需要渲染背光盖,都是它效率降低的地方。

对于模板阴影体算法中的模板缓冲区来说,不同的硬件实现中模板缓冲区占有的位数不同,有1位的,有8位的,也有16位的。在一个非常复杂的场景中,容易造成溢出,或者是因为正面的递增,也或者是因为背面的递减。OpenGL中也提供了GL_INCR_WRAP和GL_DECR_WRAP方法,来限制溢出错误,当超过上限时会变成0(例如8bit的模板缓冲区,255+1就会变成0)。能处理的场景的复杂度,很依赖模板缓冲区的位数,但是实际上只要有16位的模板缓冲区,就足够用了。

阴影体算法有两个瓶颈:(1)几何处理,需要计算几何面的向光性和背光性;(2)填充率(Fill Rate)[22],因为阴影体需要产生大量的像素来更新模板缓冲区。对于简单的场景,模板阴影体算法是适用的,但是对于复杂的场景就够呛了。对于阴影体算法在复杂场景中存在的瓶颈,也有人提出了改进建议,比如采用LOD等,更多的介绍参见Eisemann[1](2011)以及Steiner[23](2006)。

3.3.6. 实现

最后,再简单介绍下阴影确定算法中的数据结构和对它的约束条件假设数据结构与附属信息分离开来,像纹理映射信息、物理信息等。一个场景包括一个观察者、对应的视口、一组模型和一组光源,光源的数量不受限制。模型的位置和数量受到模板缓冲区的位数的限制,仅管如此,在实际应用中很少关心到这个约束限制。

模板阴影算法要求模型是封闭的三角网格构成,归结起来就是网格上的每条边必需是两个三角形共有,不允许存在“洞”,使我们能看到网格的内部,因此,一个有f个三角形的模型,最多只有3f/2条边。

class Model{
public:
	Triangle*	triangle;			//The triangles of the model.
	SrVector3*	vertex;
	Edge*		edge;
	int			numVertex;
	int			numTriangle;
};

有了每个模型基本的顶点,边等信息,还需要它的朝向、位置、颜色等信息,所以采用下面的数据结构进行更高一层的包装。

class BasicModel{
public:
	Model		model;
	SrVector3	color;
	SrVector3	position;
	SrVector3	rotate;
	SrVector3	objectSpaceLightPosition;
};

每个三角形存储有三个顶点的索引和一个Boolean域,布尔域在阴影渲染算法中会作为临时存储空间使用到。

class Edge  {
public:
	int vertexIndex[2];
	int triangleIndex[2];
};

每条边包含两个顶点的索引和两个相邻的面,边的方向是vertexIndex[0]指向vertexIndex[1],算法构造一组可能的轮廓边:

class Triangle  {
public:
	bool	lightFacing;
	int		index[3];
};

实现的阴影效果图如下所示。此外,NEHE教程或者paulsprojects[24]提供的代码中提供了另外一种数据结构的设计,paulsprojects[24] 提供的代码实现了Z-Pass,Z-Fail还有用着色语言实现的阴影体算法,很全,可读性也挺好的,只是代码量有点儿大,在文章最后也提供了该代码的实现。

2015-3-19 13-12-14

图31. 阴影体算法的实现DEMO

/************************************************************************		
\link	www.twinklingstar.cn
\author Twinkling Star
\date	2014/03/17
****************************************************************************/
#include <stdlib.h>
#include <stdio.h>
#include <GL/glut.h>
#include "SrVector3.h"

class Triangle  
{
public:
	Triangle()
	{
		index[0] = index[1] = index[2] = -1;
		lightFacing = false;
	}
	bool	lightFacing;
	int		index[3];
};

class Edge  
{
public:
	Edge()
	{
		vertexIndex[0] = vertexIndex[1] = -1;
		triangleIndex[0] = triangleIndex[1] = -1;
	}
	int vertexIndex[2];
	int triangleIndex[2];
};

class Model
{
public:
	Model()
	{
		triangle	= NULL;
		vertex		= NULL;
		edge		= NULL;
		numVertex	= 0;
		numTriangle = 0;
	}
	~Model()
	{
		if( triangle )
		{
			delete []triangle;
			triangle = NULL;
		}
		if( vertex )
		{
			delete []vertex;
			vertex = NULL;
		}
		if( edge )
		{
			delete []edge;
			edge = NULL;
		}
	}
	Triangle*	triangle;			//The triangles of the model.
	SrVector3*	vertex;
	Edge*		edge;
	int			numVertex;
	int			numTriangle;

private:
	Model(const Model&);
	const Model& operator = (const Model&);
};

class BasicModel
{
public:
	Model		model;
	SrVector3	color;
	SrVector3	position;
	SrVector3	rotate;
	SrVector3	objectSpaceLightPosition;
};

class Light
{
public:
	SrVector3 position;
	SrVector3 color;
};

float		gYAxisAngle				= 0;
float		gXAxisAngle				= 0;
int			gLastMouseX			= 0;
int			gLastMouseY			= 0;
float		gZoom				= 0;
float		gHorizon			= 0;
bool		gMoveScene			= false;
bool		gStopTimer			= false;
bool		gDisplaySide		= false;
bool		gIsZFail			= true;

BasicModel* gModel				= NULL;

SrVector3	gLightPosition		= SrVector3(1.0f,1.0f,-4.0f);
SrVector3	gRotate				= SrVector3(0,0,0);
SrVector3	gBlack				= SrVector3(0,0,0);
SrVector3	gWhite				= SrVector3(1.0f,1.0f,1.0f);

void outputInfo()
{
	printf("***********************************************************\n");
	printf("Up:		Zoom in\n");
	printf("Down:		Zoom out\n");
	printf("Left:		Move left\n");
	printf("Right:		Move Right\n");
	printf("S:		Stop or start rotation\n");
	printf("F:		Display the side of the shadow volume\n");
	printf("C:		Shift Between the Z-Fail and the Z-Pass.\n");
	printf("Default algorithm: Z-Fail\n");
	printf("***********************************************************\n");
}

void setEdges(BasicModel& BasicModel)
{
	Model* model = &BasicModel.model;
	int numEdge = model->numTriangle * 3 / 2;
	model->edge = new Edge[ numEdge ];
	int indxEdge = 0;
	int tri1 , tri2;
	int indx1 , indx2;

	for( tri1=0 ; tri1<model->numTriangle ; tri1++ )
	{
		for( indx1 = 0 ; indx1<3 ; indx1++ )
		{
			int ep1 = model->triangle[tri1].index[indx1];
			int ep2 = model->triangle[tri1].index[(indx1+1)%3];
			for( tri2=tri1+1 ; tri2<model->numTriangle ; tri2++ )
			{
				for( indx2 = 0 ; indx2<3 ; indx2++ )
				{
					int ep3 = model->triangle[tri2].index[indx2];
					int ep4 = model->triangle[tri2].index[(indx2+1)%3];
					if( ep1==ep4 && ep2==ep3 )
					{
						model->edge[ indxEdge ].triangleIndex[0] = tri1;
						model->edge[ indxEdge ].triangleIndex[1] = tri2;
						model->edge[ indxEdge ].vertexIndex[0] = ep1;
						model->edge[ indxEdge ].vertexIndex[1] = ep2;
						indxEdge ++;
					}
				}
			}
		}
	}
}

void buildTetrahedron(BasicModel*& basicModel,
					  const SrVector3& position = SrVector3(0.0f,0.0f,0.0f),
					  const SrVector3& rotate = SrVector3(0.0f,0.0f,0.0f),
					  const SrVector3& color = SrVector3(1.0f,1.0f,1.0f))
{
	basicModel = new BasicModel;
	basicModel->color = color;
	basicModel->position = position;
	basicModel->rotate = rotate;

	Model* model = &basicModel->model;

	model->numVertex = 4;
	model->vertex = new SrVector3[ model->numVertex ];

	model->numTriangle = 4;
	model->triangle = new Triangle[ model->numTriangle ];

	model->vertex[0].set(0,0,0);
	model->vertex[1].set(1,0,0);
	model->vertex[2].set(0,1,0);
	model->vertex[3].set(0,0,1);

	model->triangle[0].index[0] = 0;
	model->triangle[0].index[1] = 1;
	model->triangle[0].index[2] = 3;

	model->triangle[1].index[0] = 0;
	model->triangle[1].index[1] = 2;
	model->triangle[1].index[2] = 1;

	model->triangle[2].index[0] = 2;
	model->triangle[2].index[1] = 0;
	model->triangle[2].index[2] = 3;

	model->triangle[3].index[0] = 1;
	model->triangle[3].index[1] = 2;
	model->triangle[3].index[2] = 3;

	setEdges(*basicModel);
}

void computeLightFace(const BasicModel& basicModel)
{
	int i;
	for( i=0 ; i<basicModel.model.numTriangle ; i++ )
	{
		SrVector3 v0 , v1 , v2;
		v0 = basicModel.model.vertex[basicModel.model.triangle[i].index[0]];
		v1 = basicModel.model.vertex[basicModel.model.triangle[i].index[1]];
		v2 = basicModel.model.vertex[basicModel.model.triangle[i].index[2]];
		SrVector3 normal = (v1 - v0).cross(v2 - v0);
		if( normal.dot(basicModel.objectSpaceLightPosition) - normal.dot(v0) <0 )	
		{// Back Face
			basicModel.model.triangle[i].lightFacing = false;
		}
		else	
		{//Front Face
			basicModel.model.triangle[i].lightFacing = true;
		}
	}
}

void drawBasicModel(const BasicModel& basicModel)
{
	glColor3f(basicModel.color.x,basicModel.color.y,basicModel.color.z);

	glPushMatrix();
		glTranslatef(basicModel.position.x,basicModel.position.y,basicModel.position.z);
		glRotatef(basicModel.rotate.x,1.0f,0,0);
		glRotatef(basicModel.rotate.y,0,1.0f,0);
		glRotatef(basicModel.rotate.z,0,0,1.0f);
		int i;
		SrVector3 normal;
		for( i=0 ; i<basicModel.model.numTriangle ; i++ )
		{
			SrVector3 v0 , v1 , v2;
			v0 = basicModel.model.vertex[basicModel.model.triangle[i].index[0]];
			v1 = basicModel.model.vertex[basicModel.model.triangle[i].index[1]];
			v2 = basicModel.model.vertex[basicModel.model.triangle[i].index[2]];
			normal = (v1 - v0).cross(v2 - v0);
			glBegin(GL_TRIANGLES);
				glNormal3f(normal.x,normal.y,normal.z);
				glVertex3f(v0.x,v0.y,v0.z);
				glVertex3f(v1.x,v1.y,v1.z);
				glVertex3f(v2.x,v2.y,v2.z);
			glEnd();
		}
	glPopMatrix();
}

void drawBox(const SrVector3& minBox, const SrVector3& maxBox,const SrVector3& color = SrVector3(0,1.0f,0))
{
	glColor3f(color.x,color.y,color.z);
	glPushMatrix();
		glBegin(GL_QUADS);
		//back face
		glNormal3f(0.0f,0.0f,1.0f);
		glVertex3f(minBox.x,minBox.y,minBox.z);
		glVertex3f(maxBox.x,minBox.y,minBox.z);
		glVertex3f(maxBox.x,maxBox.y,minBox.z);
		glVertex3f(minBox.x,maxBox.y,minBox.z);

		//front face
		glNormal3f(0.0f,0.0f,-1.0f);
		glVertex3f(minBox.x,minBox.y,maxBox.z);
		glVertex3f(minBox.x,maxBox.y,maxBox.z);
		glVertex3f(maxBox.x,maxBox.y,maxBox.z);
		glVertex3f(maxBox.x,minBox.y,maxBox.z);

		//left face
		glNormal3f(1.0f,0.0f,0.0f);
		glVertex3f(minBox.x,minBox.y,minBox.z);
		glVertex3f(minBox.x,maxBox.y,minBox.z);
		glVertex3f(minBox.x,maxBox.y,maxBox.z);
		glVertex3f(minBox.x,minBox.y,maxBox.z);

		//right face
		glNormal3f(-1.0f,0.0f,0.0f);
		glVertex3f(maxBox.x,minBox.y,minBox.z);
		glVertex3f(maxBox.x,minBox.y,maxBox.z);
		glVertex3f(maxBox.x,maxBox.y,maxBox.z);
		glVertex3f(maxBox.x,maxBox.y,minBox.z);

		//top face
		glNormal3f(0.0f,-1.0f,0.0f);
		glVertex3f(minBox.x,maxBox.y,minBox.z);
		glVertex3f(maxBox.x,maxBox.y,minBox.z);
		glVertex3f(maxBox.x,maxBox.y,maxBox.z);
		glVertex3f(minBox.x,maxBox.y,maxBox.z);

		//bottom face
		glNormal3f(0.0f,1.0f,0.0f);
		glVertex3f(minBox.x,minBox.y,minBox.z);
		glVertex3f(minBox.x,minBox.y,maxBox.z);
		glVertex3f(maxBox.x,minBox.y,maxBox.z);
		glVertex3f(maxBox.x,minBox.y,minBox.z);

		glEnd();
	glPopMatrix();
}

void timerMessage(int id)
{
	gRotate.y += 10;
	gRotate.x += 10;
	glutPostRedisplay();
	if( !gStopTimer )
		glutTimerFunc(200,timerMessage,0);
}

void setNormalLight( const SrVector3& lightPosition = SrVector3(0,0,0))
{
	SrVector3 lightColor = SrVector3(0.5,0.5f,0.5f);
	glEnable(GL_LIGHTING);
	glLightfv(GL_LIGHT1, GL_POSITION, lightPosition.get()); 
	glLightfv(GL_LIGHT1, GL_AMBIENT, (lightColor/5.0f).get());
	glLightfv(GL_LIGHT1, GL_DIFFUSE, (lightColor).get());
	glLightfv(GL_LIGHT1, GL_SPECULAR, gWhite.get());
}

void setAmbient( )
{	
	SrVector3 lightColor = SrVector3(0.2,0.2f,0.2f);
	glEnable(GL_LIGHTING);
	glLightfv(GL_LIGHT1, GL_AMBIENT, (lightColor/2.0f).get());
	glLightfv(GL_LIGHT1, GL_DIFFUSE, lightColor.get());
	glLightfv(GL_LIGHT1, GL_SPECULAR, gBlack.get());
}

void getInverseMatrix(const SrVector3& shift, 
					  const SrVector3& rotate,
					  float* result)
{
	glPushMatrix();
		glLoadIdentity();
		glRotatef(-rotate.z,0,0,1.0f);
		glRotatef(-rotate.y,0,1.0f,0);
		glRotatef(-rotate.x,1.0f,0,0);
		glTranslatef(-shift.x,-shift.y,-shift.z);
		glGetFloatv(GL_MODELVIEW_MATRIX,result);
	glPopMatrix();
}

void getMatrixMultiVector3(float* matrix,const SrVector3& p, SrVector3& result)
{
	result.x = p.x*matrix[0] + p.y*matrix[4] + p.z*matrix[8] + matrix[12];
	result.y = p.x*matrix[1] + p.y*matrix[5] + p.z*matrix[9] + matrix[13];
	result.z = p.x*matrix[2] + p.y*matrix[6] + p.z*matrix[10] + matrix[14];
}

void drawLight()
{
	glColor3f(1.0f,1.0f,1.0f);
	glPushMatrix();
		glTranslatef(gLightPosition.x,gLightPosition.y,gLightPosition.z);
		glutSolidSphere(0.05,32,32);
	glPopMatrix();
}

void drawSilhouetteEdge(const BasicModel& basicModel)
{
	glPushMatrix();
		float color[3] = {1.0f,0,0};
		glTranslatef(basicModel.position.x,basicModel.position.y,basicModel.position.z);
		glRotatef(basicModel.rotate.x,1.0f,0,0);
		glRotatef(basicModel.rotate.y,0,1.0f,0);
		glRotatef(basicModel.rotate.z,0,0,1.0f);
		int numEdge = 0;
		glBegin(GL_QUADS);
			int i;
			for( i=0 ; i<basicModel.model.numTriangle*3/2 ; i++ )
			{
				bool tr1LightFacing = basicModel.model.triangle[basicModel.model.edge[i].triangleIndex[0]].lightFacing;
				bool tr2LightFacing = basicModel.model.triangle[basicModel.model.edge[i].triangleIndex[1]].lightFacing;
				if( tr1LightFacing!=tr2LightFacing )
				{
					SrVector3 v0, v1;
					if( !tr1LightFacing )
					{
						v0 = basicModel.model.vertex[basicModel.model.edge[i].vertexIndex[0]];
						v1 = basicModel.model.vertex[basicModel.model.edge[i].vertexIndex[1]];
					}
					else
					{
						v1 = basicModel.model.vertex[basicModel.model.edge[i].vertexIndex[0]];
						v0 = basicModel.model.vertex[basicModel.model.edge[i].vertexIndex[1]];

					}
					numEdge ++;
					if( numEdge%3==0 )
					{
						color[1] = 1.0f;
					}
					else
					{
						color[2] += 0.2f;
					}
					glColor3f(color[0] , color[1],color[2]);
					glVertex3f(v0.x,v0.y,v0.z);
					glVertex3f(v1.x,v1.y,v1.z);
					glVertex4f(v1.x - basicModel.objectSpaceLightPosition.x, 
								v1.y - basicModel.objectSpaceLightPosition.y, 
								v1.z - basicModel.objectSpaceLightPosition.z, 
								0.0f );
					glVertex4f(v0.x - basicModel.objectSpaceLightPosition.x, 
								v0.y - basicModel.objectSpaceLightPosition.y, 
								v0.z - basicModel.objectSpaceLightPosition.z, 
								0.0f );
				}
			}
		glEnd();

	glPopMatrix();

}

void drawCap(const BasicModel& basicModel)
{
	int i , j;
	glPushMatrix();
		glTranslatef(basicModel.position.x,basicModel.position.y,basicModel.position.z);
		glRotatef(basicModel.rotate.x,1.0f,0,0);
		glRotatef(basicModel.rotate.y,0,1.0f,0);
		glRotatef(basicModel.rotate.z,0,0,1.0f);
		glBegin(GL_TRIANGLES);
			for( i=0 ; i<basicModel.model.numTriangle ; i++ )
			{
				if( !basicModel.model.triangle[i].lightFacing )
				{
					if(gIsZFail)
					{
						for( j=0 ; j<3 ; j++ )
						{
							glVertex4f( basicModel.model.vertex[basicModel.model.triangle[i].index[j]].x - basicModel.objectSpaceLightPosition.x,
								basicModel.model.vertex[basicModel.model.triangle[i].index[j]].y - basicModel.objectSpaceLightPosition.y,
								basicModel.model.vertex[basicModel.model.triangle[i].index[j]].z - basicModel.objectSpaceLightPosition.z,
								0.0f);
						}
					}
				}
				else
				{
					for( j=0 ; j<3 ; j++ )
					{
						glVertex3f( basicModel.model.vertex[basicModel.model.triangle[i].index[j]].x,
									basicModel.model.vertex[basicModel.model.triangle[i].index[j]].y,
									basicModel.model.vertex[basicModel.model.triangle[i].index[j]].z);
					}
				}
			}
		glEnd();
	glPopMatrix();
}

void renderMessage( )
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	SrVector3 minBox = SrVector3(-2.0f,-2.0f,-10.0f);
	SrVector3 maxBox = SrVector3( 2.0f, 2.0f,10.0f);
	glTranslatef(gHorizon,0,gZoom);
	glTranslatef(0,0,-4.0f);
	glRotatef(gYAxisAngle,0,1.0f,0);
	glRotatef(gXAxisAngle,1.0f,0,0);
	glTranslatef(0,0,4.0f);

	/*drawBox(minBox,maxBox,SrVector3(0.2f, 0.2f, 1.0f));*/

	gModel->rotate = gRotate;
	/*
		Render the scene with depth testing, back-face culling, and
		all light sources disabled (ambient & emissive illumination only)
	*/
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	setAmbient();

	drawBox(minBox,maxBox,SrVector3(0.2f, 0.2f, 1.0f));
	drawBasicModel(*gModel);
	/*
		Disable depth writes, enable additive blending, and set the 
		global ambient light contribution to zero (and zero any 
		emissive contribution if present)
	*/
	glDepthMask(0);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE,GL_ONE);

	/*
		For each light source:
	*/
	glStencilFunc(GL_ALWAYS,0,0);
	glStencilMask(0xff);
	/*
		Disable color buffer writes and enable stencil testing with
		the always stencil function and writing stencil.
	*/
	glColorMask(false,false,false,false);
	/*
		Clear the stencil buffer to zero.
	*/
	glEnable(GL_STENCIL_TEST);
	glClearStencil(0);
	glClear(GL_STENCIL_BUFFER_BIT);
	/*
		For each occluder:
	*/
	/*
		Determine whether each triangle in the occluder's model is 
		front- or back-facing with respect to the light's position.
	*/
	float invMatrix[16];
	getInverseMatrix(gModel->position,gModel->rotate,invMatrix);
	getMatrixMultiVector3(invMatrix,gLightPosition,gModel->objectSpaceLightPosition);
	computeLightFace(*gModel);

	int pass = 0;
	for( pass = 0 ; pass<2 ; pass ++ )
	{
		if( pass==0 )
		{
			if( gIsZFail )
			{/*
					Configure zfail stencil testing to increment stencil for
					back-facing polygons that fail the depth test.
				*/
				glCullFace(GL_FRONT);
				glStencilOp(GL_KEEP,GL_INCR,GL_KEEP);
			}
			else
			{
				/*
					Configure zpass stencil testing to increment stencil for
					front-facing polygons that succeed the depth test.
				*/
				glCullFace(GL_BACK);
				glStencilOp(GL_KEEP,GL_KEEP,GL_INCR);
			}
		}
		else
		{
			if( gIsZFail )
			{
				/*
					Configure zfail stencil testing to decrement stencil for
					front-facing polygons that fail the depth test.
				*/
				glCullFace(GL_BACK);
				glStencilOp(GL_KEEP,GL_DECR,GL_KEEP);			
			}
			else
			{
				/*
					Configure zpass stencil testing to decrement stencil for
					back-facing polygons that succeed the depth test.
				*/
				glCullFace(GL_FRONT);
				glStencilOp(GL_KEEP,GL_KEEP,GL_DECR);
			}
		}
		/*
			Render all possible silhouette edges as quads that 
			project from the edge away from the light to infinity.
		*/
		drawSilhouetteEdge(*gModel);
		/*
			Specially render all the occluder triangles.Project and 
			render back facing triangles away from the light to infinity. 
			Render front-facing triangles directly.
		*/
		drawCap(*gModel);
	}
	/*
		Position and enable the current light (and otherwise configure 
		the light's attenuation, color, etc.)
	*/
	setNormalLight(gLightPosition);
	/*
		Set stencil testing to render only pixels with a zero stencil value, 
		i.e., visible fragments illuminated by the current light. Use equal
		depth testing to update only the visible fragments, and then, increment
		stencil stencil to avoid double blending. Re-enable color buffer writes again.
	*/
	glStencilFunc(GL_EQUAL,0,0xff);
	glStencilOp(GL_KEEP,GL_KEEP,GL_INCR);
	glDepthFunc(GL_LEQUAL);
	glColorMask(1.0f,1.0f,1.0f,1.0f);
	/*
		Re-draw the scene to add the contribution of the current light to illuminated
		(non-shadowed) regions of the scene.
	*/

	glCullFace(GL_BACK);
	drawBasicModel(*gModel);
	drawBox(minBox,maxBox,SrVector3(0.2f, 0.2f, 1.0f));
	/*
		Disable blending and stencil testing; re-enable depth writes. Restore the depth test.
	*/
	glDisable(GL_BLEND);
	glDepthFunc(GL_LEQUAL);
	glDisable(GL_STENCIL_TEST);
	glDepthMask(1);

	drawLight();

	if( gDisplaySide )
	{
		drawSilhouetteEdge(*gModel);
	}

	glFlush();
}

void init()
{
	glClearColor(0.0f,0.0f,0.0f,0.0f);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	float MatAmb[] = {0.4f, 0.4f, 0.4f, 1.0f};				// Material - Ambient Values
	float MatDif[] = {0.2f, 0.6f, 0.9f, 1.0f};				// Material - Diffuse Values
	float MatSpc[] = {0.0f, 0.0f, 0.0f, 1.0f};				// Material - Specular Values
	float MatShn[] = {1.0f};
	glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
	glMaterialfv(GL_FRONT, GL_AMBIENT, MatAmb);			// Set Material Ambience
	glMaterialfv(GL_FRONT, GL_DIFFUSE, MatDif);			// Set Material Diffuse
	glMaterialfv(GL_FRONT, GL_SPECULAR, MatSpc);		// Set Material Specular
	glMaterialfv(GL_FRONT, GL_SHININESS, MatShn);		// Set Material Shininess
	glEnable(GL_COLOR_MATERIAL);

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT1);

	if(!gModel )
	{
		buildTetrahedron(gModel,SrVector3(0.0f,0.0f,-6.0f),SrVector3(0,0,0),SrVector3(1.0f,0.0f,0.0f));
	}
}

void reshapeMessage(GLsizei w,GLsizei h)
{
	glViewport(0,0,w,h);
	float nearPlane = 1.0f;
	float farPlane	= 10000.0f;
	float fovy = 45;
	float top = nearPlane*tan(3.1415926*fovy/360.0f);
	float bott = -top;
	float right = top*(float)w/(float)h;
	float left = - right;
	if(gIsZFail)
	{
		float pinf[4][4];
		pinf[0][1] = pinf[0][2] = pinf[0][3] = pinf[1][0] =
			pinf[1][2] = pinf[1][3] = pinf[3][0] = pinf[3][1] =
			pinf[3][3] = 0;
		pinf[0][0] = 2*nearPlane / (right - left);
		pinf[1][1] = 2*nearPlane / (top - bott);
		pinf[2][0] = ( right + left ) / (right - left);
		pinf[2][1] = ( top + bott) / (top - bott);
		pinf[2][2] = pinf[2][3] = -1;
		pinf[3][2] = -2*nearPlane;
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glLoadMatrixf(&pinf[0][0]);
		glMatrixMode(GL_MODELVIEW);
	}
	else
	{
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glFrustum(left, right, bott, top, nearPlane, farPlane);
		glMatrixMode(GL_MODELVIEW);
	}

}

void mouseMotionMessage(int x, int y)
{

	if( gMoveScene )
	{
		float lenX = x - gLastMouseX;
		float lenY = y - gLastMouseY;

		gLastMouseX = x;
		gLastMouseY = y;
		gYAxisAngle += lenX*0.05f;
		gXAxisAngle += lenY*0.05f;

		glutPostRedisplay();

	}
}

void mouseClickMessage(int button, int state, int x, int y)
{
	if( state == GLUT_DOWN )
	{
		gLastMouseX = x;
		gLastMouseY = y;
		gMoveScene	= true;
	}
	else if( state == GLUT_UP )
	{
		gMoveScene = false;
	}
}

void specialKeyMessage(int key, int x, int y)
{
	switch(key)
	{
	case GLUT_KEY_UP:
		gZoom += 0.1f;
		glutPostRedisplay();
		break;
	case GLUT_KEY_DOWN:
		gZoom -= 0.1f;
		glutPostRedisplay();
		break;
	case GLUT_KEY_LEFT:
		gHorizon -= 0.1f;
		glutPostRedisplay();
		break;
	case GLUT_KEY_RIGHT:
		gHorizon += 0.1f;
		glutPostRedisplay();
		break;
	}
}

void keyMessage(unsigned char key, int x, int y)
{
	switch(key)
	{
	case 's':
	case 'S':
		gStopTimer = !gStopTimer;
		if( !gStopTimer )
		{
			glutTimerFunc(200,timerMessage,0);
		}
		break;
	case 'f':
	case 'F':
		gDisplaySide = !gDisplaySide;
		glutPostRedisplay();
		break;
	case 'c':
	case 'C':
		gIsZFail = !gIsZFail;
		if( gIsZFail )
			printf("Current algorithm: Z-Fail\n");
		else
			printf("Current algorithm: Z-Pass\n");
		glutPostRedisplay();
		break;

	}
}

int main(int argc,char ** argv)
{
	glutInit(&argc,argv);
	glutInitDisplayMode(GLUT_SINGLE|GLUT_RGB);
	glutInitWindowSize(400,400);

	glutCreateWindow("Robust-Z-Fail");

	init();
	outputInfo();
	glutReshapeFunc(reshapeMessage);
	glutDisplayFunc(renderMessage);
	glutTimerFunc(200,timerMessage,0);
	glutMouseFunc(mouseClickMessage);
	glutMotionFunc(mouseMotionMessage);
	glutSpecialFunc(specialKeyMessage);
	glutKeyboardFunc(keyMessage);

	glutMainLoop();
	return(0);
}

4. 阴影贴图

4.1. 原理

2015-3-19 13-34-43

图32. 阴影图和阴影效果图

阴影贴图算法(Shadow Mapping)的基本思想最早由Williams[25](1978)中提出的。如图32所示,算法的基本过程如下所示:

1. 把光源当作摄像机,渲染整个场景,把深度缓冲区的深度信息保存进纹理中,称为阴影贴图纹理或者深度图纹理;
2. 使用常规的摄像机渲染整个场景,对于每个可视的片断,设它的视点坐标是\left( {x,y,z} \right)

2.1. 把坐标 变换到光源的裁剪坐标\left( {{x_1},{y_1},{z_1}} \right)(即把光源当作摄像机渲染场景的情况)
2.2. 比较{z_1}和深度图纹理\left( {{x_1},{y_1}} \right)上的z值,如果{z_1} \le z,则该像素点不在阴影内;否则,在阴影内。

阴影贴图算法,基本包括两个过程:(1)以光源的视角观察,得到深度图纹理;(2)常规的摄像机视角观察,通过对比深度图的深度值,判断是否处在阴影中。用一个实际的例子来演示这个过程,第1个阶段,如图33所示,得到场景的深度图:

2015-3-19 13-34-55

图33. 第1个阶段,从光源处观察场景,得到深度图纹理

2015-3-19 13-35-06

图34. 第2阶段,以常规摄像机视角观察场景,并渲染阴影

第2个阶段,通过对比深度值,判断哪些像素需要渲染出阴影的效果,如图34所示。

那么问题出来了,第2.1步,如何将坐标由\left( {x,y,z} \right)变换为基于光源的裁剪坐标\left( {{x_1},{y_1},{z_1}} \right)呢?如图35红色箭头所示,就是把摄像机下可视片断的视点坐标\left( {x,y,z} \right)变换到光源下的裁剪坐标。

这个过程相当于分为3个子过程:

(1)把摄像机下的视点坐标变换为世界空间下的世界坐标;
(2)把世界坐标变换为光源的视点坐标;

(3)把光源的视点坐标变换为光源的裁剪坐标。

2015-3-19 13-35-19

图35. 摄像机下的视点坐标到光源的裁剪坐标的变换

M{v_{camera}}表示对象坐标向摄像机的视点坐标的变换,M{v_{light}}表示对象坐标向光源的视点坐标的变换,M{p_{light}}表示由光源的视点坐标向裁剪坐标的变换,M{v_{camera}},M{v_{light}}是仿射变换,M{p_{light}}是透视变换,透视变换是不可逆的。令V表示摄像机坐标下的视点坐标,把它变换为光源下的裁剪坐标V'可以表示为:

V' = V \cdot M = V \cdot (Mv_{camera}^{ - 1} \cdot M{v_{light}} \cdot M{p_{light}})(9)

裁剪坐标规格化后的x,y,z的坐标范围在[-1, 1]之间,而纹理坐标的范围要求是[0, 1],所以还需要把[-1, 1]变换到[0, 1]的范围。只需要把[-1, 1]缩放一半,加上 0.5即可,即最后再乘以一个矩阵B。

V' = (s,t,r,q) = V \cdot M = V \cdot (Mv_{camera}^{ - 1} \cdot M{v_{light}} \cdot M{p_{light}} \cdot B)(10)

其中,矩阵B如下所示

    \[B = \left[ {\begin{array}{*{20}{c}}{0.5}&0&0&0\\0&{0.5}&0&0\\0&0&{0.5}&0\\{0.5}&{0.5}&{0.5}&1\end{array}} \right]\]

4.2.实现

4.2.1. 保存深度图纹理

如何把第1个阶段渲染整个场景生成的深度图保存进纹理中呢?

把光源当作是摄像机,渲染整个场景,得到整个场景的深度信息,把深度缓冲区中的数据复制到纹理空间,与指定的纹理对象绑定。基本过程如下所示:

createDepthTexture()
1.    创建深度图纹理对象
       glGenTextures(1, &gShadowTex);
       glBindTexture(GL_TEXTURE_2D, gShadowTex);
       glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, gShadowWidth, gShadowHeight, 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);
2.    设置光源的投影矩阵
       glMatrixMode(GL_PROJECTION);
       glLoadMatrixf(gLightPM);
3.    设置光源的模型视图矩阵
       glMatrixMode(GL_MODELVIEW);
       glLoadMatrixf(gLightVM);
4.    设置光源的视口信息
       glViewport(0, 0, gShadowWidth, gShadowHeight);
5.    清除颜色缓冲区和深度缓冲区的值
       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
6.    设置颜色缓冲区只可读
       glColorMask(0, 0, 0, 0);
7.    渲染场景
       drawScene()
8.    绑定深度图纹理对象,将深度缓冲区中的信息复制进纹理空间中
       glBindTexture(GL_TEXTURE_2D, gShadowTex);
       glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, gShadowWidth, gShadowHeight);
9.    设置颜色缓冲区可写,并清除深度缓冲区
       glColorMask(1, 1, 1, 1);

       glClear(GL_DEPTH_BUFFER_BIT)

参见OpenGL的文档[13]解释,glCopyTexSubImage2D()可以将深度缓冲区中的深度信息复制进纹理空间中,但需要OpenGL 1.4及以后的版本才会支持。如果纹理对象指定的参数是GL_DEPTH_COMPONENT,那么OpenGL会自动匹配对应深度缓冲区的位数。至此,深度图纹理创建完成。

4.2.2. 坐标变换

这里考虑如何把摄像机下可视片断的视点坐标 变换到光源下的裁剪坐标。

矩阵的求逆运算较复杂,恰好OpenGL中提供的glTexGen*()方法帮我们解决了这个问题,参照OpenGL的文档[13]对glTexGen*()的解释。如果参数是GL_EYE_PLANE,则纹理坐标的生成函数是:

g = {p_1}'' \times {x_e} + {p_2}'' \times {y_e} + {p_3}'' \times {z_e} + {p_4}'' \times {w_e}(11)

其中,\left( {{p_1}'',{p_2}'',{p_3}'',{p_4}''} \right) = \left( {{p_1},{p_2},{p_3},{p_4}} \right){M^{ - 1}}{M^{ - 1}}表示模型视图矩阵的逆矩阵,等式(10)可以变型为:

V' = (s,t,r,q) = V \cdot M = \left( {V \cdot Mv_{camera}^{ - 1}} \right) \cdot \left( {M{v_{light}} \cdot M{p_{light}} \cdot B} \right)(12)

所以,现在只需要计算后面三个矩阵的乘积即可,基本过程如下所示:

       coordTransforms()
1.    设置环境光,并渲染场景
       drawScene();
2.    计算矩阵
3.    设置纹理坐标的生成参数
       column1[0] = M[0];column1[1] = M[4];column1[2] = M[8];column1[3] = M[12];
       glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
       glTexGenfv(GL_S, GL_EYE_PLANE, column1);
       glEnable(GL_TEXTURE_GEN_S);
 
       column2[0] = M[1];column2[1] = M[5];column2[2] = M[9];column2[3] = M[13];
       glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
       glTexGenfv(GL_T, GL_EYE_PLANE, column2);
       glEnable(GL_TEXTURE_GEN_T);
 
       column3[0] = M[2];column3[1] = M[6];column3[2] = M[10];column3[3] = M[14];
       glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
       glTexGenfv(GL_R, GL_EYE_PLANE, column3);
       glEnable(GL_TEXTURE_GEN_R);
 
       column4[0] = M[3];column4[1] = M[7];column4[2] = M[11];column4[3] = M[15];
       glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
       glTexGenfv(GL_Q, GL_EYE_PLANE, column4);

       glEnable(GL_TEXTURE_GEN_Q);

       生成的\left( {s,t,r,q} \right)是四元坐标,与齐次坐标类似,实际表示的坐标为\left( {s/q,t/q,r/q} \right)r/q就是片断与光源的z距离同,也是被缩放到[0, 1]范围内的数值。通过对比r/q与深度图纹理\left( {s/q,t/q} \right)上的z值,来判断是否是阴影。如果z \prec r/q,说明该像素处在阴影内;否则,不在阴影内。

4.2.3. 深度对比

如何将视点坐标变换后得到的深度值与深度图纹理中的值进行对比呢?

可以采用OpenGL的函数glTexParameter*()和alpha测试,参照OpenGL的文档[13]对glTexParameter*()的解释。glTexParameter*()函数对应的GL_TEXTURE_COMPARE_MODE,GL_TEXTURE_COMPARE_FUNC,GL_DEPTH_TEXTURE_MODE三个参数在 OpenGL 1.4及以后版本中才支持,而GL_TEXTURE_COMPARE_FUNC参数允许的其它对比方式,有GL_LESS,GL_GREATER,GL_EQUAL,GL_NOTEQUAL,GL_ALWAYS和GL_NEVER,在OpenGL 1.5及以后版本中才支持。

基本过程如下所示:

compareDepth()
1.    设置普通光源,包括环境光、漫反射光、镜面反射光;
2.    开启纹理对象,并绑定深度图纹理
       glEnable(GL_TEXTURE_2D);
       glBindTexture(GL_TEXTURE_2D, gShadowTex);
3.    启动与深度图的对比
       glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
4.    当变换后的深度值小于等于深度图中对应纹理数值时,给该像素的alpha通道设置为1,否则,设置为0
       glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
       glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_ALPHA);
5.    启动Alpha测试,当像素的Alpha通道的值大于等于0.99时,通过测试
       glEnable(GL_ALPHA_TEST);
       glAlphaFunc(GL_GEQUAL, 0.99f);
6.    渲染场景

       drawScene();

4.2.4. 总结

在实现中,需要判断是否支持OpenGL的扩展,包括:

(1)GL_ARB_depth_texture提供了以纹理格式来保存深度缓冲区的方法,例如,如果有一个24位的深度缓冲区,一个DEPTH_COMPONENT纹理就有一个24位通道来存储从深度缓冲区读取的值,可以使用glCopyTex[Sub]Image2D完成读取操作;

(2)GL_ARB_shadow提供了纹理对比的功能;

(3)GL_ARB_texture_non_power_of_two支持非二次幂的纹理。

完整的阴影贴图的算法流程包括前面介绍的三个阶段:

1.  保存深度图纹理;
2.  坐标变换;

3.  深度对比;

包括对场景的三次渲染,第1次渲染,为了生成深度图纹理;第2次渲染,为了渲染阴影效果,并进行摄像机下的视点坐标到光源裁剪坐标的变换;第3次渲染,判断哪些像素在阴影中,哪些不在阴影中,并进行相应的渲染。

实现代码如下所示:

/************************************************************************		
\link	www.twinklingstar.cn
\author Twinkling Star
\date	2014/03/17
****************************************************************************/
#include <stdlib.h>
#include <gl/glew.h>
#include <gl/glut.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>

#pragma comment(lib,"glew32.lib")
//Camera & light positions

float cameraPosition[]={-2.5f, 3.5f,-2.5f};
float lightPosition[]={2.0f,3.0f,-2.0f};

//shadow width and height
int		gShadowWidth;
int		gShadowHeight;

//window size
int		gWinWidth;
int		gWinHeight;

//Textures
GLuint gShadowTex;

float gCameraMV[16];
float gLightPM[16];
float gLightVM[16];

float white[]={1.0f,1.0f,1.0f,1.0f};
float black[]={0.0f,0.0f,0.0f,0.0f};

bool isExtensionSupported( char* szTargetExtension )
{
	const unsigned char *pszExtensions = NULL;
	const unsigned char *pszStart;
	unsigned char *pszWhere, *pszTerminator;

	// Extension names should not have spaces
	pszWhere = (unsigned char *) strchr( szTargetExtension, ' ' );
	if( pszWhere || *szTargetExtension == '\0' )
		return false;

	// Get Extensions String
	pszExtensions = glGetString( GL_EXTENSIONS );

	// Search The Extensions String For An Exact Copy
	pszStart = pszExtensions;
	for(;;)
	{
		pszWhere = (unsigned char *) strstr( (const char *) pszStart, szTargetExtension );
		if( !pszWhere )
			break;
		pszTerminator = pszWhere + strlen( szTargetExtension );
		if( pszWhere == pszStart || *( pszWhere - 1 ) == ' ' )
			if( *pszTerminator == ' ' || *pszTerminator == '\0' )
				return true;
		pszStart = pszTerminator;
	}
	return false;
}

void drawScene(float angle)
{
	//Display lists for objects
	static GLuint spheresList=0, torusList=0, baseList=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();
	}

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

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

bool init()
{
	if( !isExtensionSupported("GL_ARB_depth_texture") ||
		!isExtensionSupported("GL_ARB_shadow") ||
		!isExtensionSupported("GL_ARB_texture_non_power_of_two"))
	{
		printf("Don't support!\n");
		return false;
	}

	//Shading states
	glShadeModel(GL_SMOOTH);

	//Clearing color of the color buffer.
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	//Clearing depth of the depth buffer
	glClearDepth(1.0f);
	//Depth test.
	glDepthFunc(GL_LEQUAL);
	glEnable(GL_DEPTH_TEST);

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

	//Create the shadow map texture
	glGenTextures(1, &gShadowTex);
	glBindTexture(GL_TEXTURE_2D, gShadowTex);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, gShadowWidth, gShadowHeight, 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);

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

	//Calculate & save matrices
	//Load identity modelview
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glPushMatrix();
	glLoadIdentity();
	gluLookAt(cameraPosition[0], cameraPosition[1], cameraPosition[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
	glGetFloatv(GL_MODELVIEW_MATRIX, gCameraMV);

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

	glLoadIdentity();
	gluLookAt(lightPosition[0], lightPosition[1], lightPosition[2],0.0f, 0.0f, 0.0f,0.0f, 1.0f, 0.0f);
	glGetFloatv(GL_MODELVIEW_MATRIX, gLightVM);
	glPopMatrix();

	return true;
}

void multiplyMatrix4(float* matrix1,float* matrix2,float* result)
{
	result[0] = matrix1[0]*matrix2[0] + matrix1[1]*matrix2[4] + matrix1[2]*matrix2[8] + matrix1[3]*matrix2[12];
	result[1] = matrix1[0]*matrix2[1] + matrix1[1]*matrix2[5] + matrix1[2]*matrix2[9] + matrix1[3]*matrix2[13];
	result[2] = matrix1[0]*matrix2[2] + matrix1[1]*matrix2[6] + matrix1[2]*matrix2[10] + matrix1[3]*matrix2[14];
	result[3] = matrix1[0]*matrix2[3] + matrix1[1]*matrix2[7] + matrix1[2]*matrix2[11] + matrix1[3]*matrix2[15];

	result[4] = matrix1[4]*matrix2[0] + matrix1[5]*matrix2[4] + matrix1[6]*matrix2[8] + matrix1[7]*matrix2[12];
	result[5] = matrix1[4]*matrix2[1] + matrix1[5]*matrix2[5] + matrix1[6]*matrix2[9] + matrix1[7]*matrix2[13];
	result[6] = matrix1[4]*matrix2[2] + matrix1[5]*matrix2[6] + matrix1[6]*matrix2[10] + matrix1[7]*matrix2[14];
	result[7] = matrix1[4]*matrix2[3] + matrix1[5]*matrix2[7] + matrix1[6]*matrix2[11] + matrix1[7]*matrix2[15];

	result[8] = matrix1[8]*matrix2[0] + matrix1[9]*matrix2[4] + matrix1[10]*matrix2[8] + matrix1[11]*matrix2[12];
	result[9] = matrix1[8]*matrix2[1] + matrix1[9]*matrix2[5] + matrix1[10]*matrix2[9] + matrix1[11]*matrix2[13];
	result[10] = matrix1[8]*matrix2[2] + matrix1[9]*matrix2[6] + matrix1[10]*matrix2[10] + matrix1[11]*matrix2[14];
	result[11] = matrix1[8]*matrix2[3] + matrix1[9]*matrix2[7] + matrix1[10]*matrix2[11] + matrix1[11]*matrix2[15];

	result[12] = matrix1[12]*matrix2[0] + matrix1[13]*matrix2[4] + matrix1[14]*matrix2[8] + matrix1[15]*matrix2[12];
	result[13] = matrix1[12]*matrix2[1] + matrix1[13]*matrix2[5] + matrix1[14]*matrix2[9] + matrix1[15]*matrix2[13];
	result[14] = matrix1[12]*matrix2[2] + matrix1[13]*matrix2[6] + matrix1[14]*matrix2[10] + matrix1[15]*matrix2[14];
	result[15] = matrix1[12]*matrix2[3] + matrix1[13]*matrix2[7] + matrix1[14]*matrix2[11] + matrix1[15]*matrix2[15];
}

void createDepthTexture()
{

	glMatrixMode(GL_PROJECTION);
	glLoadMatrixf(gLightPM);
	glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(gLightVM);
	//Use viewport the same size as the shadow map
	glViewport(0, 0, gShadowWidth, gShadowHeight);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	//Draw back faces into the shadow map
	glCullFace(GL_FRONT);
	//Disable color writes
	glColorMask(0, 0, 0, 0);
	//Draw the scene
	drawScene(0);
	//Read the depth buffer into the shadow map texture
	glBindTexture(GL_TEXTURE_2D, gShadowTex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, gShadowWidth, gShadowHeight);
	//restore states
	glCullFace(GL_BACK);
	glColorMask(1, 1, 1, 1);
	glClear(GL_DEPTH_BUFFER_BIT);
}

void coordTransforms()
{
	//用环境光渲染渲染场景
	float lightColor[]={0.2f,0.2f,0.2f,1.0f};
	glLightfv(GL_LIGHT1, GL_POSITION, lightPosition);
	glLightfv(GL_LIGHT1, GL_AMBIENT, lightColor);
	glLightfv(GL_LIGHT1, GL_DIFFUSE, black);
	glLightfv(GL_LIGHT1, GL_SPECULAR, black);
	glEnable(GL_LIGHT1);
	glEnable(GL_LIGHTING);
	drawScene(0);

	float bias[]={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};
	float texM[16], tpM[16];
	multiplyMatrix4(gLightPM,bias,tpM);
	multiplyMatrix4(gLightVM,tpM,texM);
	//设置纹理坐标的生成参数
	float column1[4],column2[4],column3[4],column4[4];
	column1[0] = texM[0];
	column1[1] = texM[4];
	column1[2] = texM[8];
	column1[3] = texM[12];
	glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glTexGenfv(GL_S, GL_EYE_PLANE, column1);
	glEnable(GL_TEXTURE_GEN_S);

	column2[0] = texM[1];
	column2[1] = texM[5];
	column2[2] = texM[9];
	column2[3] = texM[13];
	glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glTexGenfv(GL_T, GL_EYE_PLANE, column2);
	glEnable(GL_TEXTURE_GEN_T);

	column3[0] = texM[2];
	column3[1] = texM[6];
	column3[2] = texM[10];
	column3[3] = texM[14];
	glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glTexGenfv(GL_R, GL_EYE_PLANE, column3);
	glEnable(GL_TEXTURE_GEN_R);

	column4[0] = texM[3];
	column4[1] = texM[7];
	column4[2] = texM[11];
	column4[3] = texM[15];
	glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glTexGenfv(GL_Q, GL_EYE_PLANE, column4);
	glEnable(GL_TEXTURE_GEN_Q);
}

void setupCamera()
{
	glViewport(0, 0, gWinWidth, gWinHeight);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(45.0f, (float)gWinWidth/gWinHeight, 1.0f, 100.0f);
	glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(gCameraMV);
}

void redraw(void)
{
	//创建深度图纹理对象
	createDepthTexture();

	//设置常规摄像机的视角
	setupCamera();

	//坐标变换
	coordTransforms();

	//设置光源
	glLightfv(GL_LIGHT1, GL_DIFFUSE, white);
	glLightfv(GL_LIGHT1, GL_SPECULAR, white);

	//开启深度图纹理
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, gShadowTex);

	//启动与深度图的对比
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);

	//只有深度小于深度图中对应纹理的数值,会给alpha通道赋值为1;否则,赋值为0
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_ALPHA);

	//启动Alpha测试,大于1的像素会通过测试
	glEnable(GL_ALPHA_TEST);
	glAlphaFunc(GL_GEQUAL, 0.99f);

	drawScene(0);

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

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

}

void reshape(GLsizei w,GLsizei h)
{
	gShadowWidth	=	gWinWidth	=	w;
	gShadowHeight	=	gWinHeight  =	h;

	glBindTexture(GL_TEXTURE_2D, gShadowTex);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, gShadowWidth, gShadowHeight, 0,GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL);
}

int main(int argc,char ** argv)
{
	glutInit(&argc,argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glewInit();
	glutInitWindowSize(400,400);

	glutCreateWindow("Shadow Mapping");

	assert(init());

	glutReshapeFunc(reshape);
	glutDisplayFunc(redraw);

	glutMainLoop();
	return(0);
}

4.3. 存在的问题

阴影贴图算法一个非常大的好处是:创建深度图的时候与所要渲染的图元成线性关系,访问时间几乎是常数。NVIDA甚至于在有的GPU硬件中加入了用于阴影的渲染,例如GeForce3 Ti[26],NVIDIA阴影缓冲技术包括创建一个贴图,使对象在画面中被照亮,然后将此贴图存储在阴影缓冲区中,通过位于GeForce3 Ti纹理引擎的特殊阴影缓冲的访问(与纹理类似),达到加速渲染的目的。但是阴影贴图也存在一些问题。

比如自阴影,所谓的自阴影(Self-Shadow)就是平面阴影中说的Z-Fighting问题,也称为阴影粉刺(Shadow Acne),如图36所示。

2015-3-19 13-35-33

图36. 自阴影

阴影的计算都是以深度缓冲区的精度为基础的,有限位的小数计算,一定存在精度问题,使用等号来检测非阴影区的点就会产生不精确的结果,就会产生如图32所示的自阴影的效果。针对场景中所有的对象是闭合对象的情况下,解决这个问题可以采用的方法是:在第1次渲染,保存阴影贴图纹理时,裁剪掉正面(Front Faces),这里保存的是背面(Back Faces),就能得到图37所示的正确的渲染结果。

2015-3-19 13-35-41

图37. 正确的阴影效果

也有人通过把深度值进行一定的位移,但是深度值位移不当,容易产生错误的效果,如图38所示,称这种现象为Peter Panning或者Peter Pan问题。OpenGL提供了函数glPolygonOffset()来设置偏移量,遗憾的是它的值需要手动设置。如何设置这些值,可以参考Schüler[28](2005), Schüler[29](2009),Lengyel[30](2000)。

2015-3-19 13-35-48

图38. Peter Panning问题

此外,阴影贴图还存在锯齿性问题,如何渲染复杂场景,以光源的视角如何能得到整个场景的深度图等。对这些问题更加深入的介绍和讨论,推荐Eisemann[1](2011)和Akenine-Möller[4](2008)。

参考

[1]    Elmar Eisemann, et al. Real-time shadows. CRC Press, 2011.

[2]    Han-Wei Shen. “Shadow Algorithms”. Website <http://web.cse.ohio-state.edu/~whmin/courses/cse5542-2013-spring/19-shadow.pdf>.

[3]    “Shadows”. website <http://excelsior.cs.ucsb.edu/courses/cs180/discussion/Shadows.pdf>.

[4]    Tomas Akenine-Möller, Eric Haines, and Naty Hoffman. Real-time rendering. CRC Press, 2008.

[5]    Jim Blinn. “Me and my (fake) shadow.” IEEE Computer Graphics and Applications, vol.8, no.1, pp.82-86, 1988.

[6]    Paul S. Heckbert, and Michael Herf. Simulating soft shadows with graphics hardware. No. CMU-CS-97-104. CARNEGIE-MELLON UNIV PITTSBURGH PA DEPT OF COMPUTER SCIENCE, 1997.

[7]    Michael Herf, and Paul S. Heckbert. “Fast soft shadows.” In Visual Proceedings, SIGGRAPH 96. 1996.

[8]    “OpenGL Programming Guide”. wesite<http://www.glprogramming.com/red/appendixf.html>.

[9]    “Soft shadows”. code website <http://users.polytech.unice.fr/~buffa/cours/synthese_image/www.sgi.com/software/opengl/advanced97/programs/softshadow2.c>.

[10]Franklin C Crow. “Shadow algorithms for computer graphics.” ACM siggraph computer graphics, vol.11, no. 2, ACM, 1977.

[11]“This presentation covers advanced hardware accelerated lighting techniques in OpenGL.” website <https://developer.nvidia.com/content/gdc-2001-more-advanced-hardware-rendering-techniques>.

[12]Tim Heidmann. “Real shadows, real time.” Iris Universe 18, pp.28-31, 1991.

[13]“OpenGL 2.1 Reference Pages.” website < https://www.opengl.org/sdk/docs/man2/>.

[14]Harlen Costa Batagelo, and Ilaim Costa Jr. “Real-time shadow generation using bsp trees and stencil buffers.” Computer Graphics and Image Processing, 1999. Proceedings. XII Brazilian Symposium on. IEEE, 1999.

[15]Paul Diefenbach. “Multi-pass pipeline rendering: Interaction and realism through hardware provisions.” Diss. PhD thesis, University of Pennsylvania, 1996.

[16]Mark J Kilgard. “Robust stencil shadow volumes.” CEDEC Presentation, Tokyo 115, pp.116-119, 2001.

[17]Samuel Hornus, et al. “ZP+: correct Z-pass stencil shadows.” Proceedings of the 2005 symposium on Interactive 3D graphics and games. ACM, 2005.

[18]Bill Bilodeau, and Mike Songy. “Real time shadows.” Creative Labs Sponsored Game Developer Conference, Creative Labs Inc. 1999.

[19]Cass Everitt, and Mark J. Kilgard. “Practical and robust stenciled shadow volumes for hardware-accelerated rendering.” arXiv preprint cs/0301002 , 2003.

[20]“ARB_depth_clamp.” website <https://www.opengl.org/registry/specs/ARB/depth_clamp.txt>.

[21]NVIDIA. “Robust Stencial Shadow Volumes.” webiste <https://developer.nvidia.com/sites/default/files/akamai/gamedev/docs/StencilShadows_CEDEC_E.pdf>.

[22]Wikipedia. “Fill rate.” website < http://en.wikipedia.org/wiki/Fillrate>.

[23]Christian Steiner. “Shadow volumes in complex scenes.” Master’s thesis, Institute of Computer Graphics and Algorithms, Vienna University of Technology, Favoritenstrasse 9-11/186, A-1040 Vienna, Austria, 2006.

[24]paulsprojects . website <http://www.paulsprojects.net/>.

[25]Lance Williams. “Casting curved shadows on curved surfaces.”ACM Siggraph Computer Graphics. vol.12, no.3, ACM, 1978.

[26]NVIDIA. “Shadow Buffer Technical Brief.” website <http://www.nvidia.cn/object/LO_20010929_4012.html>.

[27]Microsoft. “Common Techniques to Improve Shadow Depth Maps”. website <https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx>.

[28]Christian Schüler. “Eliminating surface acne with gradient shadow mapping.” ShaderX4: Advanced Rendering Techniques (edited by W. Engel), pp.289-297, 2005.

[29]Christian Schüle. “Multisampling extension for gradient shadow maps.” Engel W. ShaderX5, pp.207-218, 2006.

[30]Eric Lengyel. “Tweaking a vertex’s projected depth value.” Game Programming Gems, Charles River Media, M. DeLoura, Ed, pp.361-365, 2000.

[31]J. Charles Hourcade, and A. Nicolas. “Algorithms for antialiased cast shadows.” Computers & Graphics, vol.9, no.3 pp.259-265, 1985.

spacer

One comment on “实时阴影技术

  1. kbridge

    我是OpenGL小白
    本文对我相当有帮助
    作者的认真让人佩服

Leave a reply to kbridge Cancel reply