• 优化模式--脏标记模式


    理论要点

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

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

    • 使用场合
      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);
        }
    }

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

    好,结束~

  • 相关阅读:
    CI框架主题切换的功能
    centos7 编译安装 php7.4
    单用户登陆demo-后者挤到前者,类似QQ
    nginx 负载均衡的配置
    PHP计算每月几周,每周的开始结束日期
    Centos7 编译安装PHP7
    TP 3.2.3 接入PHPMailer
    外部js引用vue实例环境的方式
    linux常用命令
    计算机中的二级制
  • 原文地址:https://www.cnblogs.com/cxx-blogs/p/9159250.html
Copyright © 2020-2023  润新知