拾取(Picking)
这篇文章简单讨论一下拾取,所谓拾取就是判断一个三维场景中哪个对象被点击。要实现拾取可以有许多方法,每一个都有自己优点和缺点。
颜色键值
第一种方法是使用颜色键值。基本上,场景中的每个三维对象都会被分配一个唯一的颜色。当渲染场景时,会执行一个pass输出每个对象的颜色。这将创建一个场景包含了一系列对象本身的形状。接着对应鼠标(或其他东西,如目标矩形)位置的像素被放置到2D空间。像素所包含的颜色将组成场景中的物体,这个颜色告知我们哪个对象被点击。
这个方法的优点是拾取是“像素完美(pixel perfect)”的,也就是说,它可以以像素的精度告知对象的哪个位置被点击,这比其他方法精确得多,如果对象已经绘制,那么使用这个方法代价也是很小的。另外,如果在shader中要修改几何数据,如凹凸映射,我们也能实现拾取。
但是,这种方法也有一些缺点。首先,如果一个物体变得小于一个像素,我们将无法将它与靠近它的物体区别开来,因为我们只能达到一个像素的精度。其次,如果我们试图判断对象是否包含在一个区域中(如拖动一个盒子穿过屏幕),我们必须使用更多的采样点,这在某些计算机上可能是非常耗资源的。最后,我们只能获取最上层的对象,因为我们只能在一个像素上放置一种颜色。这意味着,如果一个对象是在另一个背后,我们就无法实现拾取,因为我们只保存了前面一个对象的颜色。这使得多个对象的选择很难,因为我们只能获得前面的对象而不能获得其后的对象。
我写了一个例子展示了这种技术,它可在Ziggyware.com上的以下网址http://www.ziggyware.com/readarticle.php?article_id=203下载。
多边形拾取
第二种方法是多边形拾取。思路是创建一个ray(射线),它从鼠标在三维空间中的位置出发,并延伸至无穷远(理论上而已,现实中我们无法做到这点,因为无法处理无穷大的数。但我们可以取到足够大直到不影响结果)。然后,我们检查ray和场景中的多边形。如果多边形之一与ray相交,我们就知道对象是在鼠标光标之下,那么我们将其添加到一个集合并移至下一个。一旦我们检查了所有对象,我们查看集合并选择最接近那个对象。
使用Viewport.Unproject()方法能容易地获得光标在3维空间中的位置,这个方法需要为鼠标的屏幕坐标使用一个Vector3变量,其中X和Y是鼠标在二维空间的X和Y位置, Z是离开“屏幕”的距离。因此,要获得我们需要的两个点,我们将创建一个Vector3代表屏幕上的光标,另外一个代表离屏幕无穷远的光标。对应分别为(X Pos, Y Pos, 0)和(X Pos, Y Pos, 1)。然而,由于我们不能使用无穷大的数,后者的Z坐标应该接近于0.99。
多边形拾取比颜色键值拾取更精确(译者注:原文是Polygon picking is more accute than color keys because it is not limted by the number of pixels in the image,但这样的话与前面所说的“像素拾取比其他方法精确得多”相矛盾),因为它不受图像中像素数量的限制。同样,只要在shader中不修改几何数据,它是非常完美的。它还可以实现叠在一起的多个对象的选取。
多边形拾取的缺点是它代价不菲。检查每个对象中的每个多边形与射线的相交很费时间。
在Ziggyware也有一个示例http://www.ziggyware.com/readarticle.php?article_id=103演示了如何实现多边形拾取。
包围盒(Bounding Volumes)
它是目前最有效的拾取方法,包围盒用几何体的近似形状代替几何体本身。最常用的形状是盒和球。检查射线与盒或球相交要远远快于检查数以千计的多边形。通常在一个模型中使用一组包围盒,这样可以使拾取更精确但同时仍然保留拾取的效率。
唯一的缺点是这种方法并不能精确到像素。但是,通常你无需让拾取精确到像素,往往检查盒,球或胶囊就足够了。
XNA提供了一些包围盒,并且已经包含了一些方法判断是否相交。这些包围盒有BoundingBox,BoundingSphere,BoundingFrustum,平面和Ray(射线)。所有对象的Intersects()方法能告诉你是否相交。
本文的最后一部分包含了实现包围盒拾取的普遍方法和适用我们的游戏引擎的特定方法。
混合实现
如果在特定场合你实在需要达到像素精度,可以考虑采取混合的办法。混合方法能让你在有些时候达到像素精度,而其余时候用近似形状。例如,如果您编写一个与一群敌人战斗的游戏,玩家可能不会注意即使他们射出的子弹偏离敌人头部几个像素远仍然可以击中敌人。接着,例如,因为玩家使用狙击视角拉近镜头,那么这种情况下就应切换到使用颜色键值拾取。
这样做更加可行,因为你只取样在视野中的像素点,你只需在玩家扣动板机时这样做。。您不必担心像素拾取的局限性,因为您正在观察的对象通常比一个像素大得多,也不必担心对象后面是什么。(除非您希望能够打穿某些材质,在这种情况下,您必须使用多边形拾取)。您也不用担心绘制颜色键的开销,因为你只需要绘制视野中的对象,因为视野通常很小所以这些对象不会很多,其余的对象可能是某些视野纹理。
优化
即使使用包围盒近似,如果游戏中有太多对象,因为要执行相交检查,游戏仍会被大大拖慢。幸运的是,我们可以减少检查对象的数量。如果使用一些优化技术,我们可以无需检查场景中的大多数对象。最常见的和最有用的方法是四叉树,八叉树,BSP,遮挡筛选和视景体裁剪。实现包围盒近似实现包围盒近似是非常简单的。首先,我们找到两点构成拾取线:对应从鼠标在3D空间的屏幕位置指向无穷远处。然后我们创建一条射线使用第一个点和下一个点的方向。然后,我们使用它来检查每个模型的BoundingBoxes。该BoundingBoxes可以通过合并模型中的每个ModelMesh计算获得,或在素材管道中遍历每个顶点,检查它们是否低于最低点或高于最高点,如果是,那么将最低点或最高点移至的顶点位置。这两个点就作为BoundingBox的顶点。这一步最好在素材管道中实现,以避免重复进行这一步。
下面的代码说明如何处理包围盒的集合:
public BoundingBox Pick (int X, int Y, out float Dist, List<BoundingBox> Check, Matrix View, Matrix Projection) { Vector3 nearSource = new Vector3((float)X, (float)Y, 0); Vector3 farSource = new Vector3((float)X, (float)Y, .99f); Vector3 nearPoint = Engine.GraphicsDevice.Viewport.Unproject(nearSource, Projection, View, Matrix.CreateTranslation(0, 0, 0)); Vector3 farPoint = Engine.GraphicsDevice.Viewport.Unproject(farSource, Projection, View, Matrix.CreateTranslation(0, 0, 0)); Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray ray = new Ray(nearPoint, direction); BoundingBox closest = null; float? closestDist = float.MaxValue; foreach (BoundingBox box in check) { float? dist; ray.Intersects(ref box, out dist); if (dist != null) if (dist.Value < closestDist) { closestDist = dist; closest = box; } } Dist = (closestDist != null ? closestDist.Value : 0); return closest; }
以下代码显示在游戏引擎中的实现:
public Component Pick (int X, int Y, out float Dist) { Vector3 nearSource = new Vector3((float)X, (float)Y, 0); Vector3 farSource = new Vector3((float)X, (float)Y, .99f); Vector3 nearPoint = Engine.GraphicsDevice.Viewport.Unproject(nearSource, Camera.Projection, Camera.View, Matrix.CreateTranslation(0, 0, 0)); Vector3 farPoint = Engine.GraphicsDevice.Viewport.Unproject(farSource, Camera.Projection, Camera.View, Matrix.CreateTranslation(0, 0, 0)); Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray ray = new Ray(nearPoint, direction); Component closest = null; float? closestDist = float.MaxValue; foreach (GameScreen screen in Engine.GameScreens) foreach (Component component in screen.Components) if (component is I3DComponent) { BoundingBox bbox = ((I3DComponent)component).BoundingBox; float? dist; ray.Intersects(ref bbox, out dist); if (dist != null && component != Camera && component != Grid) if (dist.Value < closestDist) { closestDist = dist; closest = component; } } Dist = (closestDist != null ? closestDist.Value : 0); return closest; }
注意:组件返回的BoundingBoxes应在组件位置上加上一个偏移量,即返回的BoundingBox的顶角的位置是原始顶角位置加上组件的位置。
发布时间:2009/3/24 上午8:06:16 阅读次数:7773