问题
你想检测两个模型是否发生碰撞。如果在场景中有许多模型,你将无法进行一个精确的逐三角形的碰撞检测。你想使用一个快速检测方法并在以后进行一个更好的碰撞检测。
解决方案
当进行碰撞检测时,你会发现常常需要在速度和精确度之间进行衡量。在大多数情况中,你会进行组合检测。在第一轮检测中,使用快速检测遍历所有对象,之后对快速检测中可能发生碰撞的物体进行精确检测。
本教程展示了两个方法处理两个模型间的碰撞检测。快速检测方法是找到模型的全局包围球,并通过调用一个包围球的Intersect 方法检测是否相交。
你可以改进这个方法以增加精确度。由几个ModelMeshes组成的模型存储了模型不同部分的几何信息。每个ModelMeshes都能生成各自的包围球,所以你可以检测第一个模型和第二个模型的每个包围球。显然,这样做提高了精度但计算量也随之加大。
工作原理
快速检测
这个方法使用整个模型的包围球。如果这两个模型的包围球相交,则它们可能发生了碰撞。
在某些情况下,这个方法会导致糟糕的结果,例如在滑板游戏中,当你想检测两个玩家的滑板的碰撞时,滑板的包围球相对于滑板本身大得多,滑板的体积不到包围球体积的百分之一,这个方法将会对全局包围球中的所有物体(可能包含其他滑板!)进行检测,如图4-13所示。
但是,这个方法在进行第一次检测时用得还是很多的,因为它的速度足够快。
图4-13 每个滑板都在另外一个滑板的包围球中
要实现这个快速检测,首先从加载模型和生成它们的的全局包围球开始,这一步已在教程4-5中介绍过了。包围球将存储在模型的Tag属性中:
myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank",Content);
这个包围球将包含模型的中心位置(中心位置没必要一定在模型的(0,0,0)点上)和半径。
当绘制两个模型时,你需要在每个模型上施加一个不同的世界矩阵让它可以移动(和/或缩放/旋转)至正确的位置。当你移动(和/或缩放/旋转)一个模型时,还需要移动包围球的中心位置(和/或缩放半径)。你可以使用XNAUtils 文件中的TransformBoundingSphere方法做到这一点,这个方法以包围盒和世界矩阵为参数,更加世界矩阵移动中心位置和缩放半径,并返回结果包围球。
要检测两个模型间的碰撞,你需要通过世界矩阵变换每个包围球并检查变换过的包围球是否相交。
private bool CoarseCheck(Model model1, Matrix world1, Model model2, Matrix world2) { BoundingSphere origSphere1 = (BoundingSphere)model1.Tag; BoundingSphere sphere1= XNAUtils.TransformBoundingSphere(origSphere1, world1); BoundingSphere origSphere2= (BoundingSphere)model2.Tag; BoundingSphere sphere2 = XNAUtils.TransformBoundingSphere(origSphere2,world2); bool collision = sphere1.Intersects(sphere2); return collision; }
因为模型的Tag属性可以包含任何东西(这是因为Object类在C#中是根类),你需要通过(BoundingSphere)进行类型转换。如果变换过的包围球发生碰撞,这个方法会返回true。
更加精确,但有点慢
每个模型是由多个成员组成的,几何数据是存储在模型的Meshes集合中的。每个 ModelMesh都能生成自己的包围球。这些小的包围球的总体积比模型的全局包围球小得多,如图4-14所示。图的右边你可以看到模型每个部分的独立包围球。
图4-14 全局包围球和多个小的包围球
在图的左边,两个包围球相交,这样第一次检测会显示两个模型发生碰撞。而在图的右边,没有包围球发生碰撞,所以这次检测会正确地显示两者并没有发生碰撞。第二个方法通常会返回更好的结果;但是,第一种检测只需检查一次,而第二种需要检查(members inside Model1)*(members inside Model2)次。
下面的方法是根据第二种情况的碰撞检测:
private bool FinerCheck(Model model1, Matrix world1, Model model2, Matrix world2) { if (CoarseCheck(model1, world1,model2, world2) == false) return false; bool collision = false; Matrix[] model1Transforms= new Matrix[model1.Bones.Count]; Matrix[] model2Transforms = new Matrix[model2.Bones.Count]; model1.CopyAbsoluteBoneTransformsTo(model1Transforms); model2.CopyAbsoluteBoneTransformsTo(model2Transforms); foreach (ModelMesh mesh1 in model1.Meshes) { BoundingSphere origSphere1 = mesh1.BoundingSphere; Matrix trans1 = model1Transforms[mesh1.ParentBone.Index] * world1; BoundingSpheretransSphere1 = XNAUtils.TransformBoundingSphere(origSphere1, trans1); foreach (ModelMesh mesh2 in model2.Meshes) { BoundingSphere origSphere2 = mesh2.BoundingSphere; Matrix trans2 = model2Transforms[mesh2.ParentBone.Index] * world2; BoundingSphere transSphere2 = XNAUtils.TransformBoundingSphere(origSphere2, trans2); if (transSphere1.Intersects(transSphere2)) collision = true; } } return collision; }
你首先进行快速检测,如果这次检测返回false,那就没必要进行精确检测。
如果快速检测显示可能发生碰撞,那么还有进行更加精确的检测。你首先将collision变量设置为false,除非检测到碰撞那么这个变量将保持为false。因为你需要将每个小包围球移动到正确的位置,所以需要绝对Bone矩阵。
要对第一个模型的每个ModelMesh进行这个检测,你需要将包围球移动到绝对位置。要实现这一步,你需要考虑模型包围球的位置和模型在3D世界中的位置。
然后你遍历第二个模型的所有部分并将这些包围球变换到绝对位置。对于第一个模型的每个包围球,你检查是否与第二个模型的任意一个包围球是否发生碰撞,如果是,则将collision变量设置为true。
最后,collision变量显示是否至少有一个模型1的包围球与模型2的包围球发生碰撞并返回这个变量。
优化
因为对模型1的每个部分来说,模型2的所有包围球都是以相同的方式进行变换的,显然这个方法可以进行优化。例如,你可以首先变换模型1和模型2的包围球一次,在方法的开头,将它们存储在一个数组中。之后,你只需简单地对这些变换过的包围球进行碰撞检测。
下面的代码是FinerCheck方法的优化版本:
private bool FinerCheck(Model model1, Matrix world1, Model model2, Matrix world2) { if (CoarseCheck(model1, world1, model2, world2) == false) return false; Matrix[] model1Transforms = new Matrix[model1.Bones.Count]; model1.CopyAbsoluteBoneTransformsTo(model1Transforms); BoundingSphere[] model1Spheres = new BoundingSphere[model1.Meshes.Count]; for (int i=0; i<model1.Meshes.Count; i++) { ModelMesh mesh = model1.Meshes[i]; BoundingSphere origSphere = mesh.BoundingSphere; Matrix trans = model1Transforms[mesh.ParentBone.Index]* world1; BoundingSphere transSphere = XNAUtils.TransformBoundingSphere(origSphere,trans); model1Spheres[i] = transSphere; } Matrix[] model2Transforms = new Matrix[model2.Bones.Count]; model2.CopyAbsoluteBoneTransformsTo(model2Transforms); BoundingSphere[] model2Spheres= new BoundingSphere[model2.Meshes.Count]; for (int i = 0; i < model1.Meshes.Count;i++) { ModelMesh mesh = model2.Meshes[i]; BoundingSphere origSphere = mesh.BoundingSphere; Matrix trans = model2Transforms[mesh.ParentBone.Index] * world2; BoundingSphere transSphere = XNAUtils.TransformBoundingSphere(origSphere, trans); model2Spheres[i]= transSphere; } bool collision = false; for (int i=0; i<model1Spheres.Length;i++) for (int j = 0; j < model2Spheres.Length; j++) if (model1Spheres[i].Intersects(model2Spheres[j])) return true; return collision; }
注意:CopyAbsoluteBoneTransformsTo方法只需在模型的Bone矩阵更新后被调用一次。
代码
CoarseCheck和FinerCheck方法的代码前面已经写过了。下面的Draw方法定义了两个世界矩阵将两个模型放置在不同位置。随着时间流逝,一个模型接近另一个,如果两者发生碰撞,背景会变成红色:
protected override void Draw(GameTime gameTime) { //define World matrices float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 1000.0f; Matrix worldM1= Matrix.CreateScale(0.01f) * Matrix.CreateRotationY(MathHelper.PiOver2) *Matrix.CreateTranslation(-10,0, 0); Matrix worldM2 = Matrix.CreateScale(0.01f) * Matrix.CreateRotationY(-MathHelper.PiOver2)* Matrix.CreateTranslation(10, 3, 0); worldM2 = worldM2 * Matrix.CreateTranslation(-time* 3.0f, 0, 0); //check for collision if (FinerCheck(myModel, worldM1, myModel, worldM2)) { Window.Title = "Collision!!"; device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer,Color.Red, 1, 0); } else { Window.Title = "No collision ..."; device.Clear(ClearOptions.Target| ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); } //render scene cCross.Draw(fpsCam.ViewMatrix,fpsCam.ProjectionMatrix); DrawModel(myModel, worldM1, modelTransforms); DrawModel(myModel,worldM2, modelTransforms); base.Draw(gameTime); }