一种3D游戏碰撞检测解决方案
碰撞检测在3D游戏中至关重要,好的碰撞检测要求人物在场景中可以平滑移动,遇到一定高度内的台阶可以自动上去,而过高的台阶则把人挡住,遇到斜率较小的斜坡可以上去,斜率过大则把人挡住,在各种前进方向被挡住的情况下都要尽可能地让人物沿合理的方向滑动而不是被迫停下。在满足这些要求的同时还要做到足够精确和稳定,防止人物在特殊情况下穿墙而掉出场景。
碰撞检测做得好了是应该的,不易被人注意到,因为这符合我们日常生活中的常识。做得差了却很容易让人发现,人物经常被卡住不能前进或者人物穿越了障碍。所以大部分人都觉得写碰撞检测代码是件吃力不讨好的事情,算法复杂、容易出bug、不容易出彩。下面还是回到正题,看看我们该如何解决这个难题。
早期3D游戏的碰撞检测多数基于格子或者BSP树,基于格子的系统实现简单但精度不够,不属于严格意义的3D碰撞检测。基于BSP树的碰撞检测一度十分流行,算法基本已经成熟定型,但它的固有缺点却使它不太适合现在的游戏。BSP树需要很长的预处理时间不适合加载时计算,BSP划分经常会产生原多边形数三到四倍的多边形,考虑到不用保存法线、颜色、uv等信息也要增加将近一倍的资源容量,在一个大的游戏中将模型资源的容量从200M增加到400M相信是大部分人都不愿接受的。目前对于任意复杂三角形集合(mesh)的碰撞检测多数基于BVTree(bounding volume tree),具体可以是aabb tree,obb tree或者K-dop tree,这也是当今各种物理引擎和碰撞检测引擎流行的做法。
上面是碰撞检测按数据结构不同的分类,按检测方式又可以分为离散点的碰撞检测和连续碰撞检测(CCD continuous collision detection)。
离散点的碰撞检测是指定某一时刻T的两个静态碰撞体,看它们之间是否交迭,如果没有交迭则返回它们最近点的距离,如果交迭则返回交迭深度,交迭方向等。连续碰撞检测则是分别指定在T1、T2两个时刻两个碰撞体的位置,看它们在由T1运动到T2时刻的过程中是否发生碰撞,如果碰撞则返回第一碰撞点的位置和法线。连续碰撞检测是最为自然的碰撞检测,可以大大方便碰撞响应逻辑的编写,可以很容易避免物体发生交迭或者穿越。离散点的碰撞检测则没有那么友好,当检测到碰撞时两个物体已经发生了交迭,如果其中有三角形网格对象那么已经有许多三角形发生了交迭,如何将两个交迭的对象分开并按合理的方式运动是一个挑战。虽然连续碰撞检测是最自然的方式,但它的实现非常复杂,运算开销也很大,所以目前大部分成熟的物理引擎和碰撞检测引擎还是采用了基于离散点的碰撞检测,为了避免物体交迭过深或者彼此穿越,它们都要采用比较小的模拟步长。
由于碰撞检测引擎的复杂性和对效率的高要求,我们应该尽量使用目前成熟的完整引擎,而不是自己去开发。经过评估,我决定采用Opcode碰撞检测引擎来做游戏中人物和场景的碰撞检测。Opcode的主要功能是用aabb tree管理复杂三角形集合来和射线、球体,立方体,另一个三角形集合等进行离散点上的碰撞检测,如果检测到交迭则返回所有发生交迭的三角形。Opcode的特点是高度的内存使用优化和极好的效率,ODE物理引擎底层就采用它来做复杂三角形mesh的碰撞检测,Opcode的作者也是NovodeX(PhysX前身)物理引擎的核心开发人员,据说NovodeX采用了Opcode的一个更优化版本。由此可见Opcode的成熟与效率。
确定了要使用的引擎,下面要讨论的算法就和具体引擎无关了,适合于任何离散点的碰撞检测引擎。我们用AABB包围盒来代表场景中的人物,看看如何实现文章开头所提出的效果。
首先解释一下检测地面的方式,沿人物包围盒的四条竖边向下投四条射线,射线的终点略低于人物的脚底(也就是说射线的长度是有限的),如果与场景发生碰撞并且碰撞平面的斜率小于某一值则取得碰撞点的高度,否则视为这条射线没有检测到地面。将所有射线检测到的地面高度最大值作为最后的地面高度,如果四条射线都没有检测到地面则认为人物悬空。
vD = 当前帧人物位移
p0 = 人物包围盒中心当前位置
bOnGroundP1; // 人物是否站在地面
bOnGroundP3; // 人物是否站在地面
bOnGround; // 人物是否站在地面
p1 = p0 + vD
在p1位置检测地面
if( 检测到地面 )
{
将包围盒下放到地面得到位置p2
bOnGroundP1 = true;
}
else
{
p2 = p1;
bOnGroundP1 = false;
}
测试p2点的包围盒是否与场景交迭
if( 交迭 )
{
取得所有交迭三角形的法线,将它们相加后取平均值,得到法线normal
将法线与向上的向量叉乘得到切线方向tangent
// 计算人物沿切线滑动后的位置,注意这里用p0做计算。
// 如果要使滑动更平滑可以把p0向法线方向推出一些
// p3 = p0 + normal * 0.1f + vD.Dot(tangent);
p3 = p0 + vD.Dot(tangent);
在p3位置检测地面
if( 检测到地面 )
{
将包围盒下放到地面得到位置p4
bOnGroundP3 = true;
}
Else
{
p4 = p3;
bOnGroundP3 = false;
}
测试p4点的包围盒是否与场景交迭
if( 交迭 )
{
测试p1点的包围盒是否与场景交迭
if( 交迭 )
{
// 无法得到合理的移动位置,人物位置保持不变
// 等待下一帧玩家调整前进方向再做测试
将p0作为人物的新位置
bOnGround = true;
return;
}
else
{
将p1作为人物的新位置
bOnGround = bOnGroundP1;
return;
}
}
Else
{
将p4作为人物的新位置
bOnGround = bOnGroundP3;
return;
}
}
else
{
将p2作为人物的新位置
bOnGround = bOnGroundP1;
return;
}
上面的算法基本达到了文章开头所提到的效果,在某些复杂情况下人物移动还有些不流畅,但没有发现人物有穿越障碍物的现象。在大部分情况下人物的新坐标都会由p2点返回,最坏情况是人物被卡住返回p0点。在P4 3.0G的机器上可以支持120个人物在最坏情况下保持30帧的游戏帧数。
一.概述
Opcode是一个开源的碰撞检测库,其最大的特点是占用内存少(与其他的碰撞检测库相比而言),对一个完全二叉树中的每个三角形仅用20字节,所以它的文档中说是“Memory-optimized bounding-volume hierarchies”。
在碰撞检测中,利用检测树(Bounding-volume hierachies)结构进行碰撞排除是最普遍的方法(如RAPID,SOLID,QuickCD,PQP都是如此),Opcode也不例外,Opcode中默认的包围盒是AABB。就是说若用Opcode进行一个Mesh的碰撞检测,则它会为此Mesh建立AABB树。
Opcode可以对以下情况作出检测:
- Mesh-mesh
- Sphere-mesh
- Ray-mesh
- AABB-mesh
- OBB-mesh
- Planes-mesh
二.检测过程
调用Opcode的代码做检测时主要包含三部分:
1.为Mesh生成碰撞树
这涉及到Opcode中的Model类和OPCODECREATE类,Opcode建立碰撞树分两步:首先建立一般的树(这个树最后被遗弃),然后利用一般的树建立一个优化的树。
Opcode可以建立的四种树(关于这几种树在后面的内存优化部分会讲到):
- Normal trees (2*N-1 nodes, full size)
- No-leaf trees (N-1 nodes, full size)
- Quantized trees (2*N-1 nodes, half size)
- Quantized no-leaf trees (N-1 nodes, half size)
从代码中可以看出在建立一般树的时候,Opcode允许指定对节点进行分组时的依据,如利用Geometry的AABB盒所在的最长轴(SPLIT_LARGEST_AXIS)进行分组。
2.为上面所列的查询建立相应的Collider和Qurey方式
对一类检测建立一个Collider,如Mesh-mesh查询用AABBTreeCollider,Sphere-mesh查询用SphereCollider等。
Qurey方式包括First Contact和All Contacts两种,另外还可以指定是否采用temporal coherence(时间连续性)。
3.建立cache
关于具体的调用函数在其文档中已经给出,不再罗列。
三.内存优化
如上所述,Opcode进行内存优化是其显著的特点,所以在这里专门提出。其优化方法如下:
1.合理组织内存,每个节点所占用的空间较少。如RAPID为树中的每个节点保存三个指针(本身、左子、右子),而通过特意组织内存后,完全可以只要两个指针(本身、左子),而另右子指针总在左子+1的位置上。
2.去掉叶子节点。在一个完全树中共有2*N-1个节点,其中N个为叶子节点,每个叶子节点包含一个Primitive(三角形)和其相应的BV(包围盒),通常情况下检测到叶子节点时先测试其BV碰撞情况再测试Primitive。Opcode去掉了N个叶子节点,这样大大节省了内存空间,它将原叶子节点的Primitive指针信息放到其父节点里,也剔除了原叶子节点的BV,检测时直接检测Primitive和BV的相交情况。虽然Primitive和BV的相交测试较BV-BV复杂,但其少了BV-BV这一步所以性能上并没有什么损失。
这样优化后的内存占用量为原来的50%。
这样去掉叶子节点后进行检测的伪代码如下:
A and B are two bounding volumes
A0 and A1 are both children of A
B0 and B1 are both children of B
test(A, B)
{
if(A overlaps B)
{
// A0B0
if(A0 is leaf)
if(B0 is leaf) test(leaf, leaf) => Primitive-Primitive test
else test(leaf, box) => Primitive-BV test
else
if(B0 is leaf) test(box, leaf) => Primitive-BV test
else test(box, box) => BV-BV test
// Repeat for A0B1
…
// Repeat for A1B0
…
// Repeat for A1B1
…
}
}
3.Quantize。每个float数被Quatize为16位的整型数,在runtime阶段再进行Dequantize,进行Quantize的方法在Deering, Michael, Geometry Compression, Computer Graphics (SIGGRAPH’95 Proceedings), pp. 13-20, August 1995.中。
进行Quatize和Dequantize会导致精度的下降,以至检测不够精确,因此Opcode在Dequatize时采取措施保证Dequatized的包围盒大于等于原本的包围盒,以防止检测的丢失。这样无疑会带来速度的下降,好在在采用2中优化方法后,包围盒的数量减少了一半,所以这个影响并不像对一个标准树的影响那样大。另外可以利用SAT-lite、Class III axes的方法做粗糙检测(coarser BV-BV tests),使其快速收敛到primitive-BV、primitive-primitive检测,在Opcode的代码中提供了采取此方法的选项。
这样的优化又在2的基础上将其内存占用量减为50%。
四.总结
总体上来说,Opcode在降低内存占用量的同时,良好的保持了快速性。
值得注意的是,没有一种碰撞检测库可以在任何情况下都达到最快速度,通常都是针对某一种或某些情况速度快一些,即使同样的场景,若物体动作情况不同,碰撞面多少的不同都可能会导致各个库的速度变化较大。
在Opcode的文档中是这样叙述的:
As far as speed is concerned, it is a lot more difficult to provide accurate comparisons and fair figures. Often, simply changing the relative orientation of two models makes one library faster or slower than another. In many cases we have found our implementation to outperform RAPID (up to 5 times faster), mainly when objects were deeply overlapping. This is very encouraging since intersection testing using AABB trees usually takes 50% longer than using OBB trees in cases where there is a lot of overlap among the models. In some other cases RAPID was faster, mainly in close-proximity scenarii where OBBs are really more appropriate than AABBs (and indeed RAPID sometimes uses a single OBB-OBB test in those situations, whereas we need a lot more). Nonetheless it’s interesting to note our version was often not significantly slower than RAPID in those cases.
另外值得一提的是,北卡(UNC)的GAMMA Group做了大量的碰撞检测工作,开发了多个碰撞检测库,包括用GPU的Occlusion Query来做精细检测。
Ps:这篇笔记是一年前写出的,今天偶然翻出,稍微整理放到这里,欢迎点评。
内附:
OgreOpcode的源码和动态链接库
学习例子的netbean工程
有时候仅是想用碰撞检测而不是物理引擎,比如在网络游戏中,物理引擎将带来很多困难和效率损失。
本来想自己写一个碰撞检测库,无奈水平不够,写了3天都未成雏形。
只好去找个巨人的肩膀来站站,于是找到了opcode和其ogre封装OgreOpcode。
但opcode资料少得可怜,OgreOpcode多年未更新,代码居然还有语法错误,更别说编译其工程了。
花了半天来修修补补,终于编译OgreOpcode成功。
接下来最大的问题是学习OgreOpcode的使用,自带的例子错误一大堆。
对于小白来说,没有一个可以运行的例子简直是灾难,更甚于没有文档。
放狗搜放毒搜,终于找到一份靠谱的,运行成功。
先说下代码流程:
(1)创建场景物体
(2)CollisionManager* mCollisionMgr = new CollisionManager(mSceneMgr);
创建碰撞管理器,在碰撞检测开始前需要用来做一些初始化工作
(3)mCollisionContext = mCollisionMgr->createContext("testContext");
创建CollisionContext,这是在碰撞检测时使用的
(4)mCollisionMgr->addCollClass("charactor");
mCollisionMgr->addCollType("charactor", "charactor", COLLTYPE_EXACT);
添加碰撞类型,可当作给碰撞物体分组。
(5)EntityCollisionShape *shape = CollisionManager::getSingletonPtr()->createEntityCollisionShape(entName);
shape->load(mSceneMgr->getEntity(entName));
CollisionObject* pCollisionObject = mCollisionContext->createObject(entName);
pCollisionObject->setCollClass(strCollClass.data());
pCollisionObject->setShape(shape);
mCollisionContext->addObject(pCollisionObject);
给场景物体添加碰撞外壳并添加到碰撞计算中
(6)在每一帧中调用
pCollContext->visualize(true, true, true, true, true, true);
用于显示线框显示的碰撞外壳,类似于bullet中的debug draw。
pCollContext->collide(timeDelta);
执行碰撞检测。
(7)if (pCollContext->getAttachedObject("robot")->hasCollisions()){...}
在每一帧中查询robot有没有发生碰撞,如果发生了执行相应的逻辑。
posted on 2012-01-16 23:43 Hibernate4 阅读(642) 评论(0) 编辑 收藏 举报