2.9 只绘制在相机视野中的物体:八叉树

问题

如果你使用教程2-5的方法检查相机视野中的每个模型,那么由于网格太多会极大地拖慢程序。你需要一个可扩展的方法决定场景的哪些部分需要被绘制。

解决方案

定义一个很大的立方体包含整个3D世界和所有模型。如果这个立方体包含太多模型,那么把这个立方体分割成小的子立方体。如果这些小的子立方体包含太多模型,那么再把这些立方体分割成更小的子立方体,直到立方体内包含的模型数量不超过某个特定值或立方体的大小足够小。

在绘制过程中,大立方体要求小立方体检查它们是否在相机视野中,如果子立方体不在视野中,则无需绘制之内的模型,如果在视野内并且子立方体还有自己的子立方体,则继续处理它的子立方体,直到这个子立方体在视野内并且没有在它下面没有子立方体。结果是,只有在视野内的最后的子立方体才会绘制在其内部的模型。

工作原理

首先你需要决定一个立方体内有多少个模型,本例中使用最多五个模型。只要已经包含了五个模型的立方体再增加一个模型,那么这个立方体必须被分成更小的子立方体,能分成全等的唯一方式就是分成八个小立方体,如图2-10所示。因为八个小立方体能完美地匹配它们的父立方体,而且每个子立方体能分成更小的立方体,形成一个树状结构。因为每个父节点有八个子节点,所以这种结构叫做八叉树。

2-10

图2-10 分成八个小立方体的大立方体

图2-11和图2-12显示了操作原理。

看一下图2-11,这个八叉树包含了超过五个模型,所以父立方体会分成八个子立方体,你还能看到有一个子立方体也包含了五个模型。 现在考虑这种情况:在一个已经包含五个模型的子立方体中再添加一个模型,那么这个立方体会分成八个立方体,六个模型会分布到相应的子立方体中,如图2-12所示。

2-11

图2-11 在添加了一个模型之前的八叉树

2-12

图2-12 添加了一个模型之后的八叉树(译者注:但图中显示添加了2个模型?)

使用八叉树的好处是主程序只需请求绘制根立方体,而立方体会自己判断是否在相机视野内。如果是,立方体会将这个调用传递到子立方体,如果这个子立方体包含模型则会绘制这些模型,或者将调用传递到下一层的子立方体中。通过这种方式,八叉树会自己决定需要绘制哪些节点。

DrawableModel类

为了正确地绘制模型,八叉树的每个立方体需要保存模型的世界矩阵,世界矩阵还包含模型在3D空间中的位置,所以保存世界矩阵和模型就足以让八叉树知道模型属于哪个子立方体了。

使用类可以使八叉树的代码编写变得容易。本例中你要创建一个叫做DrawableModel的新类,这个类会定义一个包含模型以及它的世界矩阵的对象。对每个存储在八叉树中的模型,会创建一个DrawableModel对象,这个对象存储了模型和它的世界矩阵,实际上是这个对象存储在了八叉树的立方体中。你也可以定义一个Draw方法,如果立方体检测到自己在相机视野内就会调用这个方法。

要创建一个新的类,应在Solution Explorer中右击项目名称并选择Add New Item,在对话框中选择Class并输入DrawableModel作为名称。在这个新类顶部添加XNA的引用:

using Microsoft.Xna.Framework; 
using Microsoft.Xna.Framework.Audio; 
using Microsoft.Xna.Framework.Content; 
using Microsoft.Xna.Framework.Graphics; 
using Microsoft.Xna.Framework.Input; 
using Microsoft.Xna.Framework.Storage; 

我已经讨论过DrawableModel类的两个属性:Model和World矩阵。你还要存储变换矩阵实现动画 (见教程4-9),存储位置,存储ID用来识别对象。创建了DrawableModel类的实例后,这个对象会接受唯一的ID值,而这个ID会被传递到主程序中,允许你改变特定对象的世界矩阵。

所以在类中添加四个变量:

class DrawableModel
{
    private Matrix worldMatrix; 
    private Model model; 
    private Matrix[] modelTransforms; 
    private Vector3 position; 
    private int modelID; 
} 

DrawableModel类的构造函数有三个参数:Model,worldMatrix和ID。位置没有指定,因为位置是包含在世界矩阵中的,世界矩阵还包含缩放和旋转。

在类中添加如下的构造函数:

public DrawableModel(Model inModel, Matrix inWorldMatrix, int inModelID)
{
    model = inModel; 
    modelTransforms = new Matrix[model.Bones.Count]; 
    worldMatrix = inWorldMatrix; 
    modelID = inModelID; 
    position = new Vector3(inWorldMatrix.M41, inWorldMatrix.M42, inWorldMatrix.M43); 
} 

你可以看到位置实际上是在世界矩阵的最后一行中。

