什麼是髒標記模式:將工做推遲到必要時進行以免沒必要要的工做。就是用一個標誌位來標記內容是否發生變化,若是沒有發生變化就直接使用緩存數據,不須要從新計算。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);
}
}
這樣修改一個節點的局部變換隻是幾條賦值語句,渲染世界時只計算了自上一幀以來最少的變更的世界變換。
好,結束~