場景圖介紹
該節內容翻譯自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節點開始顯示一些可以使用場景圖執行的更復雜的操作。交換節點的作用就像鐵路上的一個交叉點,只允許您選擇以下路徑之一(可以將它們更改為沿着兩條路徑,但這將由讀者來完成)。讓我們看一幅場景圖,圖中有一個開關節點。
現在對於場景圖的這一部分,開關表示賽車游戲中的車門。由於這輛車損壞了,我們想證明它正在損壞。當我們開始比賽時,我們希望賽車不會受到任何損壞,但隨着賽車在水平面上的前進,受到的損壞越來越多,我們需要將路徑切換到損壞更嚴重的車門上。我們甚至可以擴展這一范圍,使受損更嚴重的身體部位在產生煙霧效應后附着粒子系統。你的想象力限制了這種可能性。