注意:你也可以通过调用矩阵的Decompose方法获取位置,包含在矩阵中的变换的平移分量对应模型的位置。

接下来添加Draw方法,这将使用世界矩阵和变换矩阵绘制模型:

public void Draw(Matrix viewMatrix, Matrix projectionMatrix) 
{ 
    model.CopyAbsoluteBoneTransformsTo(modelTransforms); 
    foreach (ModelMesh mesh in model.Meshes) 
    {
        foreach (BasicEffect effect in mesh.Effects) 
        { 
            effect.EnableDefaultLighting(); 
            effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; 
            effect.View = viewMatrix; 
            effect.Projection = projectionMatrix; 
         }
         mesh.Draw(); 
     }
} 

Draw方法是此类的主要行为,但是,现在主程序还无法访问这些私有变量。你可以将这些变量设置为共有,但这样做不好,因为这样做让外部代码可以读写这些变量。好的方法是使用getter方法:

public Vector3 Position
{
    get{return position; }
}

public Model Model 
{
    get{return model; }
}

public int ModelID 
{
    get{return modelID; }
} 

getter方法属于.NET 2.0,它只是简单地返回对应的属性,下面你就会看到它的使用。从现在开始,外部程序只能通过属性访问这些变量,但不能改变这些变量,这也是我们想要的结果。

还有一个变量不要从外部访问:worldMatrix。你必须提供给主程序一个更新对象世界矩阵的方法。所以不仅要定义getter方法还需定义一个setter方法。本例中setter方法的用处有两个:一是可以改变worldMatrix的值,二是从世界矩阵提取一个新的位置变量并把它存储在位置属性中。

public Matrix WorldMatrix
{
    get { return worldMatrix; } 
    set { worldMatrix = value; position = new Vector3(value.M41, value.M42, value.M43); }
}

getter方法允许主程序读取worldMatrix,setter方法从主程序接受新的值,这个值存储在 worldMatrix变量中,而位置信息是从这个变量中提取的。

以上就是定义DrawableModel类的过程。你定义一个类,这个类包含了模型和它的世界矩阵,可以使用当前世界矩阵绘制模型,主程序可以更新世界矩阵。

OcTreeNode类

现在你已定义了一个完整的DrawableModel类,下面就可以处理八叉树了。这个数是由对应八叉树的立方体的节点构成。你将定义第二个类:OcTreeNode,这个类表示了这个节点,每个节点可以存储DrawableModel对象并可以有8个子节点。

创建一个新的类:OcTreeNode。这个类包含一些属性:

private const int maxObjectsInNode = 5; 
private const float minSize = 5.0f; 
private Vector3 center; 
private float size; 
List<DrawableModel> modelList; 
private BoundingBox nodeBoundingBox; 
OcTreeNode nodeUFL; 
OcTreeNode nodeUFR; 
OcTreeNode nodeUBL; 
OcTreeNode nodeUBR; 
OcTreeNode nodeDFL; 
OcTreeNode nodeDFR; 
OcTreeNode nodeDBL; 
OcTreeNode nodeDBR; List<OcTreeNode> childList; 
private static int modelsDrawn; 
private static int modelsStoredInQuadTree;

第一行代码定义了一个立方体可以包含几个模型。在将这个立方体分隔成更小的立方体之前,你需要检查下面的立方体是否太小,所以需要定义第二个属性minSize。

下一步,每个节点都要存储一些东西,诸如中心位置和大小。每个节点可以存储一些(5个)DrawableModel对象,这些对象存储在modelList集合中。要检查立方体是否在相机视野中,还需包含BoundingBox,这样才能进行立方体和相机视锥体的碰撞检测。

接下来是八个子节点的集合,我起了对应的名称,例如nodeUFL表示“node Upper Forward Left(左上角的节点),”nodeDBR表示“node Down Back Right。”当你查找对应位置的子立方体时会发现这种命名方式很有用。

你创建的每个OcTreeNode保存这些变量的一个副本,但最后两个变量是静态变量,这意味着这两个变量是被所有OcTreeNode对象共享的。所以当一个OcTreeNode对象改变了这两个变量,其他对象也会知道这个变化。你使用modelsDrawn变量检查八叉树是否工作,而 modelsStoredInQuadTree用来找到添加到树中的模型的新ID。

在这个类中添加的第一个方法是构造函数,它创建一个OcTreeNode对象,所需的只是新立方体的中心位置和大小:

public OcTreeNode(Vector3 center, float size) 
{
    this.center = center; 
    this.size = size; 
    modelList = new List<DrawableModel>(); 
    childList = new List<OcTreeNode>(8); 
    Vector3 diagonalVector = new Vector3(size / 2.0f, size / 2.0f, size / 2.0f);
    nodeBoundingBox = new BoundingBox(center - diagonalVector, center + diagonalVector); 
} 

