现在所有的对象已经被放置在他们正确的位置上了,是决定哪些对象需要渲染到当前场景的时候了。这个过程被称为剔除。目前有三个主流的剔除技术:二分空间划分(BSP),入口(Portal)和四叉树。
剔除技术
BSP技术通过划分和再细分整个数据库,里面的所有东西都基于划分的平面。通过放置平面划分区域,使位于平面一边的对象可见,而位于相反的一面的对象不可见,我们可以快速处理这颗树,发现哪些对象可见。虽然此技术在执行时的速度快得令人不可置信,但是它也有缺点。BSP对室内区域特别有效,这些区域由墙壁(通常是相互垂直的)划分成小的部分(例如房间)。分割平面类似于墙(Separating planes fall naturally along these walls),因为位于墙一边的物体往往不能在墙的对面看到。如果一个分割平面穿过一个物体,我们也要把此物体分割成两个部分。每一个部分只能在平面的一边可见。The intelligent placing of these separating planes typicall calls for an experienced BSP model builder and the proper modeling tools geared toward BSP maps。
第二个技术是入口剔除。入口系统是为室内地图设计的,地图上的每一个位置是一个房间和通向存在的相邻房间的入口。入口可以是门,窗户或者任何允许当前房间之外可见的口子。系统中,只有房间内的物体或者通过入口可见的物体才需要绘制。检测是递归的进行,所以如果在邻接的房子里又一个入口可见,那么我们需要继续处理通过这个入口可见看到什么物体。一直继续,直到视野内没有未做处理的入口。此系统非常快速,但是仅仅限于室内场景。
最后一个技术是四叉树或者它的派生物八叉树。四叉树技术递归的划分整个世界为四个象限(quadrants)。每个象限都是采用同样的方法来细分。一直继续,直到达到指定的递归级数或者象限尺寸。八叉树技术也是同样的操作,但是是在三维空间。如果你建模的世界在垂直轴上包含很多细节,那么可能需要使用八叉树。不是把每个正方形细分成四份,而是把每个立方体区域细分为八个立方体(即 2x2x2 而不是 2x2)。如果你的世界是平面,最好是使用四叉树来简化和加快你的剔除。
实现四叉树
图3-1 演示了四叉树结构的流程。位于左上角的梯形表示平截头体视景体。如果方块根本没和平截头体相交,这个方块和它所有的孩子都被认为剔除。如果方块完全在平截头体内,它和它所有的孩子都被认为是可见的。只有当方块部分的位于平截头体内,才继续剔除,检查它的孩子们的方块。递归继续知道每个方块都完全位于平截头体内或者到达最低级别的方块。当一个方块查明是可见的,那么所有位于此方块的物体都被标记为看见。
图 3-1:四叉树 示例
Quad类(参见列表3-2)封装了四叉树节点的特性。每个节点有一个矩形定义了方块的坐标边界,占据此节点的对象列表,和对所有的父节点和孩子节点。
列表 3.2: Quad 声明
public class Quad : IDispoable
{
private Rect m_Bounds;
private Quad m_NorthEast = null;
private Quad m_NorthWest = null;
private Quad m_SouthWest = null;
private Quad m_SouthEast = null;
private Quad m_Parent = null;
private int m_nLevel;
private SortedList m_Objects;
private float m_fRadius;
private Vector
private string m_sName;
private static Quad m_BaseQuad = null;
public Rectangle Bounds { get { return m_Bounds; } }
public string Name { get { return m_sName; } }
public void Quad( Rectangle bounds, int level, int maxlevel,
Quad parent ) { ... };
public void AddObject( Object3D obj ) { ... };
public void RemoveObject( Object3D obj ) { ... };
public void Cull( Camera cam ) { ... };
public void Update( Object3D obj ) { ... };
public void Dispose() { ... };
}
我们从类的构造函数(参见列表3-3)来看看树是如何构建的。构造函数初始化自身后,然后检查是否达到树的最大级别。如果这是被初始化的第一个方块,我们把此基本块存储在静态的引用里(译注: m_BaseQuad = this)。如果这不是第一个被初始化的方块,它创建四个更小的方块作为孩子。每一个孩子方块的包围矩形是方块的四分之一。孩子方块的级别递增,以便孩子创建在下一个递归级别上。方块的中心点坐标以及方块的包围球半径将会用来做快速测试,看物体是否位于方块之内。
列表3.3: Quad 构造函数
Public void Quad ( Rect bounds, int level, int maxlevel, Quad parent )
{
if ( m_BaseQuad == null )
{
m_BaseQuad = this;
}
m_Bounds = bounds;
m_n Level = level;
m_Parent = parent;
m_Objects = new SortedList();
m_sName = "L" + level + ":X" + bounds.Left + "Y" + bounds.Top;
m_vPosition.X = (bounds.Left + bounds. Right) / 2.
m_vPosition.Y =
m_vPosition.Z = (bounds. Top + bounds. Bottom) / 2.
double dx = bounds. Width;
double dz = bounds.Height;
m_fRadius = (float)Math.Sqrt( dx * dx + dz * dz ) / 2.
if (level < maxlevel)
{
int nHalfHeight = dz / 2;
int nHalfWidth = dx / 2;
m_NorthEast = new Quad ( new Rectangle(bounds.Left + nHalfWidth,
bounds.Top, bounds.Right, bounds.Top + nHalfHeight),
level+1, maxlevel);
m_NorthWest = new Quad (new Rectangle (bounds.Left, bounds.Top,
bounds.Left + nHalfWidth, bounds.Top + nHalfHeight),
level+1, maxlevel);
m_SouthWest = new Quad ( new Rectangle (bounds.Left,
bounds.Top + nHalfHeight,
bounds.Left + nHalfWidth, bounds.Bottom), level+1,
maxlevel);
m_SouthEast = new Quad (new Rectangle (bounds.Left + nHalfWidth,
bounds.Top + nHalfHeight, bounds.Right, bounds.Bottom),
level+1, maxlevel);
}
}
存在两个方法用来管理位于方块内的对象:AddObject(列表3-4)和RemoveObject(列表3-5,本节后面一点会讲到)。AddObject方法把一个对象的引用加入到方块的对象列表里,并且加入到此方块的子方块中,只要对象位于这些子方块中。只有最高层的(top-level)对象才加入到四叉树中,因为子对象的剔除状态(culled state)依赖于父对象的剔除状态。在此技术中,一个对象可能被相对多的方块所引用。虽然冗余会浪费一点存储空间,但是其增加的处理速度补偿了这个缺点。一旦剔除过程(culling process)确定了一个方块完全位于视野之内,我们就没有必要再继续迭代下去了。我们可以仅仅遍历此节点的对象链表,把对象都设为不剔除状态。
此方法从检查给定的对象是否有效开始。假定传递的是一个有效对象,那么该检查确定此对象是否至少部分位于方块之内。如果此对象之前在此方块中但是现在不在(译注:原文used to be in this quad,used to be表示以前在现在不在),我们删除它并且把加入对象的请求传给父方块,以便重新配置(repositioning)此对象。如果对象在此方块里,我们会检查看此对象是否已经在我们的对象列表里了。如果此对象对此方块来说是新的,就把它加入到对象列表里,并且通知此方块所有有效的子方块,以便他们把对象加入他们的对象链表里,如果需要的话。
列表 3.4:Quad AddObject 方法
public void AddObject( Object3D obj )
{
if ( obj != null )
{
if ( obj.InRect( m_Bounds ) )
{
int nIndex = m_Objects.IndexOfKey( obj.Name );
try
{
if ( nIndex < 0 ) // Add object if we don't have it yet.
{
m_Objects.Add(obj.Name, obj );
obj.m_Quads.Add(this);
if ( m_NorthEast != null && obj.InRect( m_NorthEast.Bounds ) )
{
m_NorthEast.AddObject( obj );
}
if ( m_NorthWest != null && obj.InRect( m_NorthWest.Bounds ) )
{
m_NorthWest.AddObject( obj );
}
if ( m_SouthWest != null && obj.InRect( m_SouthWest.Bounds ) )
{
m_SouthWest.AddObject( obj );
}
if ( m_SouthEast != null && obj.InRect( m_SouthEast.Bounds ) )
{
m_SouthEast.AddObject( obj );
}
}
else
{
Console.AddLine("Attempt to add another " + obj.Name );
}
}
catch (DirectXException d3de)
{
Console.AddLine("Unable to add object" );
Console.AddLine(d3de.ErrorString);
catch ( Exception e )
{
Console.AddLine("Unable to add object" );
Console.AddLine(e.Message);
}
}
else
{
int nIndex = m_Objects.IndexOfKey( obj.Name );
if ( nIndex >= 0 ) // remove the object if we have it
{
RemoveObject( obj );
if ( m_Parent != null )
{
m_Parent.AddObject( obj );
}
}
}
}
}
}
Quad操作对象的第二个方法是RemoveObject(列表 3-5)。此方法执行相反的过程,删除方块对象列表里的对象引用。如果它位于方块的对象列表里,那么它可能会出现在方块的一个或者多个子方块的对象列表里。RemoveObject方法因此会为它的每个子方块调用此方法。当一个对象从一个方块移动到另外一个方块就需要调用此方法。
列表 3.5:Quad RemoveObject 方法
public void RemoveObject ( Object3D obj )
{
if ( obj != null )
{
int nIndex = m_Objects.IndexOfKey( obj.Name );
if ( nIndex >= 0 )
{
try
{
m_Objects. Remove ( obj.Name );
}
{
Console.AddLine(
"failing while removing object from quad object list" );
}
try
{
if ( obj.m_Quads.Count > 0 )
{
obj.m_Quads.Clear();
}
}
catch
{
Console.AddLine(
"failing while clearing objects quad list");
}
m_Objects.RemoveAt( nIndex );
if ( m_NorthEast != null )
{
m_NorthEast.RemoveObject( obj );
}
if ( m_NorthWest != null )
{
m_NorthWest.RemoveObject( obj );
}
if ( m_SouthWest != null )
{
m_SouthWest.RemoveObject( obj );
}
if ( m_SouthEast != null )
{
m_SouthEast.RemoveObject( obj );
}
}
}
}
实现剔除
Cull方法(列表3-6)确定哪些对象应该被显示哪些应该剔除。此方法使用Camera类的CheckFrustum方法来确定方块的包围矩形哪些部分位于平截头体(frustum)内。如果包围矩形完全位于平截头体内,那么我们不需要继续从树的分支遍历下去了。位于此方块的所有对象都被标记为不剔除(即他们都可见)。如果方块完全位于平截头体,我也不必要对这方块再做什么,因为所有的对象默认都是剔除状态。最后,如果方块只有部分位于平截头体内,我们需要继续此树的下一级别。一旦我们到达树的底部,并且它们仅只是部分位于平截头体内,我们也必须把此方块当作是位于内部。
列表 3.6:Quad Cull 方法
public void Cull( Camera cam )
{
Object3D obj;
int i;
cam.Reset();
if ( m_Objects.Count > 0 )
{
try
{
switch ( cam.CheckFrustum( m_vPosition, m_fRadius ) )
{
case Camera.CullState.AllInside:
for ( i = 0; i < m_Objects.Count; i++ )
{
obj = (Object3D)m_Objects.GetByIndex(i);
obj.Range = cam.GetDistance( obj );
obj.Culled = false;
m_Objects.SetByIndex(i, obj);
cam.AddVisibleObject( obj );
}
break;
case Camera.CullState.AllOutside:
if ( m_Parent == null ) // i.e., if this is the root quad
{
goto case Camera.CullState.PartiallyIn;
}
// do nothing since the default state is true
// (reset after each render),
case Camera.CullState.PartiallyIn:
if ( m_NorthEast != null )
{
m_NorthEast.Cull( cam );
m_NorthWest.Cull( cam );
m_SouthWest.Cull( cam );
m_SouthEast.Cull( cam );
}
else // If partially in at the bottom level treat as in.
{
for ( i = 0; i < m_Objects.Count; i++ )
{
obj = (Object3D)m_Objects.GetByIndex(i);
obj.Culled = false;
m_Objects.SetByIndex(i, obj);
cam.AddVisibleObject( obj );
}
}
break;
}
}
catch (DirectXException d3de)
{
Console.AddLine("Unable to cull object" );
Console.AddLine (d3de.ErrorString);
}
catch ( Exception e )
{
Console.AddLine("Unable to cull object" );
Console.AddLine(e.Message);
}
}
因为有些对象会被从环境中彻底移除,所以需要方块的对象列表。Update方法(参见列表 3-7)提供了此项服务。要删除的对象引用作为参数被传入此方法。因为对象保存了一个方块的集合,这些方块都包含此对象,我们需要检查每一个这些方块。如果对象从任何一个方块这移除,都会出发复位请求。对象被移除这些方块,然后被重新插入此四叉树。
列表 3.7:Quad Update 方法
public void Update ( Object3D obj )
{
bool bResetNeeded = false;
try
{
// Only need to reset the quad for the object if it is
// no longer in one of the quads.
try
{
foreach ( Quad q in obj.m_Quads )
{
try
{
if ( !obj.InRect( q.m_Bounds ) )
{
bResetNeeded = true;
}
}
catch
{
Console.AddLine("invalid quad in object quad list");
}
}
}
catch
{
Console.AddLine("fails in foreach");
}
try
{
if ( bResetNeeded )
{
m_BaseQuad.RemoveObject( obj );
m_BaseQuad.AddObject( obj );
}
}
catch
{
Console.AddLine("fails in reset needed");
}
}
{
Console.AddLine("Unable to update a Quad " + Name);
Console.AddLine(d3de.ErrorString);
}
catch ( Exception e )
{
Console.AddLine("Unable to update a Quad " + Name);
Console.AddLine(e.Message);
}
}
此时,类的最后一个方法是 Dispose 方法,它如 列表 3-8 所示。我们在本类中没有分配任何东西是需要显式除掉的(disposed of),所以我们传递Dispose动作给每个存在的子方块。
列表 3.8:Quad Dispose 方法
public void Dispose()
{
if ( m_NorthEast != null ) m_NorthEast.Dispose();
if ( m_NorthWest != null ) m_NorthWest.Dispose();
if ( m_SouthWest != null ) m_SouthWest.Dispose();
if ( m_SouthEast != null ) m_SouthEast.Dispose();
}