NeHe OpenGL教程第三十课,DancingWind翻译

Nehe SDK

第30课

碰撞检测:

这是一课激动的教程,你也许等待它多时了。你将学会碰撞剪裁,物理模拟太多的东西,慢慢期待吧。

碰撞检测和物理模拟(作者:Dimitrios Christopoulos ([email protected]))

碰撞检测

这是一个我遇到的最困难的题目,因为它没有一个简单的解决办法.对于每一个程序都有一种检测碰撞的方法.当然这里有一种蛮力,它适用于各种不同的应用,当它非常的费时.
我们将讲述一种算法,它非常的快,简单并易于扩展.下面我们来看看这个算法包含的内容:

1) 碰撞检测
  • 移动的球-平面
  • 移动的球-圆柱
  • 移动的球-移动的球
2) 基于物理的建模
  • 碰撞表示
  • 应用重力加速度
3) 特殊效果
  • 爆炸的表示,利用互交叉的公告板形式
  • 声音使用Windows声音库
4) 关于代码
  • 代码被分为以下5个部分
Lesson30.cpp   : 主程序代码l
Image.cpp, Image.h : 加载图像
Tmatrix.cpp, Tmatrix.h : 矩阵
Tray.cpp, Tray.h : 射线
Tvector.cpp, Tvector.h : 向量



1) 碰撞检测

我们使用射线来完成相关的算法,它的定义为:

射线上的点 = 射线的原点+ t * 射线的方向

t 用来描述它距离原点的位置,它的范围是[0, 无限远).

现在我们可以使用射线来计算它和平面以及圆柱的交点了。

射线和平面的碰撞检测:

平面被描述为:

Xn dot X = d

Xn 是平面的法线.
X 是平面上的一个点.
d 是平面到原点的距离.

现在我们得到射线和平面的两个方程:

PointOnRay = Raystart + t * Raydirection
Xn dot X = d


如果他们相交,则上诉方程组有解,如下所示:

Xn dot PointOnRay = d

(Xn dot Raystart) + t * (Xn dot Raydirection) = d

解得 t:

t = (d - Xn dot Raystart) / (Xn dot Raydirection)

t代表原点到与平面相交点的参数,把t带回原方程我们会得到与平面的碰撞点.如果Xn*Raydirection=0。则说明它与平面平行,则将不产生碰撞。如果t为负值,则说明交点在射线的相反方向,也不会产生碰撞

//判断是否和平面相交,是则返回1,否则返回0
int TestIntersionPlane(const Plane& plane,const TVector& position,const TVector& direction, double& lamda, TVector& pNormal)
{

double DotProduct=direction.dot(plane._Normal);
double l2;

//判断是否平行于平面
if ((DotProduct<ZERO)&&(DotProduct>-ZERO))
return 0;

l2=(plane._Normal.dot(plane._Position-position))/DotProduct;

if (l2<-ZERO)
return 0;

pNormal=plane._Normal;
lamda=l2;
return 1;
}

射线-圆柱的碰撞检测

计算射线和圆柱方程组得解。
int TestIntersionCylinder(const Cylinder& cylinder,const TVector& position,const TVector& direction, double& lamda, TVector& pNormal,TVector& newposition)
球-球之间的碰撞检测

球被表示为中心和它的半径,决定两个球是否相交就是求出它们之间的距离是否小于它们的直径。

在处理两个移动的球是否相交时,有一个bug就是,当它们的移动速度太快,回出现它们相交,但在相邻的两步检测不出它们是否相交的情况,如下图所示:


图 1


有一个替代的办法就是细分相邻的时间片断,如果在这之间发生了碰撞,则确定有效。我们把这个细分时间段设置为3,代码如下:
//判断球和球是否相交,是则返回1,否则返回0
int FindBallCol(TVector& point, double& TimePoint, double Time2, int& BallNr1, int& BallNr2)
{
TVector RelativeV;
TRay rays;
double MyTime=0.0, Add=Time2/150.0, Timedummy=10000, Timedummy2=-1;
TVector posi;

//判断球和球是否相交
for (int i=0;i<NrOfBalls-1;i++)
{
for (int j=i+1;j<NrOfBalls;j++)
{
RelativeV=ArrayVel[i]-ArrayVel[j];
rays=TRay(OldPos[i],TVector::unit(RelativeV));
MyTime=0.0;

if ( (rays.dist(OldPos[j])) > 40) continue;

while (MyTime<Time2)
{
MyTime+=Add;
posi=OldPos[i]+RelativeV*MyTime;
if (posi.dist(OldPos[j])<=40) {
point=posi;
if (Timedummy>(MyTime-Add)) Timedummy=MyTime-Add;
BallNr1=i;
BallNr2=j;
break;
}

}
}

}

if (Timedummy!=10000) { TimePoint=Timedummy;
return 1;
}

return 0;
}