中心和大小是存储在私有变量中的,同时也实例化了modelList和childList。

技巧:因为你知道每个立方体最多包含8个节点,所以你可以在构造函数中指定这个大小。一个List最好的特点是你无需指定它的大小,但如果你指定了,那么当在List中添加一个元素时无需重新改变大小。

最后,你创建了对应立方体的BoundingBox。 BoundingBox的构造函数接受两个点,这两个点是立方体的对角线上的两点,如果立方体的边长是10,中心在(0,0,0),那么你可以指定坐标为(-5,-5,-5) 和 (5,5,5)的两点。如果中心不在(0,0,0),你需要在两个点上再加上中心位置,这一步是在构造函数的最后一行代码中实现的。

现在可以定义创建8个子立方体的方法了,这个方法马上就要用到:

private void CreateChildNodes()
{
    float sizeOver2 = size / 2.0f; 
    float sizeOver4 = size / 4.0f; 
    nodeUFR = new OcTreeNode(center + new Vector3(sizeOver4, sizeOver4, -sizeOver4), sizeOver2); 
    nodeUFL = new OcTreeNode(center + new Vector3(-sizeOver4, sizeOver4, -sizeOver4), sizeOver2); 
    nodeUBR = new OcTreeNode(center + new Vector3(sizeOver4, sizeOver4, sizeOver4), sizeOver2);
    nodeUBL = new OcTreeNode(center + new Vector3(-sizeOver4, sizeOver4, sizeOver4), sizeOver2); 
    nodeDFR = new OcTreeNode(center + new Vector3(sizeOver4, -sizeOver4, -sizeOver4), sizeOver2); 
    nodeDFL = new OcTreeNode(center + new Vector3(-sizeOver4, -sizeOver4, -sizeOver4), sizeOver2); 
    nodeDBR = new OcTreeNode(center + new Vector3(sizeOver4, -sizeOver4, sizeOver4), sizeOver2); 
    nodeDBL = new OcTreeNode(center + new Vector3(-sizeOver4, -sizeOver4, sizeOver4), sizeOver2); 
    childList.Add(nodeUFR); 
    childList.Add(nodeUFL); 
    childList.Add(nodeUBR); 
    childList.Add(nodeUBL); 
    childList.Add(nodeDFR); 
    childList.Add(nodeDFL); 
    childList.Add(nodeDBR); 
    childList.Add(nodeDBL); 
} 

首先创建8个子立方体并添加到childList中。如前所述,要创建一个节点,只需指定中心位置和大小。子立方体的边长是父立方体边长的一半。而找到子立方体的中心位置,你需要将父立方体的中心加上正确的向量。例如,要找到左上角的子立方体的中心,需要加上正Y(上)和负X和负Z的值(对应左和前)。

当父节点创建后,这些子节点不能自动建立,因为这些子节点还会包含自己的子节点。因此这个方法只有在把模型添加到节点并且节点已经包含最大数目的模型时才会被调用。这是在AddDrawableModel方法中处理的,下面是代码:

private void AddDrawableModel(DrawableModel dModel)
{
    if (childList.Count == 0) 
    {
        modelList.Add(dModel); 
        bool maxObjectsReached = (modelList.Count >maxObjectsInNode );
        bool minSizeNotReached = (size > minSize); 
        if (maxObjectsReached && minSizeNotReached) 
        {
            CreateChildNodes(); 
            foreach (DrawableModel currentDModel in modelList) 
            { 
                Distribute(currentDModel); 
            } 
            modelList.Clear(); 
        }
    }
    else
    {
        Distribute(dModel); 
    }
}

这个方法以DrawableModel对象作为参数,这个对象需要被添加到节点中。首先,你检查这个节点是否有子节点,这可以从childList是否包含超过一个元素中判断。如果不是,则将DrawableModel对象添加到modelList中。做完此步,还要检查此节点是否包含太多的DrawableModel对象,以及这个立方体是否大到可以分隔成更小的子立方体。

如果以上皆是,你将创建子节点并将每个DrawableModel对象分配到(使用Distribute方法)对应的modelList中。你将在后面创建这个Distribute方法。当所有的模型分配到子节点后,清除modelList。否则,这个节点和它的子节点都会绘制这个模型,会导致模型绘制了两次。

如果这个节点已经分隔成了子节点,那么DrawableModel对象会立即分配到对应的子节点中。

困难的部分是在Distribute方法中进行的。这个方法通过比较传入的DrawableModel 对象的位置与父节点的中心位置将这个对象放置到对应的子节点中。其实不是很难,因为你可以立即访问到DrawableModel的位置:

