2.5 检查对象是否可见

问题

你想检查对象是否可见来决定是否要绘制这个物体。

解决方案

XNA拥有BoundingFrustum类支持这个功能,你可以通过传递View和Projection矩阵创建一个BoundingFrustum类,然后就可以很容易地在这个类中检查对象是否包含在视锥体中。

工作原理

视锥体定义了相机可以看到的范围,类似于削去顶部的金字塔,如图2-4所示,它的边是由视域角,近裁平面和远裁平面决定的,更多的信息可见教程2-1。

你应该让XNA只绘制包含在视锥体中的物体,否则,你会浪费显卡的处理能力。所以你需要一个方法用来检测对象是否在视锥体中。

XNA Framework中的BoundingFrustum类包含了实现这个功能完整方法,你只需通过指定相机的观察矩阵和投影矩阵创建BoundingFrustum类即可。

1

图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会介绍一个更好的方法处理大场景。