2.5 检查对象是否可见
问题
你想检查对象是否可见来决定是否要绘制这个物体。
解决方案
XNA拥有BoundingFrustum类支持这个功能,你可以通过传递View和Projection矩阵创建一个BoundingFrustum类,然后就可以很容易地在这个类中检查对象是否包含在视锥体中。
工作原理
视锥体定义了相机可以看到的范围,类似于削去顶部的金字塔,如图2-4所示,它的边是由视域角,*裁*面和远裁*面决定的,更多的信息可见教程2-1。
你应该让XNA只绘制包含在视锥体中的物体,否则,你会浪费显卡的处理能力。所以你需要一个方法用来检测对象是否在视锥体中。
XNA Framework中的BoundingFrustum类包含了实现这个功能完整方法,你只需通过指定相机的观察矩阵和投影矩阵创建BoundingFrustum类即可。
图2-4 相机的视锥体
BoundingFrustum对象允许你使用它的Contain方法,你可以传递一个Vector3和一个BoundingSphere或BoundingBox检查是否包含在相机的视锥中。这个方法会返回一个Containment Type对象,这个对象包含三个值:
- Contains:完整包含在视锥体中的测试对象。
- Intersects:部分包含在视锥体中的测试对象,这时这个对象与视锥体相交。
- Disjoint:不在视锥体中的测试对象。
检查3D空间中的点是否在相机可视范围内
要检查一个点是否可见,只需将这个点传递到Contains方法:
Vector3 pointToTest = new Vector3(0, 0, 0); BoundingFrustum cameraFrustum = new BoundingFrustum(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix); ContainmentType containmentType = cameraFrustum.Contains(pointToTest); if (containmentType != ContainmentType.Disjoint) { Window.Title = "Point inside frustrum"; graphics.GraphicsDevice.Clear(Color.CornflowerBlue); } else { Window.Title = "Point outside frustrum"; graphics.GraphicsDevice.Clear(Color.Red); }
这个例子中你检查了点(0,0,0)是否在视锥体中,将检查结果写在了窗口标题栏中并相应地改变背景颜色。
检查一个物体是否可见
如果你想检查一个物体是否可见,一个方法是事先检查模型的每个顶点,这样做非常耗时。更快的方法(尽管不太确切)是定义一个包含模型的包围体,然后检查这个包围体是否在视锥体中。能包含模型的最简单的包围体是一个球。
XNA已在BoundingSphere类中支持创建一个包围体,使用教程4-5中介绍的LoadModelWithBoundingSphere方法载入模型,并在Tag属性中保存BoundingSphere:
myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "content/tiny", content);
技巧:因为模型的Tag属性可以存储任何类型的数据结构,你可以用让它存储任何与模型相关的数据,例如BoundingSphere,纹理等。你甚至还可以创建一个包含所有数据的结构并将这个结构存储在Tag属性中。
现在有了包围体你就可以测试它是否在视锥体中了:
BoundingFrustum cameraSight = new BoundingFrustum(fpsCam.ViewMatrix* fpsCam.ProjectionMatrix); ContainmentType containmentType = cameraSight.Contains( (BoundingSphere)myModel.Tag); if (containmentType != ContainmentType.Disjoint) { Window.Title = "Point inside frustrum"; graphics.GraphicsDevice.Clear(Color.CornflowerBlue); Window.Title = "Point outside frustrum"; graphics.GraphicsDevice.Clear(Color.Red); }
检查一个设置了世界矩阵的对象在大多数情况中,你的模型并不在(0,0,0)位置而且还可能旋转,缩放或通过设置世界矩阵放置在其他位置,更多信息可见教程4-2。显然包围体也应该旋转/缩放/移动已匹配模型。这可以通过模型的世界矩阵变换包围体实现:
Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f) *Matrix.CreateTranslation(5, 0, 0); BoundingFrustum cameraSight = new BoundingFrustum(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix); BoundingSphere origSphere = (BoundingSphere)myModel.Tag; BoundingSphere transSphere = origSphere.Transform(worldMatrix); ContainmentType containmentType = cameraSight.Contains(transSphere); if (containmentType != ContainmentType.Disjoint) { Window.Title = "Model inside frustrum"; graphics.GraphicsDevice.Clear(Color.CornflowerBlue); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); } Window.Title = "Model outside frustrum"; graphics.GraphicsDevice.Clear(Color.Red); }
注意可见教程第三章学习如何绘制一个模型。这个代码中的主要变化是你调用了origSphere. Transform方法,这个方法获取经过世界变换的BoundingSphere。本例中transSphere的大小是origSphere百分之一,中心沿x轴移动了5个单位。只有当模型在视锥体时才会绘制这个模型。
代码
下面的代码会在一个指定位置绘制一个模型,当它在视锥体之外就不会被绘制:
protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms,"content/tiny", content); } protected override void Draw(GameTime gameTime) { Matrix worldMatrix = Matrix.CreateScale(0.01f,0.01f,0.01f)*Matrix.CreateTranslation(5, 0, 0); BoundingFrustum cameraSight = new BoundingFrustum(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix); BoundingSphere origSphere = (BoundingSphere)myModel.Tag; BoundingSphere transSphere = origSphere.Transform(worldMatrix); ContainmentType containmentType = cameraSight.Contains(transSphere); if (containmentType != ContainmentType.Disjoint) { Window.Title = "Model inside frustrum"; graphics.GraphicsDevice.Clear(Color.CornflowerBlue); //draw model myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); } } else { Window.Title = "Model outside frustrum"; graphics.GraphicsDevice.Clear(Color.Red); } //draw coordcross cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); base.Draw(gameTime); }
扩展阅读
这个教程提出了一个检查点或物体是否在视锥体中的方法。但是如果场景中有大量的物体,那么要对每个对象进行检查会使程序变得很慢。在教程2-9会介绍一个更好的方法处理大场景。