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节点开始显示一些可以使用场景图执行的更复杂的操作。交换节点的作用就像铁路上的一个交叉点,只允许您选择以下路径之一(可以将它们更改为沿着两条路径,但这将由读者来完成)。让我们看一幅场景图,图中有一个开关节点。
现在对于场景图的这一部分,开关表示赛车游戏中的车门。由于这辆车损坏了,我们想证明它正在损坏。当我们开始比赛时,我们希望赛车不会受到任何损坏,但随着赛车在水平面上的前进,受到的损坏越来越多,我们需要将路径切换到损坏更严重的车门上。我们甚至可以扩展这一范围,使受损更严重的身体部位在产生烟雾效应后附着粒子系统。你的想象力限制了这种可能性。
作者: grassofsky
出处: http://www.cnblogs.com/grass-and-moon
本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可邮件(grass-of-sky@163.com)咨询.
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步