你想检测光标是否在模型上。
在XNA中,获取光标在屏幕上的2D位置是简单的。屏幕上的这个点对应3D空间中的一条射线Ray,如图4-28所示。
因此,当你想检测光标在哪个模型上,需要检测射线与模型的碰撞,所以,这个教材会用到教程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。这两个点在屏幕空间的三个坐标,在光标的情况中如下所示:
下面是代码:
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);
前面的代码看起来很好,但如果你不能测试,代码再好也看不出来。所以让我们添加一个图像可以显示光标的位置,可见教程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);