刚看了《3D Game Engine Architecture》第3章“Scene graphs and renderers"的前两节,很精彩,暂且不拿wild magic 3和其他引擎相比较,只是作为读书笔记,记录一下书中的核心内容。
我觉得第3章是此书的核心部分,全部内容就是scene graph的更新和渲染,其中第一节描述了wild magic3中的scene graph架构,第二节重点讲解了scene grapha的Geometirc State 更新体系(updateGS)。
1)wild magic3 scene graphs
包括几个核心类: Spatial, Node, Geometry
其中Spatial是Node和Geometry的父类,Spatial包含了local transform和world transform,以及world bound(世界空间的包围体)。并且Spatial拥有上一层Spatial的指针(parent spatial)。
Node包含一组子节点(注意,子节点使用Spatial指针,而不是Node指针,因为子节点可能是Node也可能是Geometry,或者他们的子类),通过Spatial和Node组成了scene 的树形结构。
而Geometry是有mesh的几何实体,包括索引数组,模型空间的顶点数组,模型空间的法线数组,以及模型空间的包围体,还有模型级的scale值。
同时他也是继承自Spatial的,所以也可以变换,也被放置到scene grapha中,但是在wild magic3中,Geometry只能作为叶子节点(没有子节点了,只有Node有子节点)
从这儿看,Node的作用就是一个group, Node不是实体,实体只能是叶子节点,一般是Node的子节点中有Geometry。
另外,对于共享模型数据,wild magic3是在low level实现的,即Geometry的子类可以共享模型数据。
2)wild magic3中的transform
Transformation 这个类,包含一个3X3 rotation matrix, 一个 vector3f translate,和一个vector3f表达的non-uniform scale。他为了减少计算量,没有像irrlicht那样直接用一个4X4 matrix。Transformation类中的Product(const Transformation& rkA, const Transformation& rkB)方法相当于矩阵相乘,用于在scene grapha更新时从上到下更新节点的世界变换。
3)wild magic3的geometric updates
为了方便看,把相关代码列到一起
class Spatial: public Object
{
public:
Transformation Local, World;
bool WorldIsCurrent;
BoundingVolumePtr WorldBound;
bool WorldBoundIsCurrent;
void UpdateGS(double dAppTime=-Mathd::MAX_REAL, bool bInitiator=true)
{
UpdateWorldData(dAppTime);
UpdateWorldBound();
if(bInitiator)
{
PropagateBoundToRoot();
}
}
void UpdateBS()
{
UpdateWorldBound();
PropagateBoundToRoot();
}
protected:
virtual void UpdateWorldData(double dAppTime)
{
UpdateControllers(dAppTime);//control可能直接修改local或world transform
if(!WorldIsCurrent)
{
if(m_pkParent)
World.Product(m_pkParent->World, Local);
else
World = Local;
}
}
virtual void UpdateWorldBound() = 0;
void PropagateBoundToRoot()
{
if(m_pkParent)
{
m_pkParent->UpdateWorldBound();
m_pkParent->PropagateBoundToRoot();
}
}
};
class Geometry: public Spatial
{
public:
//省略其他数据,如indices, vertices, normals..
BoundingVolumePtr ModelBound;
void UpdateMS();
protected:
virtual void UpdateModelBound();
virtual void UpdateModelNormals();
virtual void UpdateWorldBound()
{
ModelBound->TransformBy(World, WorldBound);
}
};
class Node: public Spatial
{
protected:
virtual void UpdateWorldData(double dAppTime)
{
Spatial::UpdateWorldData(dAppTimie);
for(int i=0; i
{
Spatial* pkChild = m_kChild[i];
if(pkChild)
pkChild->UpdateGS(dAppTime, false);
}
}
virtual void UpdateWorldBound()
{
if(!WorldBoundIsCurrent)
{
bool bFoundFirstBound = false;
for(int i=0; i
{
Sptial* pkChild = m_kChild[i];
if(pkChild)
{
if(bFoundFirstBound)
{
//Merge current world bound with child world bound
WorldBound->GrowToContain(pkChild->WorldBound);
}
else
{
//set world bound to first nonull child world bound
bFoundFirstBound = true;
WorldBound->CopyFrom(pkChild->WorldBound);
}
}
}
}
}
};
scene grapha的update主要做两件事情,一是从上到下更新world transform,二是从下往上更新world bound。其中包围体是要包括所有子节点的包围体的,而只有Geometry类型的节点需要计算自己的包围体(从顶点数据计算)。
wild magic3中,更新不是自动的,必须手工调用,而且要选择从哪一个节点开始更新,一般是某节点需要更新(比如local transform变了)并且他上面没有需要更新的父节点,那么就要调用该节点的UpdateGS,这样的节点有几个就调用几次UpdateGS。 UpdateGS里面通过遍历和递归,做了上面说的两件事情。UpdateGS的参数bInitiator 表明这个node是更新的起点,只有这个node的UpdateGS里面才会向上更新world bound volume直到root,而其他的node只会更新自己的world bound不会向上传播,这是为了提高效率。(因为这是在transform和world bound更新完成之后才调用的,所以只要更新的起点node向上更新包围体就足够了,下层的node没必要向上传播,否则也是被覆盖,浪费计算)
计算transform是在UpdateWorldData 里面做的,对于spatial的UpdateWorldData,主要就是把自己的local transform和parent的world transform级联起来,得到自己的world transform,但是这之前首先会使用controller进行更新,controller可能对transform系统产生影响也可能不影响,使用 controller时通过设置WorldIsCurrent来决定是否controller已经更新了world transform而不需要级联的更新方式。(比如skin controller)。而另外一些controller如普通key frame和IK,只是改变了local transform,还是需要使用级联的方式更新world transform的,他就不会设置WorldIsCurrent。另外一些controller没有影响到transform,也不会设置 WorldIsCurrent,比如morphe controller,但是他需要调用Geometry的UpdateMS来更新模型的一些数据(ModelBound).
Geometry没有override UpdateWorldData,所以和Spatial是一样的。
Node 的UpdateWorldData里面首先是直接调用了Spatial的UpdateWorldData来更新自己的world transform,然后对于他的所有子节点(Spitial,可能是Node或Geometry)遍历调用UpdateGS(bInitiator参数为false,因为子节点肯定不需要传播bound更新到root)。这就形成了UpdateGS的递归调用。这是一个深度优先的树遍历。树的每一层都是先计算好自己的transform然后让子节点去UpdateGS,所有子节点的调用都完成后才会计算自己的world bound,最终所有层次的UpdateGS都执行完毕,回到调用的起点节点那儿,接着执行那个节点的UpdateWorldBound。因为起始调用的节点的bInitiator是true,所以会执行PropagateBoundToRoot,向上更新world bound直到root。
更新UpdateWorldBound在spatial里面是个纯虚函数,Geometry的实现就是使用world transform变换model bound得到world bound,而Node里面则是计算一个包含所有子节点的world bound的总包围体。其中WorldBoundIsCurrent的作用是,当某个node的world bound可以通过其他途径得到时避开正常的计算流程,比如一个房间是固定的可以预先计算好一个world bound不再改变,不管其中的子节点怎么动怎么变都不再计算这个房间的world bound了。
整个过程就是这样的:
三个类(Spatial, Node, Geomotry)
三个主要函数(UpdateWorldData,UpdateWorldBound,以及PropagateBoundToRoot)
三个重要标志的设置(UpdateGS的bInitiator参数,Spatial的成员WorldIsCurrent和WorldBoundIsCurrent)
node/spatial的遍历,UpdateGS的递归
完成了world transform和world bound的更新。
另外UpdateBS是另外一个公开的接口,当model bound变化时,而transform没变化时,就只要调用UpdateBS就可以了。