private void Distribute(DrawableModel dModel)
{
    Vector3 position = dModel.Position; 
    if (position.Y > center.Y) //Up 
        if (position.Z < center.Z) //Forward
            if (position.X < center.X) //Left 
                nodeUFL.AddDrawableModel(dModel); 
            else //Right
                nodeUFR.AddDrawableModel(dModel); 
        else //Back
            if (position.X < center.X) //Left
                nodeUBL.AddDrawableModel(dModel); 
            else //Right 
                nodeUBR.AddDrawableModel(dModel); 
    else //Down
        if (position.Z < center.Z) //Forward
            if (position.X < center.X) //Left 
                nodeDFL.AddDrawableModel(dModel); 
            else //Right
                nodeDFR.AddDrawableModel(dModel); 
        else //Back
            if (position.X < center.X) //Left
                nodeDBL.AddDrawableModel(dModel); 
            else //Right 
                nodeDBR.AddDrawableModel(dModel); 
} 

直到现在你只定义了私有方法,这意味着主程序无法访问节点并添加模型。所以,你需要添加Add方法被主程序调用:

 public int Add(Model model, Matrix worldMatrix)
{
    DrawableModel newDModel = new DrawableModel(model, worldMatrix, modelsStoredInQuadTree++); 
    AddDrawableModel(newDModel); 
    return newDModel.ModelID; 
} 

这个方法只是从调用者接受模型和世界矩阵,它会创建一个存储这两个参数的DrawableModel对象,DrawableModel对象还接受它的ID,这来自于modelsStoredInQuadTree变量,当将一个模型添加到八叉树中时这个变量会加一。因为这个变量是静态的,被所有节点共享,所以这个变量总是增加的,每个新添加的DrawableModel都会有一个新的ID,这个新ID总是比前一个DrawableModel大1。接下来,新创建的DrawableModel对被添加到八叉树中。

ID需要返回到主程序,这很重要。

在本教程最后,你会看到一些代码绘制每个立方体的边界,这是要添加的最后一个功能:绘制整个八叉树。这个Draw方法需要viewMatrix,projectionMatrix和cameraFrustum。你可以从两个矩阵计算出cameraFrustum, 每个子立方体都需要进行这个计算,所以计算一次然后把它传递到子立方体会快得多。

public void Draw(Matrix viewMatrix, Matrix projectionMatrix, BoundingFrustum cameraFrustum)
{
    ContainmentType cameraNodeContainment = cameraFrustum.Contains(nodeBoundingBox); 
    if (cameraNodeContainment != ContainmentType.Disjoint) 
    {
        foreach (DrawableModel dModel in modelList) 
        { 
            dModel.Draw(viewMatrix, projectionMatrix); 
            modelsDrawn++; 
        }
        foreach (OcTreeNode childNode in childList) 
            childNode.Draw(viewMatrix, projectionMatrix, cameraFrustum); 
     }
} 

你首先检查包围盒的相机视锥体的碰撞。如果没有碰撞,则这个立方体就没必要绘制,当然它的子立方体也无需绘制。

如果发生碰撞,则这个立方体中的所有DrawableModel都需被绘制。如果这个节点还包含子节点,那么Draw还要传递到这些子节点,并检查这些子节点是否在相机视锥体中 。

使用:主程序

这个教程理论上很漂亮,但如何使用呢?很简单,只需在主程序中定义一个变量: ocTreeRoot variable,这是包含所有立方体的父立方体:

OcTreeNode ocTreeRoot; 

在Initialize方法中进行初始化:

ocTreeRoot = new OcTreeNode(new Vector3(0, 0, 0), 21); 

对于立方体中心,你指定了3D世界的中心:(0,0,0) 。第二个参数是整个3D世界的大小。通常这个值大于我在本例中使用的21。

初始化八叉树后,你就可以添加模型了。这一步可以在加载模型后的任何地方进行。在下列代码中,我在LoadContent方法中完成此步,但你也可以在程序的其他地方写入下面的代码:

myModel = content.Load<Model>("tiny"); 
int[] modelIDs = new int[8]; 
modelIDs[0] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(1, 5, 5)); 
modelIDs[1] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(5, 4, -1)); 
modelIDs[2] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(10, -4, -1)); 
modelIDs[3] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(1, 4, -3)); 
modelIDs[4] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(5, 4, -3)); 
modelIDs[5] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(10, -4, -3)); 
modelIDs[6] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(8, 8, -3)); 
modelIDs[7] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(10, 8, -3)) 

上述代码将8个模型添加到八叉树中并将ID存储在modelIDs数组中。绘制过程中只需调用八叉树的Draw方法!这个调用会继续到所有的子立方体直到所用模型都被绘制。记住只有在相机视野中的立方体中的内容才会被绘制。

BoundingFrustum cameraFrustrum = new BoundingFrustum(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix); 
ocTreeRoot.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix, cameraFrustrum); 
更新模型的世界矩阵

