优化模式--脏标记模式

理论要点

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

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

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

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

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

代码分析

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

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

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

好,如今咱们有了计算世界坐标的类了,接下来,咱们来定义游戏场景中的物体类。code

//每一个物体组成:网格(图元),坐标,它的子节点
class GraphNode
{
public:
    GraphNode(Mesh* mesh):_mesh(mesh), _local(Transform::origin()) {}

private:
    Transform _local;
    Mesh* _mesh;

    GraphNode* _children[MAX_CHILDREN];
    int _numChildren;
}

这样咱们游戏的场景其实能够看做是一个单一的根节点”GraphNode”对象,它的子节点(子子节点,等等)就是世界中的全部物体。orm

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);
    }
}

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

好,结束~

相关文章
相关标签/搜索