处理模型——检测光标是否在模型上

问题

你想检测光标是否在模型上。

解决方案

在XNA中,获取光标在屏幕上的2D位置是简单的。屏幕上的这个点对应3D空间中的一条射线Ray,如图4-28所示。

 

1 图4-28 2D光标对应3D空间的一条射线

因此,当你想检测光标在哪个模型上,需要检测射线与模型的碰撞,所以,这个教材会用到教程4-18的代码。

很有可能射线与多个模型相交,这个教程还会教你如何获取离屏幕最近的一个模型。

工作原理

你需要创建一个3D射线并将它与模型一起传递到教程4-18创建的ModelRayCollision 方法中。

只要知道了射线上的两个点就可以创建一条射线。你将使用的两个点如图4-28所示。第一个点是射线与近裁屏平面的交点;第二个点是与远裁平面的交点。

如果知道了这两个点的3D位置,你将使用ViewProjection矩阵进行转换获取屏幕上的2D位置。但是,你转换一个Vector3结果仍是一个Vector3,在结果Vector3中,通过使用ViewProjection矩阵进行变换,X和Y分量就是2D屏幕位置,第三个坐标Z也包含有用的信息,即相机与初始点的距离,为0时表示点在近裁平面,为1时表示在远裁平面。在深度缓冲中存储的正是这个距离。所以,每个在2D屏幕上绘制的像素实际上都有3个坐标值。

你要获取的两个点共享相同的像素,相同的2D位置,即它们的X和Y坐标是相同的。因为第一个点位于近裁平面,所以它的Z坐标为0。第二点位于远裁平面,所以Z坐标为1。这两个点在屏幕空间的三个坐标,在光标的情况中如下所示:

  • (mouseX, mouseY, 0)
  • (mouseX, mouseY, 1)

下面是代码:

MouseState mouseState = Mouse.GetState();   Vector3 nearScreenPoint = new Vector3(mouseState.X, mouseState.Y, 0);  Vector3 farScreenPoint = new Vector3(mouseState.X, mouseState.Y, 1); 

如果从3D空间转换到屏幕空间,你要使用ViewProjection矩阵转换3D点。而这里你想讲这些点从屏幕空间转换到3D空间,所以使用的是ViewProjection矩阵的逆矩阵。你还需要将X和Y的光标坐标映射到[-1, 1]范围中,所以需要屏幕的像素大小的高和宽。

幸运的是,XNA提供了UnProject方法实现了这个映射和反向变换:

Vector3 near3DWorldPoint = device.Viewport.Unproject(nearScreenPoint, fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity);  Vector3 far3DWorldPoint = device.Viewport.Unproject(farScreenPoint,fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity); 

你获得的这两个点如图4-28所示!

注意:你想知道相对于3D初始位置(0,0,0)的位置,所以你将Matrix. Identity作为世界矩阵。ViewProjection矩阵可以通过将View和Projection矩阵相乘获得,这也是你将这两个矩阵作为第二第三个参数的原因。

知道了射线的两个点,就可以创建一个Ray对象:

Vector3 pointerRayDirection = far3DWorldPoint - near3DWorldPoint;   pointerRayDirection.Normalize();  Ray pointerRay = new Ray(near3DWorldPoint,pointerRayDirection); 

创建了Ray之后,你就做好了使用上一个教程中的ModelRayCollision方法检测Ray和模型间碰撞的准备:

selected = ModelRayCollision(myModel, modelWorld, pointerRay); 

添加一个Crosshair

前面的代码看起来很好,但如果你不能测试,代码再好也看不出来。所以让我们添加一个图像可以显示光标的位置,可见教程3-1学习绘制一个图像的简短介绍。首先将光标的2D位置存储在一个Vector2中:

pointerPosition = new Vector2(mouseState.X, mouseState.Y); 

在LoadContent方法中,添加一个SpriteBatch对象和一个Texture2D对象保存透明的crosshair图像:

spriteBatch = new SpriteBatch(device); crosshair = content.Load("cross");

然后在Draw方法中将这个图像绘制到屏幕:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.Deferred, SaveStateMode.SaveState);   spriteBatch.Draw(cross, mouseCoords, null, Color.White, 0, new Vector2(7, 7), 1, SpriteEffects.None, 0);   spriteBatch.End(); 

这可以让你在屏幕上看到光标。图像的中心点(7,7)位于光标位置。

检测多个对象

如果在场景中有多个对象,那么可能有多个对象会与射线发生碰撞。在大多数情况中,你只关心离相机最近的那个对象,因为这个对象才占据屏幕的像素。

要做到这点,你可以稍微调整一下ModelRayCollision方法,让它返回碰撞的距离而不是简单的true或false。类似于Intersect方法,你使用一个可空类型float?变量,这样如果没有碰撞那么返回null:

private float? ModelRayCollision(Model model, Matrix modelWorld, Ray ray)   {      Matrix []modelTransforms = new Matrix[model.Bones.Count];       model.CopyAbsoluteBoneTransformsTo(modelTransforms);             float? collisionDistance = null;       foreach (ModelMesh mesh in model.Meshes)       {          Matrix absTransform = modelTransforms[mesh.ParentBone.Index]*modelWorld;           Triangle[] meshTriangles = (Triangle[])mesh.Tag;                     foreach (Triangle tri in meshTriangles)           {              Vector3 transP0 = Vector3.Transform(tri.P0, absTransform);               Vector3 transP1 = Vector3.Transform(tri.P1, absTransform);               Vector3 transP2 = Vector3.Transform(tri.P2, absTransform);                             Plane trianglePlane = new Plane(transP0, transP1, transP2);               float distanceOnRay = RayPlaneIntersection(ray, trianglePlane);               Vector3 intersectionPoint = ray.Position + distanceOnRay * ray.Direction;                             if (PointInsideTriangle(transP0, transP1, transP2, intersectionPoint))                   if ((collisionDistance == null) || (distanceOnRay < collisionDistance))                       collisionDistance = distanceOnRay;           }      }       return collisionDistance;   }

每次发生碰撞时,你检查collisionDistance是否仍是null。这会显示是第一次检测到碰撞,所以你将这个距离存储到collisionDistance 中。从这时起,你检查这个距离是否小于已知的距离,如果是,则重写这个距离。

从变量collisionDistance返回的结果将包含离相机最近的碰撞点,你可以使用这个结果检测哪个模型离相机最近。

代码

这个代码创建一个3D射线,这个射线描述所有属于通过光标显示的像素的点。这个射线传递到ModelRayCollision方法:

Vector3 nearScreenPoint = new Vector3(mouseState.X, mouseState.Y, 0);  Vector3 farScreenPoint = new Vector3(mouseState.X, mouseState.Y, 1);   Vector3 near3DWorldPoint = device.Viewport.Unproject(nearScreenPoint, fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity);   Vector3 far3DWorldPoint = device.Viewport.Unproject(farScreenPoint, fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity);     Vector3 pointerRayDirection = far3DWorldPoint - near3DWorldPoint;   pointerRayDirection.Normalize();   Ray pointerRay = new Ray(near3DWorldPoint, pointerRayDirection);   selected = ModelRayCollision(myModel, worldMatrix, pointerRay);

你可能感兴趣的:(处理)