剛看了《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就可以了。