怎样应用我们的知识

现在我们已经可以决定射线和平面/圆柱的交点了,如下图所示:


图 2a                                         图 2b

当我们找到了碰撞位置后,下一步我们需要知道它是否发生在当前这一步中.如果距离碰撞点的位置小于这一步球体运动的间隔,则碰撞发生.我们使用如下的方程计算运动到碰撞时所需的时间:
Tc= Dsc*T / Dst
接着我们知道碰撞点位置,如下面公式所示:
Collision point= Start + Velocity*Tc

2) 基于物理的模拟


碰撞反应

为了计算对于一个静止物体的碰撞,我们需要知道以下信息:碰撞点,碰撞法线,碰撞时间.

它是基于以下物理规律的,碰撞的入射角等于反射角.如下图所示:


图 3


R 为反射方向
I 为入射方向
N 为法线方向

反射方向有以下公式计算 :

R= 2*(-I dot N)*N + I
rt2=ArrayVel[BallNr].mag();						// 返回速度向量的模
ArrayVel[BallNr].unit();						// 归一化速度向量

// 计算反射向量
ArrayVel[BallNr]=TVector::unit( (normal*(2*normal.dot(-ArrayVel[BallNr]))) + ArrayVel[BallNr] );
ArrayVel[BallNr]=ArrayVel[BallNr]*rt2;					
球体之间的碰撞

由于它很复杂,我们用下图来说明这个原理.


图 4



U1和U2为速度向量,我们用X_Axis表示两个球中心连线的轴,U1X和U2X为U1和U2在这个轴上的分量。U1y和U2y为垂直于X_Axis轴的分量。M1和M2为两个球体的分量。V1和V2为碰撞后的速度,V1x,V1y,V2x,V2y为他们的分量。

在我们的例子里,所有球的质量都相等,解得方程为,在垂直轴上的速度不变,在X_Axis轴上互相交换速度。代码如下:

TVector pb1,pb2,xaxis,U1x,U1y,U2x,U2y,V1x,V1y,V2x,V2y;
double a,b;
pb1=OldPos[BallColNr1]+ArrayVel[BallColNr1]*BallTime;			// 球1的位置
pb2=OldPos[BallColNr2]+ArrayVel[BallColNr2]*BallTime;			// 球2的位置
xaxis=(pb2-pb1).unit();							// X-Axis轴
a=xaxis.dot(ArrayVel[BallColNr1]);					// X_Axis投影系数
U1x=xaxis*a;								// 计算在X_Axis轴上的速度
U1y=ArrayVel[BallColNr1]-U1x; // 计算在垂直轴上的速度
xaxis=(pb1-pb2).unit();							
b=xaxis.dot(ArrayVel[BallColNr2]);					
U2x=xaxis*b;								
U2y=ArrayVel[BallColNr2]-U2x;
V1x=(U1x+U2x-(U1x-U2x))*0.5;						// 计算新的速度
V2x=(U1x+U2x-(U2x-U1x))*0.5;
V1y=U1y;
V2y=U2y;
for (j=0;j<NrOfBalls;j++)						// 更新所有球的位置
ArrayPos[j]=OldPos[j]+ArrayVel[j]*BallTime;
ArrayVel[BallColNr1]=V1x+V1y;						// 设置新的速度
ArrayVel[BallColNr2]=V2x+V2y;						
万有引力的模拟

我们使用欧拉方程来模拟万有引力,如下所示:
Velocity_New = Velovity_Old + Acceleration*TimeStep
Position_New = Position_Old + Velocity_New*TimeStep

在每次模拟中,我们用上面公式计算的速度取代旧的速度

3) 特殊效果

爆炸

最好的表示爆炸效果的就是使用两个互相垂直的平面,并使用alpha混合在窗口中显示它们。接着让alpha变为0,设定爆炸效果不可见。代码如下所示:
// 渲染/混合爆炸效果
glEnable(GL_BLEND);							// 使用混合
glDepthMask(GL_FALSE);							// 禁用深度缓存
glBindTexture(GL_TEXTURE_2D, texture[1]);				// 设置纹理
for(i=0; i<20; i++)							// 渲染20个爆炸效果
{
	if(ExplosionArray[i]._Alpha>=0)
	{
		glPushMatrix();
		ExplosionArray[i]._Alpha-=0.01f;			// 设置alpha
		ExplosionArray[i]._Scale+=0.03f;			// 设置缩放
		// 设置颜色
		glColor4f(1,1,0,ExplosionArray[i]._Alpha);		
		glScalef(ExplosionArray[i]._Scale,ExplosionArray[i]._Scale,ExplosionArray[i]._Scale);
		// 设置位置
		glTranslatef((float)ExplosionArray[i]._Position.X()/ExplosionArray[i]._Scale,
			(float)ExplosionArray[i]._Position.Y()/ExplosionArray[i]._Scale,
			(float)ExplosionArray[i]._Position.Z()/ExplosionArray[i]._Scale);
		glCallList(dlist);					// 调用显示列表绘制爆炸效果
		glPopMatrix();
	}
}
声音