在程序运行时,你还想更小模型的世界矩阵。这也是为什么主程序需要在八叉树中保存模型ID的原因:你需要通过ID改变模型的世界矩阵。要扩展八叉树使它可以改变世界矩阵,需要在OcTreeNode类中添加两个方法,第一个是公有方法可以被主程序调用:

public void UpdateModelWorldMatrix(int modelID, Matrix newWorldMatrix) 
{
    DrawableModel deletedModel = RemoveDrawableModel(modelID); 
    deletedModel.WorldMatrix = newWorldMatrix; 
    AddDrawableModel(deletedModel); 
} 

注意:这个方法只用到了RemoveDrawableModel和AddDrawableModel方法。实际上,你不想在每次更新世界矩阵时都删除并创建一个新对象,你可以使用一些技巧,例如,将所有的矩阵存储在一个大的集合中,这样你就可以在主程序中直接更新集合中的内容。

如你所见,主程序通过ID和新的世界矩阵指定它想改变的模型。首先,ID被传递到RemoveDrawableModel方法,这个方法在八叉树中搜索具有这个ID的DrawableModel。如果找到,则从立方体的modelList 中移除这个DrawableModel并返回它。然后这个DrawableModel的世界矩阵被更新并重新添加到八叉树中。很好,显然难的是RemoveDrawableModel方法,下面是代码:

private DrawableModel RemoveDrawableModel(int modelID) 
{
    DrawableModel dModel = null; 
    for (int index = 0; index < modelList.Count; index++) 
    {
        if (modelList[index].ModelID == modelID) 
        {
            dModel = modelList[index]; 
            modelList.Remove(dModel); 
        }
    }
    int child = 0; 
    while ((dModel == null) && (child < childList.Count))
    {
        dModel = childList[child++].RemoveDrawableModel(modelID); 
    } 
    return dModel; 
} 

这个方法首先通过ID检查这个模型是否在modelList中,如果是,这个DrawableModel会被存储到dModel变量并从modelList中移除。

注意:本例中不使用foreach循环遍历modelList,这是因为你要改变集合中的内容,而这在foreach中是不允许的。

如果找到模型,那么dModel就不再是null,所以while循环不被触发,DrawableModel被返回。

如果modelList中没有找到DrawableModel,你将检查八个子节点,这一步是在while循环中进行的,只要DrawableModel没有被找到而且八个子立方体没有被搜索,那么就会搜索下一个子节点。

很好,现在你可以在主程序中更新模型的世界矩阵了,这可以在update过程中实现:

time += 0.01f; 
Vector3 startingPos = new Vector3(1, 5, 5); 
Vector3 moveDirection = new Vector3(0, 0, -1); 
Matrix newWMatrix = Matrix.CreateScale(0.01f, 0.01f,0.01f)*Matrix.CreateTranslation(startingPos+time*moveDirection); 
int modelToChange = 0; 
ocTreeRoot.UpdateModelWorldMatrix(modelToChange, newWMatrix); 

上述代码可以让ID为0的模型移动到(0,0,-1)。

检查是否正常工作:绘制的模型数量

现在可以检查程序是否工作正常了。OcTreeNode类包含一个modelsDrawn变量,它是静态的,这意味着这个变量在所有节点中都是一样的,只有在模型被绘制的情况下这个变量才会在Draw方法中增加。

这是个私有变量,所以你无法在类外访问它。你可以将它变成公有,但这里我们使用getter+setter方法,将下列代码添加到OcTreeNode类中:

public int ModelsDrawn 
{
    get { return modelsDrawn; } 
    set { modelsDrawn = value; }
} 

在绘制过程中,你想在绘制八叉树前将这个值设置为0,当绘制完成,这个值会保存实际绘制到屏幕的模型数量并输出,例如,输出到窗口的标题栏中:

ocTreeRoot.ModelsDrawn = 0; 
BoundingFrustum cameraFrustrum = new BoundingFrustum(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix);
ocTreeRoot.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix, cameraFrustrum); 
Window.Title = string.Format("Models drawn: {0}", ocTreeRoot.ModelsDrawn); 

当立方体中的模型离开相机视野时,你会发现这个数值会减小。

检查是否正常工作:绘制立方体的边界

如果你想检查八叉树的结构,你可以使用XNAUtils 类中的DrawBoundingBox方法绘制包围盒的边界:

public void DrawBoxLines(Matrix viewMatrix, Matrix projectionMatrix, GraphicsDevice device, BasicEffect basicEffect) 
{
    foreach (OcTreeNode childNode in childList) 
        childNode.DrawBoxLines(viewMatrix, projectionMatrix,device, basicEffect); 
        if (childList.Count == 0) 
            XNAUtils.DrawBoundingBox(nodeBoundingBox, device, basicEffect, Matrix.Identity, viewMatrix, projectionMatrix); 
} 

