优化模式

阅读: 评论:0

优化模式

优化模式

理论要点

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

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

  • 使用场合
    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 
//往这个节点树中添加子节点,即就形成了我们的场景图(与cocos节点树不谋而合)

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

void renderMesh(Mesh* mesh, Transform transform);

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

void GraphNode::render(Transform parentWorld)
{Transform world = _localbine(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());

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

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

class GraphNode
{
public:GraphNode(Mesh* mesh):_mesh(mesh),_local(Transform::origin()),_dirty(true){}//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 = _localbine(parentWorld);_dirty = false;}if(_mesh) renderMesh(_mesh, _world);//父节点变化,递归子节点for(int i = 0; i < _numChildren; i++){_children[i]->render(_world, dirty);}
}

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

好,结束~

本文发布于:2024-02-04 07:50:54,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170702378953676.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:模式
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23