为什么(愿景)
现在Cocos2d渲染机制不错,但慢慢开始显得有点跟不上潮流了。而且,它对于现代平板和手机上流行的多核cpu,不能很好地利用它们的优势。
所以我们想要设计Cocos渲染器,使它性能更好、更优雅、更具有扩展性,更灵活,但保持易用性。我们也想要保留Cocos2d原有或是和原有类似的API,那么使用Cocos2d的开发者会感到很舒服,不需要因为底层的改动而烦恼。
我们实现这个渲染器后,依然保留Cocos2d开发者熟悉的、原有的核心的概念,如:场景(Scenes)、节点(Nodes)、图层(Layers)、精灵(Sprites)。
是什么(目标)
以下是一个在Cocos2d v3.0我们将会实现的关于新特性和提高的高层抽象的视图:
-
从渲染器中把场景图形解耦出来。
访问节点时发布图形命令,并把它们添加到一个队列上,但实际上不调用任何OpenGL的渲染代码。
-
分形几何选择裁剪视图
精灵(Sprite)以及一般的几何图形,如果它们不出现在摄影机的视图范围内,会自动被当前帧移除,不会被选人呢
有效地较少绘图调用次数,自动将绘图调用尽可能地成批处理(如:使用同一纹理的精灵)。
-
基于节点的自定义渲染
在当前的Cocos版本中,开发者能够按需在每个节点的基础上自定义渲染,直接调用OpenGL命令,不顾官方的渲染器(不过很可能导致性能减弱)。
-
对2D渲染进行优化,同样适用于3D
新的渲染器会对2D游戏进行优化,但它对3D的对象也同样有效。
怎样做(计划)
新设计的核心概念是RenderQueue。当访问一个节点时,渲染器不会再直接调用OpenGL命令了。它会将渲染指令(RenderCommands)添加到RenderQueue中去。在这个队列的命令会被渲染器后台不断地读取、按需处理,并推送到实际上执行渲染的API(比如:OpenGL)(如图)。
渲染器后台(运行在独立线程中),会基于一个键来对命令进行排序,将队列中排序好的命令移除队列,进行处理并实际执行。任何有锁或是消耗大量cpu的OpenGL指令会被渲染器后台线程执行,从而使得Cocos的主线程保持空闲,能继续解析场景图形或者处理和渲染无关的任务。这会对并行处理有帮助,特别是在多核cpu的时候。(如图)
“推送指令”这个步骤会在cocos2d的主线程处理,而"对指令排序"和“执行指令”会在渲染队列(RenderQueue)中处理。
指令
一条指令能够是任何东西,从“画一个四边形“到“执行这段OpenGL”。
每一条指令会有一个原料(material)。原料是以下的组合:
另外地,每一条指令有一个64位的键,用于对指令进行排序。拥有更小的键值的指令能够更快被执行。
生成键
键由以下元素构成:
-
视图尺寸:3bits
-
透明/半透明:1 bit
-
命令(cmd) + 指令ID :1bit + 32bits
-
深度:24bits
-
原料ID:24bits
如果半透明位是关闭的(意味着它是一个不透明的对象),那么,深度属性则作废,因为不透明的对象需要被从前到后地绘画,而半透明对象则是从后到前地绘画。
四边形指令
RenderCommandQuads是一个预定义的指令,它会使用一个或多个四边形遍历缓冲区(VBO)。这条指令将被精灵(每个精灵对应1个四边形)、TileMap(对应多个四边形)、SpriteBatchNode(对应多个四边形)、DrawNode(对应多个四边形)使用。
OpenGL指令
通过创建指令来执行自定义的OpenGL调用是可行的(就像在Cocos2d-x v2.x一样)。
为了完成这个任务,开发者需要创建一个RenderCommandOpenGL对象,然后通过OpenGL调用设置xxx属性为一个lambda 对象。
开发者有必要在RenderCommandOpenGL执行后恢复渲染队列的状态。
尽管我们能方便地执行RenderCommandOpenGL的指令,在上下文切换中依然可能出现比较大的性能消耗。另外它也会冲刷四边形的缓冲区。(详见自动批处理)。
3D指令
尽管渲染器为了2D游戏作了优化,但同时也支持3D对象。
为了渲染3D的对象,开发者必须使用一个 Command的子类并对其需要的特性进行实现。
组指令
RenderCommandGroup是一个特殊的指令,它会让渲染器:
-
下列的指令必须被分在同一组。
-
它们不与其他“全局”指令作排序。取而代之的是,它们之间作排序。
用途:
-
ClippingNode:
它的子代必须按照Clipping规则被绘画。
-
RenderTexture
它的自带必须在一个新纹理上被绘画
其他需要对自己的子代作出影响的节点
自动批处理
在Cocos2d-x 3.0里我们想要介绍自动批处理的概念。实际上,我们相信通过减少绘图调用的次数和渲染设备的状态改变会提高动态渲染的速度。
另外,RenderQueue会使用一个缓冲区来绘制四边形(Sprite,Tiles,Drawing primitives)。对四边形的唯一要求是,四边形的每个顶点必须有3个这样的属性:
-
Vertex:3 floats(x,y,z)
-
纹理坐标(Texture coordinates):2 floats(u,v)
-
颜色:4bytes(r,g,b,a)
这些四边形不需要共享相同的原料。但如果所有的四边形共享相同的原料,那么只需要一个绘图调用(自动批处理)。
一个简化版的自动批处理算法工作流程如下:
只使用一个缓冲区(VBO)从而最小化缓冲区切换。
缓冲区会有一个最大的容量——10922个四边形(65536/4)如果缓冲区满了,那么缓冲区会被冲刷(所有没有绘制的四边形会被绘制),然后它能够被再次被使用。
自动裁剪
自动裁剪会在cocos2d主线程被执行。会对每一个节点进行检查。如果节点的AABB在屏幕之外,那么没有指令会被提交到RenderQueue中。
实现细节
节点
ClippingNode
可能的实现:
它会重写visit函数,并发送一条“Push Group;Push Clipping Area;Set Clippiing Area”的指令。
在离开visit函数前,它必须执行“Pop Clipping Area;PopGroup”的指令。
渲染器必须有创建Clipping Area的功能。
DrawNode
需要更深的研究,但RenderCommandQuads似乎是可行的。
它很可能需要自己的着色器,与别的节点一起处理似乎不可行。
Label
它必须使用RenderCommandQuads。
默认的着色器足够好了,但如果距离属性被实现了,它可能需要使用自己的着色器,所以和别的节点一起批处理可能是不可行的。
Layer
它是一个空的、无效的节点,没有指令会被发送到队列中去。
LayerColor、LayerGradient
这些节点应该被DrawNode取代。
如果它们没有被取代,它们必须使用RenderCommandQuads。
Menu
它是一个空的、无效的节点。没有指令会被发送到队列中去。
MenuItem
MenuItemImage、MenuItemSprite、MenuItemLabel必须使用RenderCommandQuads。
MotionStreak
有待讨论
Node
ParallaxNode
ParticleSystem
ParticleSystem使用一个有V3F_C4B_T2F四边形的数组来渲染粒子。所以,一个RenderCommandQuads足够了,让批处理精灵和粒子变得有可行性。
ParticleSystemBatchNode
和ParticleSystem一样,不过移除这个节点大部分时候是安全的。
ProgressTimer
RenderTexture
它必须重写visit函数,发送"Push Group Command;Push Texture;Switch To Texture"的指令。
在离开visit函数前,它必须执行“Pop Texture;Pop Group”的指令。
渲染西必须有创建新Render Texture对象的功能。
Scene
Sprite
它只给RenderCommandQuads发送一个四边形。
SpriteBatchNode
它发送一条RenderCommandQuads指令以及许多包含在批处理中得精灵的四边形。
不包含在内的精灵在渲染器中不进行排序是有价值的。育儿呆滞的是,RenderCommandQuads会基于SpriteBatchNode的值被排序,而不是SpriteBatchNode的子代值。
TextField
TMXTiledMap
和SpriteBatchNode一样
渲染器伪代码
https://gist.github.com/ricardoquesada/7049216
引用
Misc