- Edited
Cocos2d-x - Batch all Skeletons / Sprites from one atlas
Hey,
I just recently started using Spine with cocos2d-x and I'm having trouble with the rendering.
For testing purposes, I created the same SkeletonAnimation twice and it is taking 2 draw calls.
Since these two are from the same atlas, it should be possible to batch draw them and save performance.
How can I achieve this batch rendering all SkeletonAnimations from one texture atlas?
Here's my code that results in two draw calls:
_skeletonOne = SkeletonAnimation::createWithFile("gatling-test.json", "gatling-test.atlas");
_skeletonOne->setPosition(Vec2(180, 180));
addChild(_skeletonOne);
_skeletonTwo = SkeletonAnimation::createWithFile("gatling-test.json", "gatling-test.atlas");
_skeletonTwo->setPosition(Vec2(380, 180));
addChild(_skeletonTwo);
Unfortunately the rendering part of programming isn't something I know very much about (well programming in general is pretty alien to me).
However I've let Nate know you're in a hurry to get a reply so hopefully he can get back to you quick
SkeletonAnimation::createWithFile
loads the SkeletonData and atlas from disk. Loading the atlas also involves loading the atlas images. This means you have loaded the SkeletonData twice and the atlas twice and the atlas images twice. Since you have two textures there is no batching.
Load the SkeletonData yourself and use the SkeletonAnimation::createWithData
method to create each SkeletonAnimation. See SkeletonRenderer::initWithFile
for how to load the SkeletonData.
Okay, I've just tried that and it's still two draw calls. Any more ideas?
std::string atlasFile = "gatling-test.atlas";
std::string skeletonDataFile = "gatling-test.json";
float timeScale = 1.0;
_atlas = spAtlas_createFromFile(atlasFile.c_str(), 0);
CCASSERT(_atlas, "Error reading atlas file.");
spSkeletonJson* json = spSkeletonJson_create(_atlas);
json->scale = timeScale;
spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(json, skeletonDataFile.c_str());
CCASSERT(skeletonData, json->error ? json->error : "Error reading skeleton data file.");
spSkeletonJson_dispose(json);
_skeletonOne = SkeletonAnimation::createWithData(skeletonData);
_skeletonOne->setPosition(Vec2(180, 180));
addChild(_skeletonOne);
_skeletonTwo = SkeletonAnimation::createWithData(skeletonData);
_skeletonTwo->setPosition(Vec2(380, 180));
addChild(_skeletonTwo);
On another note: I'm using cocos2d-x 3.9 with Spine Runtime 2.3, downloaded yesterday from here:
https://github.com/EsotericSoftware/spine-runtimes/tree/master/spine-cocos2dx/3
Hey @framusrock - did you ever get it to batch render properly?
Sorry I didn't see your response sooner.
Ah, so I got my runtime implementations confused. spine-cocos2dx is using PolygonBatch
to batch geometry and this is flushed per skeleton. In a scene graph like cocos2d-x, batching can be difficult. Typically some built-in mechanism is used for rendering. This provides a single place for the framework to know when texture changes occur so it can batch across scene graph nodes.
At the time we wrote the runtime, there did not appear to be a suitable API. Do you know if there is way for the framework to do the rendering? Does cocos2d-x still parallel cocos2d-iphone? I thought I read they had started to diverge some time ago. FWIW, cocos2d-iphone does have such an API.
As an alternative to "dynamic" batching performed by the framework, batching can also be done with an interesting trick. When a scene graph node is added to the batch, it also adds any adjacent nodes to the batch and marks them as "batched" so they don't try to draw when it's their turn.
Nate wroteAh, so I got my runtime implementations confused. spine-cocos2dx is using PolygonBatch to batch geometry and this is flushed per skeleton. In a scene graph like cocos2d-x, batching can be difficult. Typically some built-in mechanism is used for rendering. This provides a single place for the framework to know when texture changes occur so it can batch across scene graph nodes.
At the time we wrote the runtime, there did not appear to be a suitable API. Do you know if there is way for the framework to do the rendering? Does cocos2d-x still parallel cocos2d-iphone? I thought I read they had started to diverge some time ago. FWIW, cocos2d-iphone does have such an API.
Yes, right. I come from using cocos2d-iPhone and I know it works just fine there. You're also right about the two cocos2d-versions diverging some time ago. Basically when they did, I went to cocos2d-x. There was a lot of drama and the main developer and founder of cocos2d-iPhone is now working on cocos2d-x (along other things happening). They even split cocos2d-iphone into another two versions....so basically there's no real hope for any of these iphone versions in the near future....
But back to topic, cocos2d-x also has some kind of dynamic batching which is based on the scene graph. If you add two sprites from the same texture to the scene, they get batched dynamically. It also works for BitmapFont labels, etc. Though I'm still fairly new to cocos2d-x, so I don't know yet how it works exactly.
This guy here (kind of) explains how the cocos2d-x dynamic batching works, but the links for further explanations are down:
http://discuss.cocos2d-x.org/t/cocos2d-x-3-batch-drawing-explained/15282/5
If this is not enough to understand, maybe you could ask in the cocos2d-x forums how the batching would be handled best for Spine. I tried myself asking the question there, but got no response at all. Since they know you, you've probably got better chances then me, getting a helpful answer
Nate wroteAs an alternative to "dynamic" batching performed by the framework, batching can also be done with an interesting trick. When a scene graph node is added to the batch, it also adds any adjacent nodes to the batch and marks them as "batched" so they don't try to draw when it's their turn.
This sounds interesting, too - though I don't quite understand how it would work in our case. Would one PolygonBatch render all of the Skeletons?
I guess the best way to do it, is the way with the best performance outcome. What do you think which way this would be?
Dynamic batching only works when using the framework supplied rendering method. Since we need to draw meshes and the framework did not appear to support this, we had to do drawing ourselves. The question is if the framework now has an API we can use. I will ask around and see what I come up with.
The alternative to dynamic batching should perform roughly the same. Say you have 3 scene graph siblings: A, B, C. When A draws, it also draws B and C in a single batch because they are adjacent siblings. When B and C go to draw, they do nothing since they were marked as already having been drawn by A.
Now imagine you have A, B, X, C where X is a non-skeleton scene graph node. When A draws it also draws B but not C because C isn't adjacent. When B goes to draw is does nothing because it was marked as already having been drawn by A. When C goes to draw it draws itself because it hasn't been drawn yet, resulting in 2 draw calls for your skeleton nodes. Dynamic batching should result in the same number of draws.
Yes, I understand that. I'm pretty sure it has this ability by now, but I'm not as familiar as I was with cocos2d-iPhone, so I'm sorry that I can't point you to it. Thanks a lot for trying to find out with me!
The second method sounds like it might break the globalZOrder feature of cocos2d-x. If we have a Scene Graph with A, B, X, C, Y - again A, B, C being skeletons but X and Y being Labels. When assigning a globalZOrder of 1 to A, B, C and one of 2 to X and Y, ideally there's only 2 draw calls. X and Y would be always drawn above A, B, C and still move with them (as they are still children of them).
Would this still work or be broken?
Yep, globalZOrder
seems like it would break if the adjacent batching was implemented. This just goes to show that a scene graph is often not the right choice for building applications. A scene graph couples the model and view and it removes a lot of flexibility.
Nate wroteYep, globalZOrder seems like it would break if the adjacent batching was implemented. This just goes to show that a scene graph is often not the right choice for building applications. A scene graph couples the model and view and it removes a lot of flexibility.
Yes, I see. It would be really cool if we could find a solution to have batch rendering and not break any of these cool features.
I've done some research in the meantime:
1) Cocos2d-x now has mesh-rendering for Sprites:
http://discuss.cocos2d-x.org/t/new-feature-meshsprite-polygonsprite-updated/21153
https://www.codeandweb.com/blog/2015/10/01/cocos2d-x-performance-optimization
2) More interesting: I've checked out the cocos2d-x Renderer a bit more. This is from CCRenderer.cpp:
void Renderer::addCommand(RenderCommand* command, int renderQueue)
{
CCASSERT(!_isRendering, "Cannot add command while rendering");
CCASSERT(renderQueue >=0, "Invalid render queue");
CCASSERT(command->getType() != RenderCommand::Type::UNKNOWN_COMMAND, "Invalid Command Type");
_renderGroups[renderQueue].push_back(command);
}
Every single sprite owns one of these RendererCommands and adds its renderer command to the list. The RenderCommand also knows about its personal globalZOrder (here called globalOrder). There's a TrianglesCommand and a QuadCommand subclass. TrianglesCommand can contain one or more triangles to render, hence all the triangles of a PolygonBatch could be included in one of these (same for the quads equivalent).
Then, all RenderCommands that use the same texture and are adjacent in globalOrder will get batched automatically. Quads and Triangles can not be batched together at the moment.
That's at least how I understood it from reading the code, I hope this helps finding a way to get batch drawing for Skeletons to work in a nice way
Great, thanks! At a glance it seems similar to cocos2d-iphone. What do you say Pharan, want to take a crack at it? Can compare to spine-cocos2d-phone.
A crack at what? I don't know how cocos works yet. Took me a day to get it to even compile.
If framusrock guides me through it, I may have a chance. :rofl:
Also, what's with all the API changes in Cocos2d-x 3?
And is 3.10 different from 3.1? Cocos2D-X versions don't work that way?
Well, I'm by no means an expert at cocos2d-x' Renderer yet. What I've summarized above took me about 15 minutes of reading their code and trying to understand what it does.
Usually cocos2d-x compiles just fine for me, and 3.10 is very different from 3.1, since 3.10 is 9 versions after 3.1.
So I really don't know how I should guide you there, and I also don't really have the time to do it.
I'm about to buy your product, Spine - as I really love it and the animations that are possible with it.
But batch rendering is a dealbreaker-feature for me. If Spine does not support I cannot use it, since I will have hundreds of Skeletons on the screen at once.
As far as I've understood the cocos2d-x renderer, it should be fairly straightforward to support it.
Haha! Don't worry. I'll figure it out. And if it can't work, you'll be the first to know. Don't go rushing into buying Spine or anything.
Well, we know for sure there's way to make it work. Cocos2d-x does rendering for its Sprites based on textures and triangles which is the same as Spine can use.
Going this path will even keep Cocos2d-x features as globalZOrder working.
Thanks, I'm really looking forward to a solution to this issue.
@[deleted]
I don't get it.
SkeletonRenderer already uses a CCCustomCommand that has a callback to draw the skeleton.
That's already being given to the cocos2dx renderer.
https://github.com/EsotericSoftware/spine-runtimes/blob/master/spine-cocos2dx/3/src/spine/SkeletonRenderer.cpp#L147-L149
Does supporting dynamic batching amount to just using CCTrianglesCommand like Sprite does instead?
https://github.com/cocos2d/cocos2d-x/blob/v3/cocos/2d/CCSprite.cpp#L668-L669
This is a pretty old thread.
If we need to use CCTrianglesCommand, it's looking like we have to build and maybe cache a whole vertex and triangle array for each skeleton instance, just like Unity. That's a pretty significant change to SkeletonRenderer.
That renderer->addCommand
just calls drawSkeleton
at the right time. I guess it is using a separate thread to render? It is taking a whole lot of effort not to be judgmental here. :bang: Breathe, stay on topic.
Being called on the right thread isn't enough to get batching. How the drawing is done is the important part. We use our own "render command" which calls drawSkeleton
which uses PolygonBatch
which issues a draw call immediately.
There are triangle and quad commands, but possibly we can use our own command then use another API to do the rendering in a batch friendly way. This is what spine-cocos2d-iphone does:
https://github.com/EsotericSoftware/spine-runtimes/blob/master/spine-cocos2d-iphone/3/src/spine/SkeletonRenderer.m#L232-L247
Even if we need to pass triangle data, this is what PolygonBatch
is doing already so it isn't too different.
Nate is right. Spine is currently using a CustomCommand to do the rendering. Using a custom commands makes cocos2d-x call a callback function when rendering:
void CustomCommand::execute()
{
if(func)
{
func();
}
}
Spine defines drawSkeleton() of SkeletonRenderer as this function which calls flush() of the PolygonBatch. This happens once for every Skeleton, resulting in lots of draw calls.
Spine should use TrianglesCommand instead of CustomCommand and let the cocos2d-x renderer decide which triangles can be batched.