这个方法检查节点是否包含子节点。如果是,将调用传递到每个子节点。当到达最后一个节点时,会使用DrawBoundingBox方法绘制它的包围盒。要绘制所用立方体的边,你只需调用根节点的DrawBoxLines方法,这个方法会深入到所有的子节点:

ocTreeRoot.DrawBoxLines(fpsCam.GetViewMatrix(), fpsCam.GetProjectionMatrix(),basicEffect); 

这里使用的绘制立方体包围盒的DrawBoundingBox方法是快捷但丑陋(dirty)的。说它丑陋是因为每帧都要重复的创建顶点和索引。说它快捷是因为不需要在节点中存储额外的变量,因此这个方法很容易被添加和移除,因为在很多场合中绘制包围盒的边界是有用的,所以我把它放在了XNAUtils类中:

public static void DrawBoundingBox(BoundingBox bBox, GraphicsDevice device, BasicEffect basicEffect, 
		Matrix worldMatrix,Matrix viewMatrix, Matrix projectionMatrix) 
{
    Vector3 v1 = bBox.Min; 
    Vector3 v2 = bBox.Max; 
    VertexPositionColor[] cubeLineVertices = new VertexPositionColor[8]; 
    cubeLineVertices[0] = new VertexPositionColor(v1, Color.White); 
    cubeLineVertices[1] = new VertexPositionColor(new Vector3(v2.X, v1.Y, v1.Z), Color.Red); 
    cubeLineVertices[2] = new VertexPositionColor(new Vector3(v2.X,v1.Y, v2.Z), Color.Green); 
    cubeLineVertices[3] = new VertexPositionColor(new Vector3(v1.X, v1.Y, v2.Z), Color.Blue); 
    cubeLineVertices[4] = new VertexPositionColor(new Vector3(v1.X, v2.Y, v1.Z), Color.White);
    cubeLineVertices[5] = new VertexPositionColor(new Vector3(v2.X, v2.Y, v1.Z), Color.Red); 
    cubeLineVertices[6] = new VertexPositionColor(v2, Color.Green); 
    cubeLineVertices[7] = new VertexPositionColor(new Vector3(v1.X, v2.Y, v2.Z), Color.Blue); 
    short[] cubeLineIndices = { 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 }; 
    basicEffect.World = worldMatrix; 
    basicEffect.View = viewMatrix; 
    basicEffect.Projection = projectionMatrix; 
    basicEffect.VertexColorEnabled = true; 
    device.RenderState.FillMode = FillMode.Solid; 
    basicEffect.Begin(); 
    foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) 
    {
        pass.Begin(); 
        device.VertexDeclaration = new VertexDeclaration(device, ertexPositionColor.VertexElements); 
        device.DrawUserIndexedPrimitives<VertexPositionColor>(PrimitiveType.LineList, cubeLineVertices, 
        0, 8,cubeLineIndices, 0, 12); 
        pass.End(); 
    }
    basicEffect.End(); 
} 

技巧:你可以将这个方法作为使用索引化的LineList的例子。首先,八个顶点存储在一个数组中,然后创建一个对应顶点数组的索引数组。索引数组中每两个数字定义一条线段,所以24个索引定义立方体的12条线段。绘制图元的更多信息请见教程5-1。

代码

DrawableModel类包含绘制3D模型的所有信息和方法:

class DrawableModel
{
    private Matrix worldMatrix; 
    private Model model; 
    private Matrix[] modelTransforms; 
    private Vector3 position; 
    private int modelID; 
    
    public Matrix WorldMatrix
    {
        get { return worldMatrix; } 
        set 
        {
            worldMatrix = value;
            position = new Vector3(value.M41, value.M42, value.M43); 
        }
    }
    
    public Vector3 Position 
    {
        get { return position; } 
    }
    
    public Model Model
    {
        get { return model; } 
    }
    
    public int ModelID 
    {
        get { return modelID; } 
    }
    
    public DrawableModel(Model inModel, Matrix inWorldMatrix, int inModelID) 
    {
        model = inModel; 
        modelTransforms = new Matrix[model.Bones.Count]; 
        worldMatrix = inWorldMatrix; 
        modelID = inModelID; 
        position = new Vector3(inWorldMatrix.M41, inWorldMatrix.M42, inWorldMatrix.M43); 
    }
    
    public void Draw(Matrix viewMatrix, Matrix projectionMatrix) 
    {
        model.CopyAbsoluteBoneTransformsTo(modelTransforms); 
        foreach (ModelMesh mesh in model.Meshes) 
        {
            foreach (BasicEffect effect in mesh.Effects) 
            {
                effect.EnableDefaultLighting(); 
                effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; 
                effect.View = viewMatrix; 
                effect.Projection = projectionMatrix; 
            }
            mesh.Draw(); 
        }
    }
} 

OcTreeNode类存储每个模型的一个链接,也包含检查节点是否在相机视野和添加/移除模型的所有功能:

