1. 程式人生 > 實用技巧 >SceneGraph(場景圖) 簡介

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節點開始顯示一些可以使用場景圖執行的更復雜的操作。交換節點的作用就像鐵路上的一個交叉點,只允許您選擇以下路徑之一(可以將它們更改為沿著兩條路徑,但這將由讀者來完成)。讓我們看一幅場景圖,圖中有一個開關節點。

現在對於場景圖的這一部分,開關表示賽車遊戲中的車門。由於這輛車損壞了,我們想證明它正在損壞。當我們開始比賽時,我們希望賽車不會受到任何損壞,但隨著賽車在水平面上的前進,受到的損壞越來越多,我們需要將路徑切換到損壞更嚴重的車門上。我們甚至可以擴充套件這一範圍,使受損更嚴重的身體部位在產生煙霧效應後附著粒子系統。你的想象力限制了這種可能性。