SceneGraph(場景圖) 簡介
場景圖介紹
該節內容翻譯自gemedev的一篇文章 blog-SceneGraph Introduction。
什麼是場景圖
場景圖是一種將資料排序到層次結構中的方法,在層次結構中父節點影響子節點。你可能會說“這不是樹嗎?”你說得沒錯,場景圖就是一棵n-tree。也就是說,它可以有任意多的孩子。但是場景圖比一棵簡單的樹要複雜一些。它們表示在處理子物件之前要執行的某些操作。如果現在對這個概念不好理解,不用擔心,這一切都會在後面的內容中給出解釋。
為什麼場景圖有用
如果你還沒有發現為什麼場景圖如此酷,那麼讓我來解釋一下場景圖的一些細節。假設你需要在你的遊戲中模擬太陽系。這個系統裡面,在中心有一顆恆星,帶有兩顆行星。每個行星也有兩顆衛星。有兩種方式可以實現這個功能。 我們可以為太陽系中的每個物體建立一個複雜的行為函式,但是如果設計師想要改變行星的位置,那麼通過改變所有其他圍繞它旋轉的物體,就有很多工作要做。 另一個選擇是建立一個場景圖,讓我們的生活變得簡單。下圖顯示瞭如何建立場景圖來表示物件:
假設旋轉節點儲存當前世界矩陣,並將其與旋轉相乘。這將影響其後渲染的所有其他物件。所以有了這個場景圖,讓我們看看這個場景圖的邏輯流程。
- 繪製Star
- 儲存當前的矩陣(star)
- 執行旋轉(star)
- 繪製Planet 1
- 儲存當前的矩陣(planet1)
- 執行旋轉(planet1)
- 繪製Moon A
- 繪製Moon B
- 恢復儲存的矩陣(planet1)
- 繪製Planet2
- 儲存當前的矩陣(Planet2)
- 執行旋轉(Planet2)
- 繪製Moon C
- 繪製Moon D
- 恢復儲存的矩陣(Planet2)
- 恢復儲存的矩陣(star)
這是一個非常簡單的場景圖的實現,你也應該發現為什麼場景圖是一個值得擁有的東西。但你可能會對自己說,這很容易做到,只要硬編碼就可以了。場景圖的優勢在於場景圖的顯示方式可以不通過硬編碼的方式實現,雖然對於你能想象到的節點,比如旋轉,渲染等是硬編碼實現的。基於這些知識,我們可以將上面的場景變得更加複雜,let's do it。讓我們在太陽系中增加一些生命,讓1號行星稍微搖晃一下。是的,1號行星被一顆大小行星撞擊,現在正稍微偏離其軸旋轉。不用擔心,我們只需要建立一個抖動節點,並在繪製行星1之前設定它。
但是行星1的擺動對我來說還不夠真實,讓我們繼續這樣做,讓這兩顆行星以不同的速度旋轉。
現在,這個場景圖比最初呈現的要複雜得多,現在讓我們來看看程式的邏輯流程。
- 繪製Star
- 儲存當前的矩陣
- 應用旋轉
- 儲存當前的矩陣
- 應用抖動
- 繪製planet1
- 儲存當前的矩陣
- 應用旋轉
- 繪製Moon A
- 繪製Moon B
- 應用旋轉
- 恢復矩陣
- 儲存當前的矩陣
- 恢復矩陣
- 儲存當前的矩陣
- 應用旋轉
- 恢復矩陣
- 儲存當前的矩陣
- 應用旋轉
- 繪製planet2
- 儲存當前的矩陣
- 應用旋轉
- 繪製Moon C
- 繪製Moon D
- 恢復矩陣
- 應用旋轉
- 恢復矩陣
真的!現在這只是一個簡單的太陽系模型!想象一下,如果我們模仿這個級別的其他部分會發生什麼。
簡單實現示例
我認為這已經足夠對場景圖進行高層次的討論了,讓我們來談談我們將如何實現它們。為此,我們需要一個基類,以便從所有場景圖節點派生。
class CSceneNode
{
public:
// constructor
CSceneNode() { }
// destructor - calls destroy
virtual ~CSceneNode() { Destroy(); }
// release this object from memory
void Release() { delete this; }
// update our scene node
virtual void Update()
{
// loop through the list and update the children
for( std::list<CSceneNode*>::iterator i = m_lstChildren.begin();
i != m_lstChildren.end(); i++ )
{
(*i)->Update();
}
}
// destroy all the children
void Destroy()
{
for( std::list<CSceneNode*>::iterator i = m_lstChildren.begin();
i != m_lstChildren.end(); i++ )
(*i)->Release();
m_lstChildren.clear();
}
// add a child to our custody
void AddChild( CSceneNode* pNode )
{
m_lstChildren.push_back(pNode);
}
protected:
// list of children
std::list<CSceneNode*> m_lstChildren;
}
現在這已經超出了我們的方式,我們現在可以做一個我們享有的所有型別的節點的清單。這是我認為每個場景圖都應該具有的節點列表。當然,如果你覺得合適的話,你可以新增新的型別。
- Geometry Node
- DOF(下面會有解釋)
- Rotation(animated)
- Scaling(animated)
- Translating(animated)
- Animated DOF
- Switch
對於一個基本的場景圖引擎來說,這應該足夠了。你總是可以在你的引擎裡新增更多的東西,使它成為最好的新東西。
Geometry Node
會有一個沒有圖形的圖形引擎麼?這是不可能的。所以,現在介紹一下最重要的節點:
class CGeometryNode: public CSceneNode
{
public:
CGeometryNode() { }
~CGeometryNode() { }
void Update()
{
// Draw our geometry here!
CSceneNode::Update();
}
};
注意,上面的渲染程式碼上有點敷衍。你應該對於如何處理這個節點,是非常清楚的。先執行幾何體的渲染(或將其傳送到要渲染的位置),然後更新我們的子物件。
DOF
DOF節點通常稱為變換。它們只不過是一個表示偏移、旋轉或縮放的矩陣。如果不想將矩陣儲存在Geometry Node中,這些選項非常有用。在下一個示例中,我們假設使用OpenGL進行渲染。
class CDOFNode: public CSceneNode
{
public:
CDOFNode() { }
~CDOFNode() { }
void Initialize( float m[4][4] )
{
for( int i = 0; i < 4; i++ )
for( int j = 0; j < 4; j++ )
m_fvMatrix[i][j] = m[i][j];
}
void Update()
{
glPushMatrix();
glLoadMatrix( (float*)m_fvMatrix );
CSceneNode::Update();
glPopMatrix();
}
private:
float m_fvMatrix[4][4];
};
Switch Node
switch節點開始顯示一些可以使用場景圖執行的更復雜的操作。交換節點的作用就像鐵路上的一個交叉點,只允許您選擇以下路徑之一(可以將它們更改為沿著兩條路徑,但這將由讀者來完成)。讓我們看一幅場景圖,圖中有一個開關節點。
現在對於場景圖的這一部分,開關表示賽車遊戲中的車門。由於這輛車損壞了,我們想證明它正在損壞。當我們開始比賽時,我們希望賽車不會受到任何損壞,但隨著賽車在水平面上的前進,受到的損壞越來越多,我們需要將路徑切換到損壞更嚴重的車門上。我們甚至可以擴充套件這一範圍,使受損更嚴重的身體部位在產生煙霧效應後附著粒子系統。你的想象力限制了這種可能性。