class OcTreeNode
{
    private const int maxObjectsInNode = 5; 
    private const float minSize = 5.0f; 
    private Vector3 center; 
    private float size; 
    List<DrawableModel> modelList; 
    private BoundingBox nodeBoundingBox; 
    OcTreeNode nodeUFL;
    OcTreeNode nodeUFR; 
    OcTreeNode nodeUBL; 
    OcTreeNode nodeUBR; 
    OcTreeNode nodeDFL; 
    OcTreeNode nodeDFR; 
    OcTreeNode nodeDBL; 
    OcTreeNode nodeDBR; 
    
    List<OcTreeNode> childList;     
    public static int modelsDrawn; 
    private static int modelsStoredInQuadTree;
    public int ModelsDrawn 
    { 
        get { return modelsDrawn;} 
        set { modelsDrawn = value; } 
    } 
	
	public OcTreeNode(Vector3 center, float size) 
	{
	    this.center = center; 
	    this.size = size; 
	    modelList = new List();
	    childList = new List(8); 
	    Vector3 diagonalVector = new Vector3(size / 2.0f, size / 2.0f, size / 2.0f); 
	    nodeBoundingBox = new BoundingBox(center - diagonalVector, center + diagonalVector); 
	} 
	
	public int Add(Model model, Matrix worldMatrix) 
	{ 
	    DrawableModel newDModel = new DrawableModel(model, worldMatrix,	modelsStoredInQuadTree++); 
	    AddDrawableModel(newDModel); 
	    return newDModel.ModelID; 
	} 
	
	public void UpdateModelWorldMatrix(int modelID, Matrix newWorldMatrix) 
	{ 
	    DrawableModel deletedModel = RemoveDrawableModel(modelID); 
	    deletedModel.WorldMatrix = newWorldMatrix; AddDrawableModel(deletedModel);
	} 
	
	private DrawableModel RemoveDrawableModel(int modelID) 
	{ 
	    DrawableModel dModel = null; 
	    for (int index = 0; index < modelList.Count; index++)
	    { 
	        if 	(modelList[index].ModelID == modelID) 
	        {
	            dModel = modelList[index]; 
	            modelList.Remove(dModel); 
	        } 
	    } 
	    
	    int child = 0;
	    while ((dModel == null) && (child < childList.Count)) 
	    { 
	        dModel = childList[child++].RemoveDrawableModel(modelID);
	    } 
	    return dModel; 
	}
	
	private void AddDrawableModel(DrawableModel dModel)
	{
	    if (childList.Count == 0) 
	    { 
	        modelList.Add(dModel); 
	        bool maxObjectsReached = (modelList.Count > maxObjectsInNode); 
	        bool minSizeNotReached = (size > minSize); 
	        if (maxObjectsReached && minSizeNotReached) 
	        {
	            CreateChildNodes(); 
	            foreach (DrawableModel currentDModel in modelList) 
	            {
	                Distribute(currentDModel); 
	            } 
	            modelList.Clear(); 
	        }
	    }
	    else
	    {
	        Distribute(dModel); 
	    }
	}
	
	private void CreateChildNodes()
	{
	    float sizeOver2 = size / 2.0f; 
	    float sizeOver4 = size / 4.0f; 
	    nodeUFR = new OcTreeNode(center + new Vector3(sizeOver4, sizeOver4, -sizeOver4), sizeOver2);
	    nodeUFL = new OcTreeNode(center + new Vector3(-sizeOver4, sizeOver4, -sizeOver4), sizeOver2); 
	    nodeUBR = new OcTreeNode(center + new Vector3(sizeOver4, sizeOver4, sizeOver4), sizeOver2); 
	    nodeUBL = new OcTreeNode(center + new Vector3(-sizeOver4, sizeOver4, sizeOver4), sizeOver2); 
	    nodeDFR = new OcTreeNode(center + new Vector3(sizeOver4, -sizeOver4, -sizeOver4), sizeOver2); 
	    nodeDFL = new OcTreeNode(center + new Vector3(-sizeOver4, -sizeOver4, -sizeOver4), sizeOver2); 
	    nodeDBR = new OcTreeNode(center + new Vector3(sizeOver4, -sizeOver4, sizeOver4), sizeOver2); 
	    nodeDBL = new OcTreeNode(center + new Vector3(-sizeOver4, -sizeOver4, sizeOver4), sizeOver2); 
	    childList.Add(nodeUFR); 
	    childList.Add(nodeUFL); 
	    childList.Add(nodeUBR); 
	    childList.Add(nodeUBL); 
	    childList.Add(nodeDFR); 
	    childList.Add(nodeDFL); 
	    childList.Add(nodeDBR); 
	    childList.Add(nodeDBL); 
	} 
	