在Windows下我们简单的调用PlaySound()函数播放声音。

4) 代码的流程

如果你成功的读完了理论部分,在你开始运行程序并播放声音以前。我们将用伪代码向你介绍一些整个流程,以便你能成功的看懂代码。
While (Timestep!=0)
{
	对每一个球
	{
		计算最近的与平面碰撞的位置;
		计算最近的与圆柱碰撞的位置;
		如果碰撞发生,则保存并替换最近的碰撞点;
	}
	检测各个球之间的碰撞;
	如果碰撞发生,则保存并替换最近的碰撞点;

	If (碰撞发生)
	{
		移动所有的球道碰撞点的时间;
		(We already have computed the point, normal and collision time.)
		计算碰撞后的效果;
		Timestep-=CollisonTime;
	}
	else
		移动所有的球体一步
}

下面是对上面伪代码的实现:

//模拟函数,计算碰撞检测和物理模拟
void idle()
{
double rt,rt2,rt4,lamda=10000;
TVector norm,uveloc;
TVector normal,point,time;
double RestTime,BallTime;
TVector Pos2;
int BallNr=0,dummy=0,BallColNr1,BallColNr2;
TVector Nc;

//如果没有锁定到球上,旋转摄像机
if (!hook_toball1)
{
camera_rotation+=0.1f;
if (camera_rotation>360)
camera_rotation=0;
}

RestTime=Time;
lamda=1000;

//计算重力加速度
for (int j=0;j<NrOfBalls;j++)
ArrayVel[j]+=accel*RestTime;

//如果在一步的模拟时间内(如果来不及计算,则跳过几步)
while (RestTime>ZERO)
{
lamda=10000;

//对于每个球,找到它们最近的碰撞点
for (int i=0;i<NrOfBalls;i++)
{
//计算新的位置和移动的距离
OldPos[i]=ArrayPos[i];
TVector::unit(ArrayVel[i],uveloc);
ArrayPos[i]=ArrayPos[i]+ArrayVel[i]*RestTime;
rt2=OldPos[i].dist(ArrayPos[i]);

//测试是否和墙面碰撞
if (TestIntersionPlane(pl1,OldPos[i],uveloc,rt,norm))
{
//计算碰撞的时间
rt4=rt*RestTime/rt2;

//如果小于当前保存的碰撞时间,则更新它
if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=OldPos[i]+uveloc*rt;
lamda=rt4;
BallNr=i;
}
}
}

if (TestIntersionPlane(pl2,OldPos[i],uveloc,rt,norm))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=OldPos[i]+uveloc*rt;
lamda=rt4;
BallNr=i;
dummy=1;
}
}

}

if (TestIntersionPlane(pl3,OldPos[i],uveloc,rt,norm))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=OldPos[i]+uveloc*rt;
lamda=rt4;
BallNr=i;
}
}
}

if (TestIntersionPlane(pl4,OldPos[i],uveloc,rt,norm))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=OldPos[i]+uveloc*rt;
lamda=rt4;
BallNr=i;
}
}
}

if (TestIntersionPlane(pl5,OldPos[i],uveloc,rt,norm))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=OldPos[i]+uveloc*rt;
lamda=rt4;
BallNr=i;
}
}
}

//测试是否与三个圆柱相碰
if (TestIntersionCylinder(cyl1,OldPos[i],uveloc,rt,norm,Nc))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=Nc;
lamda=rt4;
BallNr=i;
}
}

}
if (TestIntersionCylinder(cyl2,OldPos[i],uveloc,rt,norm,Nc))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=Nc;
lamda=rt4;
BallNr=i;
}
}

}
if (TestIntersionCylinder(cyl3,OldPos[i],uveloc,rt,norm,Nc))
{
rt4=rt*RestTime/rt2;

if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=Nc;
lamda=rt4;
BallNr=i;
}
}

}
}


