-
说明:
- 1、以前学习过Android UI 布局
- 2、使用Java编写过动画效果demo
- 学习之前:我之前简单的认为,场景、层、精灵在图画绘制的关系就是由大到小的包含布局关系,并没有深入地了解这三者从无到有的绘制过程。
- 学习目的:理解cocos2d-x如何使用OpenGL来渲染成像的。
参考博客:
OpenGL–3D世界(视图变换,模型变换,投影变换,视口变换)
http://blog.csdn.net/u010223072/article/details/44620871
OpenGL 的渲染流水线
http://blog.csdn.net/pizi0475/article/details/50605972
Cocos场景遍历与渲染
http://blog.csdn.net/wlk1229/article/details/55005282
渲染流水线
http://www.cnblogs.com/suimeng/p/4655834.html
cocos2dx的渲染机制
http://blog.csdn.net/han1558249222/article/details/48093931
贴图、纹理、材质的区别
https://www.zhihu.com/question/25745472/answer/31462038
cocos2d-x3.2渲染机制的优化
http://blog.csdn.net/mydreamremindme/article/details/47091601
浅谈Draw Call和Batch的区别
http://blog.csdn.net/molti/article/details/42495993
参考书籍:《Cocos2d-x高级开发教程》
基础知识:
1、导演director、场景scene、层layer、精灵sprite 的基本概念和继承关系树。
http://www.cocos.com/docs/native/
2、世界坐标系、绘图坐标系、模型坐标系。
说说我现在的理解,世界坐标系是不动的,在cocos2d-x里面,世界坐标系的原点是在可视屏幕的左下角,x轴往右,Y轴往上。绘图坐标系顾名思义,是OpenGL绘制图形的时候使用的坐标系,cocos2d-x也参照了OpenGL,套用上了这个绘图坐标系,在没有设计任何坐标变换时(绘图坐标系具有可变性),绘图坐标系和世界坐标系的原点相同。模型坐标系,模型坐标系就是以纹理自身的锚点作为坐标系的原点,X轴和Y轴方向与世界坐标系相同。
3、坐标系变换及坐标系变换矩阵。
坐标系的变换,主要的关键再与绘图坐标系与世界坐标系的关系。前面说到绘图坐标系是具备可变性的,它的原点和X轴Y轴的方向是可变,但纹理在绘图坐标系中的坐标是不会改变的。比如绘制一个精灵,通过平移200PX,那么就会通过当前绘图坐标系原点所在的世界坐标系的值,通过平移矩阵的变换,得到绘图坐标系原点新的世界坐标系的值,然后再绘制这个精灵。变换矩阵通过《Cocos2d-x高级开发教程》中学习。
4、节点层级
节点是场景、层和精灵共同的父类,在这里将它们统称节点。我把节点分为组合节点和独立节点。组合节点由“自身节点+组合节点or独立节点”构成,是一个树的结构。
从生活体验可以知道,画家在绘制同一幅画时,都是有先后顺序的,OpenGL的绘制也是如此。这个优先顺序称为节点层级。在组合节点的子节点中,节点层级通过三个值globalZOrder、localZOrder、orderOfArrival 进行比较,都是值越大越靠前。三个值的对层级影响的优先顺序为globalZOrder、localZOrder、orderOfArrival。当globalZOrder的值大时,则会忽略localZOrder和orderOfArrival,globalZOrder值大则排在前面。我们发现了一个词叫ZOrder,Z轴坐标,由于OpenGL绘制成像是有过程的,cocos2d-x中,通过提供ZOrder属性,来对图形的绘制顺序形象立体化,来应对组合节点的绘制过程以及图像的重叠等问题。
orderOfArrival
orderOfArrival是节点添加到父节点时会被设置,
child->setOrderOfArrival(s_globalOrderOfArrival++)。
所有节点的orderOfArriva都是通过 s_ globalOrderOfArrival值累加得到的。s_ globalOrderOfArrival是Node节点的一个静态变量,初始值为1。
int Node::s_globalOrderOfArrival = 1;
每次有调用addChild函数,则该值会加1,所以所有节点的该值都不一样。
localZOrder
localZOrder在节点的构造函数中设置为0,默认值为0。可以通过函数
voidNode::_setLocalZOrder(intz)设置。
localZOrder和orderOfArrival都只是在遍历子节点时排序使用,排序比较时会先比价LocalZOrder的值,如过不相等则返回相应的比较结果,如果相等则会比较orderOfArrival的值。因为两个值都只是用在子节点排序上,所以localZOrder和orderOfArrival只会影响子节点之间的排序,所以如果两个节点的父节点不同则无法通过localZOrder和orderOfArrival控制层级。
对于子节点和父节点的遍历顺序:
LocalZOrder小于0的子节点>父节点自身>LocalZOrder大于等于0的子节点。
所以LocalZOrder小于0会绘制在父节点下面。
globalZOrder
globalZOrder在节点的构造函数中设置为0,默认值为0。
globalZOrder可以通过函数voidNode::setGlobalZOrder(floatglobalZOrder)设置。
globalZOrder是在场景绘制命令生成后,会根据globalZOrder对节点的绘制命令排序。所以globalZOrder是对场景全局节点的层级进行控制的,不管节点的父节点是谁,只要globalZOrder的值大,则绘制会更靠前。globalZOrder只是控制一个场景的节点,不可跨场景。
5、渲染树结构
编程中有一个结构叫做递归。渲染树结构就是利用了递归的结构。在上文中提及,Node节点是父类,在Node类中定义了两个重要的方法,visit 和 draw 方法,其中draw方法在cocos2d-x 3.x版本之后,定义为虚函数,draw方法就是绘制节点自身的函数。在Node类中,将draw方法的调用放到了visit的方法体中,通过调用visit方法来调用draw。visit方法,顾名思义,探望探亲,通过visit方法来寻找与自身节点相关的子节点。visit方法则使用了函数的递归结构。visit方法中,首先判断是不是组合节点,如果是组合节点,那么会根据其子节点的节点层级进行一次排序,根据排序的结果,优先渲染localZOrder小于零的子节点,再渲染localZOrder大于零的子节点。在渲染子节点时,会调用子节点的visit方法,继续查找是不是组合节点;如果发现不是组合节点,则执行此独立节点的Draw方法,进行绘制。如此类推,整个cocos图像成型的体系就像一个树一样。
6、cocos2d-x 3.2 渲染优化机制。
从第五点可知,我们要绘制一个完整的cocos图像,就要绘制里面每一个节点,这样会给GPU带来很大的负荷。从cocos2d-x 3.2 开始,对Node类中的draw方法进行了优化改动,不再在draw方法中直接调用OpenGL的绘图函数。而是新增加了一个Render渲染类,通过Render类中的函数来调用OpenGL的渲染函数,Render类对象作为visit函数和draw函数的一个参数进行传递。当这一帧中所有的Node对象都被visit遍历过之后,再按照RenderQueue队列对图形进行渲染。
void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
// quick return if not visible. children won't be drawn.
if (!_visible)
{
return;
}
uint32_t flags = processParentFlags(parentTransform, parentFlags);
// IMPORTANT:
// To ease the migration to v3.0, we still support the Mat4 stack,
// but it is deprecated and your code should not rely on it
_director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
_director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);
bool visibleByCamera = isVisitableByVisitingCamera();
int i = 0;
if(!_children.empty())
{
sortAllChildren();//根据节点层级排序
// draw children zOrder < 0
for( ; i < _children.size(); i++ )
{
auto node = _children.at(i);
if (node && node->_localZOrder < 0)//小于零的字节点
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
// self draw
//渲染自身,只有是在渲染自身的时候,才调用draw方法,子节点都是通过调用visit方法来调用draw
if (visibleByCamera)
this->draw(renderer, _modelViewTransform, flags);
for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)//大于零
(*it)->visit(renderer, _modelViewTransform, flags);
}
else if (visibleByCamera)
{
this->draw(renderer, _modelViewTransform, flags);
}
_director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
// FIX ME: Why need to set _orderOfArrival to 0??
// Please refer to https://github.com/cocos2d/cocos2d-x/pull/6920
// reset for next frame
// _orderOfArrival = 0;
}
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
#if CC_USE_CULLING
// Don't do calculate the culling if the transform was not updated
_insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
if(_insideBounds)
#endif
{
_trianglesCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, _polyInfo.triangles, transform, flags);
renderer->addCommand(&_trianglesCommand);
}
}
在draw方法中,Render类的实例对象调用了addCommand方法,这个方法是因为Render渲染器具备一个RenderQueue,addCommand将渲染该节点的任务Command添加到队列中。
渲染命令RenderCommand分成以下几种:
TrianglesComman,
QuadCommand,
MeshCommand,
GroupCommand,
BatchCommand,
PrimitiveCommand精灵的绘制主要用QuadCommand这种渲染命令。
Render类中定义了一个drawBatchQuads的方法,下面是方法内部分代码:
void Renderer::drawBatchedQuads(){}
//Start drawing vertices in batch
for(const auto& cmd : _batchQuadCommands)
{
bool commandQueued = true;
auto newMaterialID = cmd->getMaterialID();
if(_lastMaterialID != newMaterialID || newMaterialID == MATERIAL_ID_DO_NOT_BATCH)
{
// flush buffer
if(indexToDraw > 0)
{
glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) );
_drawnBatches++;
_drawnVertices += indexToDraw;
startIndex += indexToDraw;
indexToDraw = 0;
}
//Use new material
_lastMaterialID = newMaterialID;
cmd->useMaterial();
}
if (commandQueued)
{
indexToDraw += cmd->getQuadCount() * 6;
}
}
重点看_materialID 的构成。
void QuadCommand::generateMaterialID()
{
_skipBatching = false;
if(_glProgramState->getUniformCount() == 0)
{
int glProgram = (int)_glProgramState->getGLProgram()->getProgram();
int intArray[4] = { glProgram, (int)_textureID, (int)_blendType.src, (int)_blendType.dst};
_materialID = XXH32((const void*)intArray, sizeof(intArray), 0);
}
else
{
_materialID = Renderer::MATERIAL_ID_DO_NOT_BATCH;
_skipBatching = true;
}
}
OpenGL全写Open Graphics Library,开发的绘制图形库。OpenGL被大家俗称为状态机,也就是具备很多个开关的机器(关于OpenGL详情可查看OpenGL渲染流水线)。我们绘制不同的图像时打开的开关种类和数量不同,那么如果我们绘制的图像基本相同,并且打开开光的种类和数量相同,是不是就可以把多个图像一起绘制?
_materialID 就是作为这样的一个判断,如果 当前加入RenderQueue的精灵具备与上一个加入RenderQueue的精灵相同的 _materialID,那么就可以将这两个精灵结合在一起,给Render传入两个精灵的顶点,让它一起绘制,从而节省了一次调用GPU渲染的过程。
渲染优化机制应用
cocos2d-x游戏开发中,一般使用plist文件将多个图片集合在一起,plist文件中的所有图片都是具备同一种纹理的,也就是可以说 它们的_materialID 都是相同的。那么就可以如下进行,绘制多个图片,传递多个图片的顶点数据,最后只调用一次GPU。
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("close.plist");
for(int i=0;i<100;i++){
Sprite* sprite1 = Sprite::createWithSpriteFrameName("CloseNormal.png");
Sprite* sprite2 = Sprite::createWithSpriteFrameName("CloseSelected.png");
Sprite* spriteN = Sprite::createWithSpriteFrameName("name.png");
sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+sprite1->getContentSize().height/2*(i%10));
sprite2->setPosition(origin.x+sprite2->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-sprite2->getContentSize().height/2*(i%10));
spriteN->setPosition(origin.x+spriteN->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-spriteN->getContentSize().height/2*(i%10));
this->addChild(spriteN);
this->addChild(sprite2);
this->addChild(sprite1);
}
不过由于渲染机制的Queue的队列方式,只能够通过最外层的RenderCommand的_lastMaterialID 进行对比,所以,局限性是被渲染树的结构所限制。
本文深入探讨Cocos2d-x中场景、层、精灵的关系,解析坐标系变换、节点层级与渲染树结构。通过3.2版本的渲染优化机制,理解如何减少GPU负荷,提高性能,包括Batch渲染和RenderQueue的使用。

3311

被折叠的 条评论
为什么被折叠?



