Is there any chance to import a skeleton file into the Spine via command line interface? I have about 100 spine models with textures and I'm gonna to pack textures in one atlas (because of performance). The problem is that the artist called the textures by similar names, and I have 100 heads, 100 hands... in separated folders but when I tried to get one shared atlas, you know, I found one big problem... I sorted out the problem with my handmade utility which renames the textures with new names and its names within json (exported from spine file) file but i can't do backward import into the spine for the future creation of shared atlas .
Looks like it's not described itself by documentation http://esotericsoftware.com/spine-export#Command-line ?
How to import json sceleton to spine file via command-line?
- संपादित
There currently isn't a way to import JSON to a project file via the command line, sorry. If you want to do that just for texture packing, I think you may not need to.
in separated folders but when I tried to get one shared atlas, you know, I found one big problem
What exactly is the problem you found? You can run the texture packer (separately from doing a JSON or binary data export) and use Combine subdirectories
. This ignores the folder structure and packs all the images into a single atlas page (if they fit, check your max page size).
- संपादित
Just for a better understanding. I had following structure of folder:
/hero1
+-body.png
+-head.png
+-...
/hero2
+-body.png
+-head.png
+-...
hero1.spine
hero2.spine
The options Combine subdirectories is on and as a result, I get the following shared atlas:
shared.png
size: 1024,1024
format: RGBA8888
filter: Linear,Linear
repeat: none
body
rotate: true
xy: 223, 594
size: 23, 38
orig: 23, 38
offset: 0, 0
index: -1
head
rotate: true
xy: 246, 722
size: 49, 44
orig: 49, 44
offset: 0, 0
index: -1
body // here is broken link because of 1st 'body'
rotate: true
xy: 387, 607
size: 37, 45
orig: 37, 45
offset: 0, 0
index: -1
//...
The problem is that information about links within shared atlas file is lost and for all models that use the element "body"(for example) from own separated folder before. As result the first found element "body" from the shared atlas will be always used by sceleton.
If you uncheck Flatten paths
then you will get atlas regions named hero1/head
and hero2/head
. For that to work, your skeletons would need to use the root folder and have attachment names or paths like hero1/head
. This is because by default AtlasAttachmentLoader looks in the atlas for the region by the attachment's path, eg:
spine-runtimes/AtlasAttachmentLoader.java at 3.6
AtlasRegion region = atlas.findRegion(path);
You can change your skeleton image paths and use Find and Replace to fix up the attachment names/paths. If you don't want to do that, you can write your own AttachmentLoader to add a prefix. Eg, create your own class that is a copy of AtlasAttachmentLoader and add a String field so you can tell the loader what prefix to use based on which skeleton you are loading. Then change the region and mesh methods to use the prefix:
AtlasRegion region = atlas.findRegion(prefix + "/" + path);
Set the prefix appropriately before loading each skeleton.
Both approaches are not good if it's out the box of spine and need additional programming activity from developer side.
In the first case, we get "The texture is not found" via runtime, because a skeleton has a broken reference to "head" and has not reference to shared "hero1/head".
In second case, of course, you can use the manual mode by Find and Replace operation in Spine GUI, but this works for "poor" models and with the number of 10-20, but when it comes to 100+ "rich" models with skins, frame-by-frame animations etc, so it turns into a manual hell. :broken:
My idea is that anything which worked with separate atlases should work within the shared atlas without any additional developer's activities. Because there were no physical changes, there was just a logical redistribution of objects in memory to better performance, this should not work differently than expected before.
Sergiy लिखाBoth approaches are not good if it's out the box of spine and need additional programming activity from developer side. [snip] My idea is that anything which worked with separate atlases should work within the shared atlas without any additional developer's activities.
For it to "just work", the skeleton attachments must use the full path, ie hero1/head
. Since this wasn't done, you can either modify the projects or customize the behavior at runtime.
Sergiy लिखाBecause there were no physical changes, there was just a logical redistribution of objects in memory to better performance, this should not work differently than expected before.
There is a big difference: the names of the regions in the texture atlas. You can see how it works above, AtlasAttachmentLoader uses the attachment path to find the region. If you want anything else, you'll have to write your own AttachmentLoader. Doing that is not difficult and is well within how the runtimes are intended to be used. It's only a few lines of code more than what is needed to load the JSON.
As I understand you right, in the situation I described above, the end user does not have any opportunity to use the shared atlas "as is" without AtlasAttachmentLoader or other modification. In the same time there are no software options at Spine to solve this problem if artist used the same names in different spine files.
This is because the Spine/or spine-runtime considers the value of skeleton.images in skeleton file unnecessary and nonessential. The value of skeleton.images used only by Spine GUI when user want import skeleton to spine but ignored in runtime during search within atlas (as alternative way to get necessary entry when searching texture key, [like $PATH variable worked in OS]).
Yes, skeleton.images
is only used for import. It is a path on the computer where the Spine project is and not something that is very useful at runtime.
Note I'm not suggesting modifying AtlasAttachmentLoader or the Spine Runtimes. The Spine Runtimes allow the AttachmentLoader to be specified. It is part of the configuration of SkeletonJson and SkeletonBinary when loading skeleton data. Eg, with spine-libgdx, instead of the usual:
TextureAtlas atlas;
...
SkeletonJson json = new SkeletonJson(atlas);
json.readSkeletonData(hero1);
json.readSkeletonData(hero2);
TextureAtlas atlas;
String prefix;
...
SkeletonJson json = new SkeletonJson(new AtlasAttachmentLoader(atlas) {
public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
return super.newRegionAttachment(skin, name, prefix + path);
}
public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
return super.newMeshAttachment(skin, name, prefix + path);
}
});
prefix = "hero1";
json.readSkeletonData(hero1);
prefix = "hero2";
json.readSkeletonData(hero2);
We do have a task for adding JSON import at the CLI:
https://waffle.io/EsotericSoftware/spine/cards/580bacfc29fef328007f9dd9
I've added your use case to it. It does make sense to want to mass import skeletons after processing the JSON data.
Nate लिखाIt is part of the configuration of SkeletonJson and SkeletonBinary when loading skeleton data. Eg, with spine-libgdx, instead of the usual
I agree with you that a code with spine-libgdx looks easy with anonymous classes and it's beautiful overrides, but nevertheless it is a java. And what about C++? Specifically, I'm interested in spine-cocos2d-x interface, is it possible to get the same with a minimum of effort? Often I use:
SkeletonRenderer* SkeletonRenderer::createWithFile (const std::string& skeletonDataFile, const std::string& atlasFile, float scale)
How can I add a prefix with cocos2d-x? I myself will answer this question, it's not easy, because there is no OOP and OOD:
#include <spine/Cocos2dAttachmentLoader.h>
#include <spine/AttachmentVertices.h>
#include <spine/extension.h>
struct CustomAttachmentLoader {
spAttachmentLoader super;
spAtlasAttachmentLoader* atlasAttachmentLoader;
std::string key;
};
static unsigned short quadTriangles[6] = {0, 1, 2, 2, 3, 0};
// just general approach
spAttachment* _CustomAttachmentLoader_createAttachment (spAttachmentLoader* loader, spSkin* skin, spAttachmentType type,
const char* name, const char* path) {
CustomAttachmentLoader* self = SUB_CAST(CustomAttachmentLoader, loader);
std::string newPath = self->key + "/";
newPath += path;
return spAttachmentLoader_createAttachment(SUPER(self->atlasAttachmentLoader), skin, type, name, newPath.c_str());
}
void _CustomAttachmentLoader_configureAttachment (spAttachmentLoader* loader, spAttachment* attachment) {
attachment->attachmentLoader = loader;
switch (attachment->type) {
case SP_ATTACHMENT_REGION: {
spRegionAttachment* regionAttachment = SUB_CAST(spRegionAttachment, attachment);
spAtlasRegion* region = (spAtlasRegion*)regionAttachment->rendererObject;
AttachmentVertices* attachmentVertices = new AttachmentVertices((Texture2D*)region->page->rendererObject, 4, quadTriangles, 6);
V3F_C4B_T2F* vertices = attachmentVertices->_triangles->verts;
for (int i = 0, ii = 0; i < 4; ++i, ii += 2) {
vertices[i].texCoords.u = regionAttachment->uvs[ii];
vertices[i].texCoords.v = regionAttachment->uvs[ii + 1];
}
regionAttachment->rendererObject = attachmentVertices;
break;
}
case SP_ATTACHMENT_MESH: {
spMeshAttachment* meshAttachment = SUB_CAST(spMeshAttachment, attachment);
spAtlasRegion* region = (spAtlasRegion*)meshAttachment->rendererObject;
AttachmentVertices* attachmentVertices = new AttachmentVertices((Texture2D*)region->page->rendererObject,
meshAttachment->super.worldVerticesLength >> 1, meshAttachment->triangles, meshAttachment->trianglesCount);
V3F_C4B_T2F* vertices = attachmentVertices->_triangles->verts;
for (int i = 0, ii = 0, nn = meshAttachment->super.worldVerticesLength; ii < nn; ++i, ii += 2) {
vertices[i].texCoords.u = meshAttachment->uvs[ii];
vertices[i].texCoords.v = meshAttachment->uvs[ii + 1];
}
meshAttachment->rendererObject = attachmentVertices;
break;
}
default: ;
}
}
void _CustomAttachmentLoader_disposeAttachment (spAttachmentLoader* loader, spAttachment* attachment) {
switch (attachment->type) {
case SP_ATTACHMENT_REGION: {
spRegionAttachment* regionAttachment = SUB_CAST(spRegionAttachment, attachment);
delete (AttachmentVertices*)regionAttachment->rendererObject;
break;
}
case SP_ATTACHMENT_MESH: {
spMeshAttachment* meshAttachment = SUB_CAST(spMeshAttachment, attachment);
delete (AttachmentVertices*)meshAttachment->rendererObject;
break;
}
default: ;
}
}
void _CustomAttachmentLoader_dispose (spAttachmentLoader* loader) {
CustomAttachmentLoader* self = SUB_CAST(CustomAttachmentLoader, loader);
spAttachmentLoader_dispose(SUPER_CAST(spAttachmentLoader, self->atlasAttachmentLoader));
_spAttachmentLoader_deinit(loader);
}
RichAnimation::RichAnimation(const std::string& prefix) : _prefix(prefix) {
}
RichAnimation* RichAnimation::createWithJsonFile (const std::string& prefix, const std::string& skeletonJsonFile, spAtlas* atlas, float scale) {
RichAnimation* node = new RichAnimation(prefix);
node->initWithJsonFileExtend(skeletonJsonFile, atlas, scale);
node->autorelease();
return node;
}
RichAnimation* RichAnimation::createWithJsonFile (const std::string& prefix, const std::string& skeletonJsonFile, const std::string& atlasFile, float scale) {
RichAnimation* node = new RichAnimation(prefix);
spAtlas* atlas = spAtlas_createFromFile(atlasFile.c_str(), 0);
node->initWithJsonFileExtend(skeletonJsonFile, atlas, scale);
node->autorelease();
return node;
}
RichAnimation* RichAnimation::createWithBinaryFile (const std::string& prefix, const std::string& skeletonBinaryFile, spAtlas* atlas, float scale) {
RichAnimation* node = new RichAnimation(prefix);
node->initWithBinaryFileExtend(skeletonBinaryFile, atlas, scale);
node->autorelease();
return node;
}
RichAnimation* RichAnimation::createWithBinaryFile (const std::string& prefix, const std::string& skeletonBinaryFile, const std::string& atlasFile, float scale) {
RichAnimation* node = new RichAnimation(prefix);
spAtlas* atlas = spAtlas_createFromFile(atlasFile.c_str(), 0);
node->initWithBinaryFileExtend(skeletonBinaryFile, atlas, scale);
node->autorelease();
return node;
}
void RichAnimation::initWithJsonFileExtend (const std::string& skeletonDataFile, spAtlas* atlas, float scale) {
_atlas = atlas;
createCustomAttachmentLoader(_atlas);
spSkeletonJson* json = spSkeletonJson_createWithLoader(_attachmentLoader);
json->scale = scale;
spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(json, skeletonDataFile.c_str());
CCASSERT(skeletonData, json->error ? json->error : "Error reading skeleton data.");
spSkeletonJson_dispose(json);
setSkeletonData(skeletonData, true);
initialize();
}
void RichAnimation::initWithJsonFileExtend (const std::string& skeletonDataFile, const std::string& atlasFile, float scale) {
_atlas = spAtlas_createFromFile(atlasFile.c_str(), 0);
CCASSERT(_atlas, "Error reading atlas file.");
createCustomAttachmentLoader(_atlas);
spSkeletonJson* json = spSkeletonJson_createWithLoader(_attachmentLoader);
json->scale = scale;
spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(json, skeletonDataFile.c_str());
CCASSERT(skeletonData, json->error ? json->error : "Error reading skeleton data file.");
spSkeletonJson_dispose(json);
setSkeletonData(skeletonData, true);
initialize();
}
void RichAnimation::initWithBinaryFileExtend (const std::string& skeletonDataFile, spAtlas* atlas, float scale) {
_atlas = atlas;
createCustomAttachmentLoader(_atlas);
spSkeletonBinary* binary = spSkeletonBinary_createWithLoader(_attachmentLoader);
binary->scale = scale;
spSkeletonData* skeletonData = spSkeletonBinary_readSkeletonDataFile(binary, skeletonDataFile.c_str());
CCASSERT(skeletonData, binary->error ? binary->error : "Error reading skeleton data file.");
spSkeletonBinary_dispose(binary);
setSkeletonData(skeletonData, true);
initialize();
}
void RichAnimation::initWithBinaryFileExtend (const std::string& skeletonDataFile, const std::string& atlasFile, float scale) {
_atlas = spAtlas_createFromFile(atlasFile.c_str(), 0);
CCASSERT(_atlas, "Error reading atlas file.");
createCustomAttachmentLoader(_atlas);
spSkeletonBinary* binary = spSkeletonBinary_createWithLoader(_attachmentLoader);
binary->scale = scale;
spSkeletonData* skeletonData = spSkeletonBinary_readSkeletonDataFile(binary, skeletonDataFile.c_str());
CCASSERT(skeletonData, binary->error ? binary->error : "Error reading skeleton data file.");
spSkeletonBinary_dispose(binary);
setSkeletonData(skeletonData, true);
initialize();
}
void RichAnimation::createCustomAttachmentLoader (spAtlas* atlas) {
CustomAttachmentLoader* self = NEW(CustomAttachmentLoader);
_spAttachmentLoader_init(SUPER(self), _CustomAttachmentLoader_dispose, _CustomAttachmentLoader_createAttachment,
_CustomAttachmentLoader_configureAttachment, _CustomAttachmentLoader_disposeAttachment);
self->atlasAttachmentLoader = spAtlasAttachmentLoader_create(atlas);
self->key = _prefix;
_attachmentLoader = &self->super;
}
Ah, yeah that is a bit rough. It's made worse by cocos2dx needing its own AttachmentLoader. It's easier over here in Java-land! No need to hurt yourself anymore! I'm just kidding though. We'll have CLI import soon.