//计算每个球之间的碰撞,如果碰撞时间小于与上面的碰撞,则替换它们
if (FindBallCol(Pos2,BallTime,RestTime,BallColNr1,BallColNr2))
{
if (sounds)
PlaySound("Data/Explode.wav",NULL,SND_FILENAME|SND_ASYNC);

if ( (lamda==10000) || (lamda>BallTime) )
{
RestTime=RestTime-BallTime;

TVector pb1,pb2,xaxis,U1x,U1y,U2x,U2y,V1x,V1y,V2x,V2y;
double a,b;

pb1=OldPos[BallColNr1]+ArrayVel[BallColNr1]*BallTime;
pb2=OldPos[BallColNr2]+ArrayVel[BallColNr2]*BallTime;
xaxis=(pb2-pb1).unit();

a=xaxis.dot(ArrayVel[BallColNr1]);
U1x=xaxis*a;
U1y=ArrayVel[BallColNr1]-U1x;

xaxis=(pb1-pb2).unit();
b=xaxis.dot(ArrayVel[BallColNr2]);
U2x=xaxis*b;
U2y=ArrayVel[BallColNr2]-U2x;

V1x=(U1x+U2x-(U1x-U2x))*0.5;
V2x=(U1x+U2x-(U2x-U1x))*0.5;
V1y=U1y;
V2y=U2y;

for (j=0;j<NrOfBalls;j++)
ArrayPos[j]=OldPos[j]+ArrayVel[j]*BallTime;

ArrayVel[BallColNr1]=V1x+V1y;
ArrayVel[BallColNr2]=V2x+V2y;

//Update explosion array
for(j=0;j<20;j++)
{
if (ExplosionArray[j]._Alpha<=0)
{
ExplosionArray[j]._Alpha=1;
ExplosionArray[j]._Position=ArrayPos[BallColNr1];
ExplosionArray[j]._Scale=1;
break;
}
}

continue;
}
}

//最后的测试,替换下次碰撞的时间,并更新爆炸效果的数组
if (lamda!=10000)
{
RestTime-=lamda;

for (j=0;j<NrOfBalls;j++)
ArrayPos[j]=OldPos[j]+ArrayVel[j]*lamda;

rt2=ArrayVel[BallNr].mag();
ArrayVel[BallNr].unit();
ArrayVel[BallNr]=TVector::unit( (normal*(2*normal.dot(-ArrayVel[BallNr]))) + ArrayVel[BallNr] );
ArrayVel[BallNr]=ArrayVel[BallNr]*rt2;

for(j=0;j<20;j++)
{
if (ExplosionArray[j]._Alpha<=0)
{
ExplosionArray[j]._Alpha=1;
ExplosionArray[j]._Position=point;
ExplosionArray[j]._Scale=1;
break;
}
}
}
else
RestTime=0;

}

}


你可以从源代码得到全部的信息,我尽了最大的努力来解释每一行代码,一旦碰撞的原理知道了,代码是非常简单的.

就像我开头所说的,碰撞检测这个题目是非常难得,你已经学会了很多新的知识,并能够用它创建出非常棒的演示.但在这个课题,你认友很多需要学习,既然你已经开始了,其它的原理和模型就非常容易了.

版权与使用声明:
我是个对学习和生活充满激情的普通男孩,在网络上我以DancingWind为昵称,我的联系方式是[email protected],如果你有任何问题,都可以联系我。

引子
网络是一个共享的资源,但我在自己的学习生涯中浪费大量的时间去搜索可用的资料,在现实生活中花费了大量的金钱和时间在书店中寻找资料,于是我给自己起了个昵称DancingWind,其意义是想风一样从各个知识的站点中吸取成长的养料。在飘荡了多年之后,我决定把自己收集的资料整理为一个统一的资源库。

版权声明
所有DancingWind发表的内容,大多都来自共享的资源,所以我没有资格把它们据为己有,或声称自己为这些资源作出了一点贡献。故任何人都可以复制,修改,重新发表,甚至以自己的名义发表,我都不会追究,但你在做以上事情的时候必须保证内容的完整性,给后来的人一个完整的教程。最后,任何人不能以这些资料的任何部分,谋取任何形式的报酬。

发展计划
在国外,很多资料都是很多人花费几年的时间慢慢积累起来的。如果任何人有兴趣与别人共享你的知识,我很欢迎你与我联系,但你必须同意我上面的声明。

感谢
感谢我的母亲一直以来对我的支持和在生活上的照顾。
感谢我深爱的女友田芹,一直以来默默的在精神上和生活中对我的支持,她甚至把买衣服的钱都用来给我买书了,她真的是我见过的最好的女孩,希望我能带给她幸福。

源码 RAR格式

< 第29课 第31课 >