	private void Distribute(DrawableModel dModel)
	{
	    Vector3 position = dModel.Position; 
	    if (position.Y > center.Y) //Up 
	        if (position.Z < center.Z) //Forward
	            if (position.X < center.X) //Left 
	                nodeUFL.AddDrawableModel(dModel); 
	            else //Right
	                nodeUFR.AddDrawableModel(dModel); 
	        else //Back
	            if (position.X < center.X) //Left
	                nodeUBL.AddDrawableModel(dModel); 
	            else //Right 
	                nodeUBR.AddDrawableModel(dModel); 
	    else //Down
	        if (position.Z < center.Z) //Forward
	            if (position.X < center.X) //Left 
	                nodeDFL.AddDrawableModel(dModel); 
	            else //Right
	                nodeDFR.AddDrawableModel(dModel); 
	        else //Back
	            if (position.X < center.X) //Left
	                nodeDBL.AddDrawableModel(dModel); 
	            else //Right 
	                nodeDBR.AddDrawableModel(dModel); 
	} 
			
	public void Draw(Matrix viewMatrix, Matrix projectionMatrix, BoundingFrustum cameraFrustrum) 
	{
	    ContainmentType cameraNodeContainment = cameraFrustrum.Contains(nodeBoundingBox);
	    if (cameraNodeContainment != ContainmentType.Disjoint) 
	    {
	        foreach (DrawableModel dModel in modelList) 
	        {
	            dModel.Draw(viewMatrix, projectionMatrix); 
	            modelsDrawn++; 
	        }
	        foreach (OcTreeNode childNode in childList) 
	            childNode.Draw(viewMatrix, projectionMatrix, cameraFrustrum); 
	    }
	}
	
	public void DrawBoxLines(Matrix viewMatrix, Matrix projectionMatrix, GraphicsDevice device, BasicEffect basicEffect)
	{
	    foreach (OcTreeNode childNode in childList) 
	        childNode.DrawBoxLines(viewMatrix, projectionMatrix, device, basicEffect); 
	        if (childList.Count == 0) 
	            XNAUtils.DrawBoundingBox(nodeBoundingBox, device, basicEffect, Matrix.Identity, viewMatrix, projectionMatrix); 
} 

在XNA项目中,你在八叉树中存储了所有模型以及它们的世界矩阵:

protected override void LoadContent() 
{
    device = graphics.GraphicsDevice; 
    cCross = new CoordCross(device); 
    basicEffect = new BasicEffect(device, null); 
    myModel = content.Load<Model>("content/tiny"); 
    int[] modelIDs = new int[8]; 
    modelIDs[0] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(1, 5, 5)); 
    modelIDs[1] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(5, 4, -1)); 
    modelIDs[2] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(10, -4, -1)); 
    modelIDs[3] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(1, 4, -3));
    modelIDs[4] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(5, 4, -3)); 
    modelIDs[5] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(10, -4, -3)); 
    modelIDs[6] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(8, 8, -3)); 
    modelIDs[7] = ocTreeRoot.Add(myModel, Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(10, 8, -3)); 
} 

将模型存储到八叉树后,你可以通过一个简单的调用绘制它们。传递一个视锥体,如果不在相机视野内,八叉树就不会绘制模型。

BoundingFrustum cameraFrustrum = new BoundingFrustum(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix); 
ocTreeRoot.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix, cameraFrustrum); 

如果你想更新模型的世界矩阵,只需使用前面已经定义的UpdateModelWorldMatrix 方法:

ocTreeRoot.UpdateModelWorldMatrix(modelToChange, newWorldMatrix); 
扩展阅读

本教程的八叉树是基本的教程,帮助你理解空间分隔的思想。代码中还有一点小缺陷,例如,改变一个不存在的modelID的模型的世界矩阵会引发错误。

DeleteDrawableModel方法也可以加以改进,因为整个八叉树会一直搜索直到最后一个对象。更好的方法是将所有模型的位置存储在一个集合中,这样八叉树可以以一个更有效率的方式找到模型。而且,ID已经包含了应该朝哪个方向搜索的信息。

而且,这个方法让你只存储(绘制)包含effect的模型,因为DrawableModel的Draw方法是非常具体的。更普遍的方法是定义一个虚拟DrawableObj类,这个类可以存储在八叉树中,其他类可以从这个类继承。

再次说,本教程是帮助你理解八叉树的概念的,只需添加更严格的功能,这个结构己经足以介绍八叉树了。

对象剔除的知识非常广泛。本教程展示了一个基本例子,因为这个例子只剔除不在相机视野中的对象,所以方法不是很难。更先进的解决方案是闭塞剔除(occlusion culling),一个3D空间分隔成对应的3D空间本身。例如,将物体放在房子内,会使用房子对应的空间而不是对称的立方体。这些房间彼此以房门连接。通过这种方式,你可以检查房间是否在视野内或者检查墙或房门是否阻挡了视线。

22