cocos中的UI节点Node成树形结构。以Scene对象为根节点,树中每个节点都是Node对象。每个对象有一个children集合和parent节点,Scene的parent为空。
一、UI树结构
class Node {
void addChild(Node * child);
void addChild(Node * child, int localZOrder);
void addChild(Node* child, int localZOrder, int tag);
void addChild(Node* child, int localZOrder, const std::string &name);
void setParent(Node* parent);
Node* getParent() { return _parent; } // 1个父节点
Node* getChildByTag(int tag) const;
Node* getChildByName(const std::string& name) const;
ssize_t getChildrenCount() const;
Vector<Node*>& getChildren() { return _children; } // 多个子节点
}
二、UI树遍历
渲染系统最重要的职责就是遍历UI树,然后将元素绘制到屏幕中。
UI树的遍历有两个重要目的:
- 决定绘制树元素绘制顺序
- 元素的模型视图变换矩阵计算,结果供OpenGL ES渲染管线计算顶点位置。
3D游戏是根据模型顶点的Z坐标值经过投影变换之后得到深度值,然后通过深度测试(Depth Test)来先后进行绘制。2D游戏顶点Z坐标都0,也就是说没有深度(前后)的概念,后调用的绘制会覆盖前调用的绘制内容。
因此我们需要自己设计一个深度值(我们叫它“逻辑深度”)来决定这些节点的绘制顺序,Node类中的localZOrder就是元素的“逻辑深度”。UI树采用中序遍历的深度优先算法进行遍历,具体如下:
点击查看【processon】
遍历源码:
Node::visit(......){
......
if(!_children.empty())
{
// *******************************************
// ***** 第一步,排序子节点,按localZOrder升序
// *******************************************
sortAllChildren();
// *******************************************
// ***** 第二步,绘制localZOrder < 0的子节点,即上图的“左子节点”
// *******************************************
for(auto size = _children.size(); i < size; ++i)
{
auto node = _children.at(i);
if (node && node->_localZOrder < 0)
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
// *******************************************
// ***** 第三步,绘制自己
// *******************************************
if (visibleByCamera)
// 见下面解释,我们以Sprite的draw为例
this->draw(renderer, _modelViewTransform, flags);
// *******************************************
// ***** 第四步,绘制localZOrder >=0的子节点,即上图的“右子节点”
// *******************************************
for(auto it=_children.cbegin()+i, itCend = _children.cend(); it != itCend; ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
......
}
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
if (_texture == nullptr)
{
return;
}
#if CC_USE_CULLING
...... // 如果出现在出现在屏幕外面,就不会生成绘制指令,叫auto culling
if(_insideBounds)
#endif
{
// 绘制精灵用的是TrianglesCommand。
_trianglesCommand.init(_globalZOrder,
_texture,
getGLProgramState(),
_blendFunc,
_polyInfo.triangles,
transform,
flags);
// 并没有执行绘制,而是加入了绘制栈中,后续统一执行
renderer->addCommand(&_trianglesCommand);
}
通过“逻辑深度”,我们可以指定同级元素的绘制顺序,即node节点与其直接子节点的绘制顺序,但是无法指定不同级元素的绘制顺序,比如让一个低层级的元素始终可见,当然我们可以将元素添加到满足需求的节点上,显然这就失去了灵活性。
cocos通过globalZOrder解决这个问题,新的绘制顺序逻辑如下:
- 1、globalZOrder默认0。
- 2、globalZOrder!=0,按globalZOrder进行排序。
- 3、globalZOrder==0,按localZOrder进行排序。
但不能对SpriteBatchNode的子元素分别设置globalZOrder,因为这些元素会组织成一个BatchCommand,已经无需用到ZOrder了。
三、模型视图变换矩阵
每个节点维护了一个模型视图变换矩阵_modelViewTransform,它通过传入的父级变换矩阵parentTransform右乘节点在本地坐标系中的变换矩阵得到。
节点的变换矩阵和节点的相对坐标传入OpenGL ES渲染管线中,然后在管线中对相对位置执行该坐标变换。
相关源码:
void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags){
// UI树遍历时,传入了父节点的变换矩阵parentTransform
......
uint32_t flags = processParentFlags(parentTransform, parentFlags);
......
}
uint32_t Node::processParentFlags(const Mat4& parentTransform, uint32_t parentFlags){
......
if(flags & FLAGS_DIRTY_MASK) // 某些元素的位置发生变化,如自身位置
// 或者父链上的某个元素位置发生变化。
// 总之是会影响到当前节点位置的节点的位置发生变动时,
// 才进行计算,可以提升遍历性能。
_modelViewTransform = this->transform(parentTransform);
return flags;
}
Mat4 Node::transform(const Mat4& parentTransform)
{
// 父节点模型视图变换矩阵 右乘 节点在本地坐标系中的变换矩阵 = 节点的模型视图变换矩阵。
return parentTransform * this->getNodeToParentTransform();
}