【Cocos2d-x】3.0渲染架构原理分析

来源:互联网 发布:网赌用什么软件 编辑:程序博客网 时间:2024/05/06 07:42

Cocos2d-x 3.0的性能优化

《程序员》:较之上一个版本,Cocos2d-x 3.0在哪些方面做了改进?

王哲:在Cocos2d-x 3.0中,我们着重改进一些很基础的工作,从之前发布的Alpha版本来看,大家最关心的主要围绕以下三点:性能、兼容性(尤其是Android手机的兼容性)、CPU和内存消耗。在性能方面,我们优化最好的是自动技术、当一个游戏场景大出手机屏幕很多时,引擎会自动帮你把屏幕之外的东西给剔除掉,这使大场景游戏的流畅度有2~8倍的提升。另一个是模拟合并,从技术支持的反馈来看,有50%的开发者不懂得如何使用这个功能,而在3.0中,该功能可以自己判断是否开启,并且性能已非常接近与手动调试。在提升兼容性方面,我们测试了200款Android机型,较之2.2版本的85.71%,3.0版本的兼容性达到了90.71%,而在国外,一些引擎的兼容性只能达到三分之一左右。至于CPU和内存消耗,3.0比2.2版本的启动时间缩短了25%,CPU的平均占用率也降低了36%。

原文链接:

http://blog.csdn.net/zhangshuliai/article/details/25740343#comments

3.0之前是visit后就draw,而draw是真正的OpenGL操作。也就是说,每访问一个对象,先计算节点的渲染数据,然后马上渲染。因为节点是按树形结构组织的,如果两个父节点p1和p2的深度分别是10和20,c1是p1的子节点,其深度是30,c2是p2的子节点,其深度是5。则渲染顺序是p1、c1、c2、p2。

3.0也是在visit后就draw,但是draw并不进行OpenGL操作。3.0抽象了一个RenderCommand,在draw的时候其实是生成一个渲染命令,渲染命令其实就是对渲染所需要的数据的封装。RenderCommand作为基类,只包含了两个成员,一个是命令类型_type,这个很必要,正是靠这个来获取子类对象的具体类型的,这里没有用运行时类型,应该是考虑到效率;另一个是z深度_globalOrder,这个也很必要,渲染的时候必然要对节点排序,而z序是唯一的依据。

3.0包含了以下几个RenderCommand的子类:

1,CustomCommand:

顾名思义,是客户自定义的。它没有过多的数据,只有一个std::function<>类型成员func,这个是在节点draw创建CustomCommand的时候传入的参数,而在真正渲染的时候调用的正是这个函数。比如Label,它的draw函数体为:

void Label::draw()

{

    _customCommand.init(_globalOrder);

   _customCommand.func=CC_CALLBACK_0(Label::onDraw, this,transform,transformUpdated);

   render->addCommand(&_customCommand);

}

可见,真正的渲染是在onDraw回调函数中。onDraw的操作和此前版本的draw函数相差不大,都是设置混合方式,使用GLProgram,更新shader的变换矩阵等shader所用到的uniform变量,更新子节点位置(注:因为是批量渲染,这个和此前的CCSpriteBatchNode很相近),调用TextureAtlas::drawQuads();

使用CustomCommand还有Layer,LableAtlas,LabelBMFont,此外还有RenderTexture,这个比较特殊,下面会说。

注:估计作者是对所有不太好抽象的渲染节点,就干脆做了一个CustomCommand。

2,QuadCommand:

和CustomCommand不同,这个类包含了很多属性:纹理_textureID,Program _shader,混合函数_blendType,定点数据_quad,模型视图矩阵_mv,还有一个材质属性_materialID。材质是一种抽象,当且仅当纹理、shader、混合函数都相等的时候,材质才相等。抽象材质的好处是,如果两个对象渲染顺序相邻,且材质相同,就可以使用批量渲染。当然,两个对象必须都使用QuadCommand,因为只有QuadCommand才有材质属性。

QuadCommand的最终渲染操作放在了Render::drawBatchQuads中,其输入是一个所有QuadCommand的顶点排序后组装成的VBO,一个是这些定点对应的QuadCommand命令数组。查看这个函数也的确是当材质发生变化的时候才进行一次渲染,可见它是把相同材质的放到了一起批量渲染。

值得一提的是3.0主要是对QuadCommand进行了优化,实际上游戏中也大多数是这种情况。在Render::render()函数中,如果当前是QuadCommand,则顶点和Command放入数组,如果是其它,则先渲染此前的QuadCommand数组,然后再渲染当前命令。

使用CustomCommand命令的类有Sprite,ParticleSystemQuad,而Sprite应该是游戏中最常用的类,所以3.0的优化还是很有意义的。

3,BatchCommand:

看名字就知道,BatchCommand是CCSpriteBatchNode的改进,而事实上,也的确是CCSpriteBatchNode和CCPaticleBatchNode使用了BatchCommand。BatchCommand的属性和QuadCommand很相似,不过没有了顶点数据_quad,变成了_textureAtlas,其渲染函数也和此前的差别不大,都是材质三剑客(shader, texutre, blend),然后是更新shader的uniform变量,最后调用_textureAtlas->drawQuads(),和Label的渲染很相似,只不过Label的更新子节点的顶点放在了渲染函数中,而BatchCommdand放在了节点的visit中,具体可参加源代码。

4,GroupCommand:

这是一个Command组合,但是GroupCommand只有一个属性_renderQueueID,而GroupCommand的所有子Command其实存放在Render中。GroupCommand像其它Command一样,存放在同一个队列中,遍历的时候,如果是GroupCommand,则获取其_renderQueueID,然后根据_renderQueueID找到指定的RenderCommand队列,进而渲染这个队列。可见,在Render中,可以包含多个Command队列,一些是根节点队列(没有父Command),其它的是指定ID的GroupCommand的子命令队列。

