优化模式--脏标记模式

来源:互联网 发布:cucumber java 编辑:程序博客网 时间:2024/06/06 03:09

理论要点

  • 什么是脏标记模式:将工作推迟到必要时进行以避免不必要的工作。就是用一个标志位来标记内容是否发生变化,如果没有发生变化就直接使用缓存数据,不需要重新计算。

  • 要点
    脏标识模式:当前有一组原始数据随着时间变化而改变。由这些原始数据计算出目标数据需要耗费一定的计算量。这个时候,可以用一个脏标识,来追踪目前的原始数据是否与之前的原始数据保持一致,而此脏标识会在被标记的原始数据改变时改变。那么,若这个标记没被改变,就可以使用之前缓存的目标数据,不用再重复计算。反之,若此标记已经改变,则需用新的原始数据计算目标数据。

  • 使用场合
    1,原始数据转换到目标数据会消耗很多时间,都可以考虑使用脏标记模式来节省开销。

    2,游戏中物体局部变换到世界变换的计算,当没有变化时不需要每帧重复计算。(从根节点沿着它的父链将变换组合起来,矩阵相乘=世界变换)。还有游戏场景图中每帧渲染的对象,对于没有发生变化的对象可以不必重新渲染。再有我们的文档存档也可以用到,内存中就是我们的原始数据,存盘到磁盘就是我们的目标数据,当然不需要实时存盘。

    3,若原始数据的变化速度远高于目标数据的使用速度,此时数据会因为随后的修改而失效,此时就不适合使用脏标记模式。

代码分析

1,就如上面提到的,游戏场景中,物体运动并渲染需要知道它的世界坐标,这就意味着我们需要计算场景中所有对象的世界变换。很多对象都有很深的父链,父节点运动其上子节点也跟着变化,如果每个对象都每帧重新计算世界变换,这种开销也是很恐怖的。
下面我们就来分析怎么用脏标记模式来避免这种重复计算:
首先,局部坐标到世界坐标换算的矩阵计算不在我们这里的讨论范围,我们假设它的实现在其他什么地方。

class Transform{public:    //原始变换,单位矩阵表示没有移动、旋转或者缩放    static Transform origin();    //组合父链中所有的局部变换得到它的世界变换    Transform combine(Transform& other);}

再来一个世界变换换算过程的示意图帮助理解计算过程,如下:
这里写图片描述
这里写图片描述

好,现在我们有了计算世界坐标的类了,接下来,我们来定义游戏场景中的物体类。

//每个物体组成:网格(图元),坐标,它的子节点class GraphNode{public:    GraphNode(Mesh* mesh):_mesh(mesh), _local(Transform::origin()) {}private:    Transform _local;    Mesh* _mesh;    GraphNode* _children[MAX_CHILDREN];    int _numChildren;}

这样我们游戏的场景其实可以看作是一个单一的根节点”GraphNode”对象,它的子节点(子子节点,等等)就是世界中的所有物体。

GraphNode* _graphRoot = new GraphNode(NULL);//Add children to root graph node...//往这个节点树中添加子节点,即就形成了我们的场景图(与cocos节点树不谋而合)

渲染整个场景,其实就是遍历节点树,从根节点开始,通过正确的世界变换为每个节点图元调用下面的方法。

void renderMesh(Mesh* mesh, Transform transform);

我们这里不实现它,目的只是了解游戏场景形成的大概流程。现在我们的主要精力是看在遍历这个节点树计算世界变换并调用renderMesh最终渲染这个过程中是怎么优化的。
老套路,先来看不优化最直接的实现方式:

void GraphNode::render(Transform parentWorld){    Transform world = _local.combine(parentWorld)    if(_mesh) renderMesh(_mesh, world);    for(int i = 0; i < _numChildren; i++)    {        _children[i]->render(world);    }}

我们通过“parentWorld”将父节点的世界变换传给它。这样这个节点的世界变换就是它本身的局部变换_local与parentWorld组合了。我们不需要回溯到父节点去重新计算,因为我们沿着父链下来已经计算过了。
我们计算节点的世界变换并保存到world中,然后如果有图元的话,就渲染它。最后我们递归进入子节点中,将当前节点的世界变换传递进去。总之,这是一个紧凑、简单的递归调用。
为了绘制整个场景图,我们从空根节点开始渲染:

_graphRoot ->render(Transform::origin());

分析下,我们上面是正确的实现了场景图的渲染,但是它并不高效,它每帧都在每个节点上调用_local.combine(parentWorld)计算世界变换。

2,下面就来看看怎么用脏标记来优化这个计算。首先我们需要添加两个成员到GraphNode类中。

class GraphNode{public:    GraphNode(Mesh* mesh)    :_mesh(mesh),     _local(Transform::origin()),     _dirty(true)    {}    //Other methods...private:    Transform _local;    Mesh* _mesh;    //添加的两个成员    Transform _world;  //缓存上次计算的世界变换    bool _dirty;       //脏标记    GraphNode* _children[MAX_CHILDREN];    int _numChildren;}

在物体移动,发生局部变换时,我们需要设置脏标记。

//设置脏标记void GraphNode::setTransform(Transform local){    _local = local;    _dirty = true;}

这样之后我们再来看看优化后的每帧渲染接口:

void GraphNode::render(Transform parentWorld, bool dirty){    dirty |= _dirty;    if(dirty)    {        //清除脏标记        _world = _local.combine(parentWorld);        _dirty = false;    }    if(_mesh) renderMesh(_mesh, _world);    //父节点变化,递归子节点    for(int i = 0; i < _numChildren; i++)    {        _children[i]->render(_world, dirty);    }}

这样修改一个节点的局部变换只是几条赋值语句,渲染世界时只计算了自上一帧以来最少的变动的世界变换。

好,结束~

原创粉丝点击