使用GroupCommand的主要有RenderTexture,因为RenderTexture包含了两次分隔的渲染操作,一次是begin(),一次是end()。

引擎只提供了四种RenderCommand,用的最广泛的是QuadCommand。这些远远不够,比如没有支持多重纹理。

如果我想在Sprite上加入一个多重纹理的功能,应该怎么办?是扩展QuadCommand还重新创建一个新的RenderCommand?


原文链接:

http://dualface.github.io/blog/2013/08/02/cocos2dx-v3-rendering-pipeline/

8月1号这一天,cocos2d-x 官方微博上说 x 团队正在纠结是否用智能指针替换现有的引用计数内存管理机制,结果引发大家的争论。

在我看来,引发这个争论的原因是 cocos2d-x 打算实现多线程化,从而充分利用现代智能手机的多核处理器资源。而多线程化的一个最重大需求,应该就是 cocos2d-x 3.0 新的渲染架构了。

cocos2d-x 3.0 的渲染架构设计来自 Zynga(原始设计文档,需翻墙)。在这个架构设计中,游戏将分为两个主要的线程:main thread 和 draw thread。

  • main thread 运行游戏的逻辑代码,例如在场景的 update() 函数中改变角色位置、在 schedule selector 函数中创建新敌人等等。

    main thread 中的代码会改变 Scene 中的 Node Tree,但并不实际执行 OpenGL 渲染操作。作为替代,main thread 将绘图操作转换为一系列的 draw command,发送到一个 queue 中。

  • draw thread 则在每一帧绘制前,从 queue 里提取 draw command,然后进行优化,再转换为实际的 OpenGL 渲染操作。

在单线程架构中,游戏逻辑和渲染操作是顺序执行的。两者消耗的时间加起来如果超过 1/60 秒,就会导致游戏出现卡顿现象。

分成多线程后,main thread 有更多的时间来执行游戏逻辑。而 draw thread 也有更多的时间做优化和渲染操作,例如实现 automatic batching。


新渲染架构可能的实现方案

Zynga 的设计文档并没有给出细节实现原理,我就以大家讨论的内容整理了一个可能的方案。

  • main thread 会每 1/60 秒的频率调用 Scene::drawScene() 方法。这个方法有如下步骤:

    1. 发送一个 begin command 到一个 command queue
    2. update() 执行所有的 schedule selector 和 event listener,游戏逻辑大部分就在其中。
    3. 扫描整个 Node Tree,构造一系列 draw command,并发送到 queue
    4. 发送一个 end command 到 queue,形成一个完整的 commands list

    如果某次 drawScene() 消耗的时间超过 1/60 秒,则无需等待,立即开始下一次 drawScene() 调用。这样确保游戏的逻辑能够以最高 60fps 运行,但又不会超过 60fps。

  • draw thread 按照如下步骤以最快 1/60 秒的频率执行:

    1. 从 queue 中找出最后一组 list(如果 draw thread 每次绘图消耗时间超过 1/60 秒,queue 中就可能包含多组 list,放弃之前的 list 可以确保画面符合最近的游戏状态)。
    2. 对 list 进行处理,执行画面渲染。
    3. 更新 queue,删除已绘制的 list 及其之前的所有 list。
  • Texture 对象不再保存材质的实际数据,改为保存一个 texture id。材质的实际数据由 draw thread 进行管理。

    当加载一个材质文件时,main thread 中构造一个新的 Texture 对象,并获得一个新的 texture id。然后发送 load texture command,并将材质文件名和 texture id 作为 command 的参数传递给 draw thread。draw thread 可以在自己线程里载入材质数据,或新开线程载入数据。

    当一个 Texture 被 Sprite 使用时,增加 Texture 对象的引用计数。而使用该 Texture 的 Sprite 删除时,则减小引用计数。如果 TextureCache 需要释放不再引用计数为 0 的 Texture 对象,就发送 release texture command。

  • 在 cocos2d-x 2.x 版中,CCNode 包含了位置、大小、缩放、旋转等信息,以及 OpenGL 绘图需要的数据。

    在新架构里,main thread 维护的 Node Tree 中,Node 不再包含 OpenGL 绘图需要的数据。而是在构造 Node、改变 Node 状态时,才将 Node 的状态数据发送给 draw thread。

    draw thread 根据 node id 维护 node 的绘图数据。

    如果 main thread 执行后,Node 的状态没有发生改变,就无需发送该 Node 的 draw command,也就避免了 draw thread 对没有发生变化的 Node 重新计算绘图数据。


cocos2d-x 3.0 必须要实现线程安全?

在整个设计里,main thread 不停的往 command queue 追加数据,而 draw thread 每完成一次画面渲染就清理一次 command queue。因此 queue 的实现必须是线程安全的,主要的操作都应该满足原子性要求。

但从这个设计看,只要保证两点,那么 main thread 里的对象是不是线程安全根本无所谓:

  1. queue 是线程安全的
  2. draw thread 不使用自动释放的引用计数对象

所以这样一番分析后,感觉 cocos2d-x 3.0 要多线程化,就不一定要做大改动。

但实际上 cocos2d-x 3.0 里还需要多线程完成异步 IO(例如网络)、异步计算等等。而且提供一个可靠的线程安全架构,也方便开发者创建线程来优化游戏体验(比如把 AI 算法放入单独的线程)。

所以从整体架构上看,cocos2d-x 3.0 实现线程安全还是很有必要的。但实现线程安全是否就一定要用智能指针吗?这个我会另外写一篇文章来探讨此问题。

最后,欢迎大家多多提意见。

- END -

0 0