Okay, here’s the solution I got, cleaned up and refactored for external use.
Note this solution is probably not the best but it works for us and our uses. This solution will definitely leak a small amount of memory if you don’t call delete XXX
on the heap allocated objects once you’re done using them, e.g. AtlasPage, AtlasRegion, and the Attachment itself.
SpriteFrameAttachmentLoader.h
//
// SpriteFrameAttachmentLoader.h
// Replaces attachments on a spine skeleton with an arbitrary cocos2d::SpriteFrame.
//
// Created by Tyelor K. on 9/21/21 for ExceptioNULL Games.
// Use freely, and modify to your hearts content. Neither Myself, ExceptioNULL Games,
// nor this code are associated with or affiliated with Cocos2d-x or its license holders.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this code, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of this code, and to permit persons to whom the code is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#ifndef SpriteFrameAttachmentLoader_h
#define SpriteFrameAttachmentLoader_h
#include "cocos2d.h"
#include <spine/spine.h>
#include <spine/spine-cocos2dx.h>
namespace spineextensions {
using spString = spine::String;
static unsigned short quadTriangles[6] = { 0, 1, 2, 2, 3, 0 };
class SpriteFrameAttachmentLoader
{
public:
static void replaceAttachment(spine::SkeletonAnimation* skeleton, const std::string& slotName, cocos2d::SpriteFrame* spriteFrame);
private:
static spine::AtlasRegion* toAtlasRegion(cocos2d::SpriteFrame* spriteFrame);
static spine::Attachment* getRemappedClone(spine::Attachment* attachment, spine::AtlasRegion* atlasRegion, bool cloneMeshAsLinked = true, bool useOriginalRegionSize = true, float scale = 1.0f);
static void configureAttachment(spine::Attachment* attachment);
static spine::AtlasPage* toSpineAtlasPage(cocos2d::SpriteFrame* spriteFrame);
static spine::AtlasPage* toSpineAtlasPage(cocos2d::Texture2D* tex);
static void setAttachmentRegion(spine::RegionAttachment* attachment, spine::AtlasRegion* atlasRegion);
static void setAttachmentRegion(spine::MeshAttachment* attachment, spine::AtlasRegion* atlasRegion);
static void setAttachmentVertices(spine::RegionAttachment* attachment);
static void setAttachmentVertices(spine::MeshAttachment* attachment);
static void deleteAttachmentVertices(void* vertices);
};
} // ns_spineextensions
#endif /* SpriteFrameAttachmentLoader_h */
SpriteFrameAttachmentLoader.cpp
//
// SpriteFrameAttachmentLoader.cpp
// Replaces attachments on a spine skeleton with an arbitrary cocos2d::SpriteFrame.
//
// Created by Tyelor K. on 9/21/21 for ExceptioNULL Games.
// Use freely, and modify to your hearts content. Neither Myself, ExceptioNULL Games,
// nor this code are associated with or affiliated with Cocos2d-x or its license holders.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this code, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of this code, and to permit persons to whom the code is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include "SpriteFrameAttachmentLoader.h"
#include <spine/AttachmentVertices.h>
#include <spine/Atlas.h>
USING_NS_CC;
using namespace spine;
using namespace spineextensions;
//
// Usage: spineextensions::SpriteFrameAttachmentLoader::replaceAttachment(skeletonWithAttachmentToReplace, slotNameHoldingAttachmentToReplace, spriteFrameToReplaceAttachmentWith)
//
// Note: This solution modifies the input skeleton animation, and thus requires no return value.
//
void SpriteFrameAttachmentLoader::replaceAttachment(spine::SkeletonAnimation* skeleton, const std::string& slotName, cocos2d::SpriteFrame* spriteFrame)
{
// Ensure parameters are valid:
if (skeleton == nullptr || spriteFrame == nullptr || slotName.empty()) {
return;
}
// Find the slot for the attachment we're replacing:
auto slot = skeleton->findSlot(slotName);
// If slot was found, find it's attachment:
if (slot != nullptr) {
auto attachment = slot->getAttachment();
// If attachment was found, replace it:
if (attachment != nullptr) {
auto atlasRegion = toAtlasRegion(spriteFrame);
auto newAttachment = getRemappedClone(attachment, atlasRegion);
// Replace the attachment only if the new attachment is not null:
if (newAttachment != nullptr) {
slot->setAttachment(newAttachment);
}
}
}
}
spine::AtlasRegion* SpriteFrameAttachmentLoader::toAtlasRegion(cocos2d::SpriteFrame* spriteFrame)
{
// Ensure parameters are valid:
if (spriteFrame == nullptr) {
return nullptr;
}
auto page = toSpineAtlasPage(spriteFrame);
auto width = page->width;
auto height = page->height;
auto region = new(__FILE__, __LINE__) AtlasRegion();
region->name = std::to_string(spriteFrame->getTexture()->getName()).c_str();
region->index = -1;
region->rotate = spriteFrame->isRotated();
// World space units
Vec2 boundsMin = Vec2::ZERO;
Vec2 boundsMax = Vec2(width, height);
auto frameRect = spriteFrame->getRect();
// Texture space/pixel units
region->width = (int)frameRect.size.width;
region->originalWidth = (int)spriteFrame->getOriginalSizeInPixels().width;
region->height = (int)frameRect.size.height;
region->originalHeight = (int)spriteFrame->getOriginalSizeInPixels().height;
region->offsetX = 0.0f;
region->offsetY = 0.0f;
// Get UVs based on the SpriteFrame:
region->u = frameRect.origin.x / width;
region->v = frameRect.origin.y / height;
region->u2 = (frameRect.origin.x + frameRect.size.width) / width;
region->v2 = (frameRect.origin.y + frameRect.size.height) / height;
region->x = 0.0f;
region->y = 0.0f;
region->page = page;
return region;
}
Attachment* SpriteFrameAttachmentLoader::getRemappedClone(spine::Attachment* attachment, spine::AtlasRegion* atlasRegion, bool cloneMeshAsLinked, bool useOriginalRegionSize, float scale)
{
// Ensure parameters are valid:
if (attachment == nullptr || atlasRegion == nullptr) {
return nullptr;
}
auto regionAttachment = dynamic_cast<RegionAttachment*>(attachment);
if (regionAttachment != nullptr) {
RegionAttachment* newAttachment = static_cast<RegionAttachment*>(regionAttachment->copy());
setAttachmentRegion(newAttachment, atlasRegion);
configureAttachment(newAttachment);
if (!useOriginalRegionSize) {
newAttachment->setRegionWidth(atlasRegion->width * scale);
newAttachment->setRegionHeight(atlasRegion->height * scale);
}
newAttachment->updateOffset();
return newAttachment;
}
auto meshAttachment = dynamic_cast<MeshAttachment*>(attachment);
if (meshAttachment != nullptr) {
MeshAttachment* newAttachment = cloneMeshAsLinked ? meshAttachment->newLinkedMesh() : (MeshAttachment*)meshAttachment->copy();
setAttachmentRegion(newAttachment, atlasRegion);
configureAttachment(newAttachment);
return newAttachment;
}
// Non-renderable Attachments will return as normal cloned attachments.
return attachment->copy();
}
spine::AtlasPage* SpriteFrameAttachmentLoader::toSpineAtlasPage(cocos2d::SpriteFrame* spriteFrame)
{
// Ensure parameters are valid:
if (spriteFrame == nullptr) {
return nullptr;
}
return toSpineAtlasPage(spriteFrame->getTexture());
}
AtlasPage* SpriteFrameAttachmentLoader::toSpineAtlasPage(Texture2D* texture)
{
// Ensure parameters are valid:
if (texture == nullptr) {
return nullptr;
}
auto newPage = new(__FILE__, __LINE__) AtlasPage(std::to_string(texture->getName()).c_str());
newPage->setRendererObject(texture);
newPage->texturePath = texture->getPath().c_str();
newPage->width = texture->getPixelsWide();
newPage->height = texture->getPixelsHigh();
return newPage;
}
void SpriteFrameAttachmentLoader::configureAttachment(Attachment* attachment)
{
auto regionAttachment = dynamic_cast<RegionAttachment*>(attachment);
if (regionAttachment != nullptr) {
setAttachmentVertices(regionAttachment);
return;
}
auto meshAttachment = dynamic_cast<MeshAttachment*>(attachment);
if (meshAttachment != nullptr) {
setAttachmentVertices(meshAttachment);
return;
}
}
void SpriteFrameAttachmentLoader::setAttachmentRegion(RegionAttachment* attachment, AtlasRegion* atlasRegion)
{
// Ensure parameters are valid:
if (attachment == nullptr || atlasRegion == nullptr) {
return;
}
attachment->setRendererObject(atlasRegion);
attachment->setUVs(atlasRegion->u, atlasRegion->v, atlasRegion->u2, atlasRegion->v2, atlasRegion->rotate);
attachment->setRegionWidth(atlasRegion->width);
attachment->setRegionHeight(atlasRegion->height);
attachment->setRegionOffsetX(atlasRegion->offsetX);
attachment->setRegionOffsetY(atlasRegion->offsetY);
attachment->setRegionOriginalWidth(atlasRegion->originalWidth);
attachment->setRegionOriginalHeight(atlasRegion->originalHeight);
}
void SpriteFrameAttachmentLoader::setAttachmentRegion(MeshAttachment* attachment, AtlasRegion* atlasRegion)
{
// Ensure parameters are valid:
if (attachment == nullptr || atlasRegion == nullptr) {
return;
}
attachment->setRendererObject(atlasRegion);
attachment->setRegionU(atlasRegion->u);
attachment->setRegionV(atlasRegion->v);
attachment->setRegionU2(atlasRegion->u2);
attachment->setRegionV2(atlasRegion->v2);
attachment->setRegionRotate(atlasRegion->rotate);
attachment->setRegionWidth(atlasRegion->width);
attachment->setRegionHeight(atlasRegion->height);
attachment->setRegionOffsetX(atlasRegion->offsetX);
attachment->setRegionOffsetY(atlasRegion->offsetY);
attachment->setRegionOriginalWidth(atlasRegion->originalWidth);
attachment->setRegionOriginalHeight(atlasRegion->originalHeight);
}
void SpriteFrameAttachmentLoader::setAttachmentVertices(RegionAttachment* attachment)
{
// Ensure parameters are valid:
if (attachment == nullptr) {
return;
}
AtlasRegion* region = (AtlasRegion*)attachment->getRendererObject();
auto renderObject = (region == nullptr || region->page == nullptr) ? nullptr : (Texture2D*)region->page->getRendererObject();
AttachmentVertices* attachmentVertices = new AttachmentVertices(renderObject, 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 = attachment->getUVs()[ii];
vertices[i].texCoords.v = attachment->getUVs()[ii + 1];
}
attachment->setRendererObject(attachmentVertices, deleteAttachmentVertices);
}
void SpriteFrameAttachmentLoader::setAttachmentVertices(MeshAttachment* attachment)
{
// Ensure parameters are valid:
if (attachment == nullptr) {
return;
}
AtlasRegion* region = (AtlasRegion*)attachment->getRendererObject();
auto renderObject = (region == nullptr || region->page == nullptr) ? nullptr : (Texture2D*)region->page->getRendererObject();
AttachmentVertices* attachmentVertices = new AttachmentVertices(renderObject, attachment->getWorldVerticesLength() >> 1, attachment->getTriangles().buffer(), attachment->getTriangles().size());
V3F_C4B_T2F* vertices = attachmentVertices->_triangles->verts;
for (int i = 0, ii = 0, nn = attachment->getWorldVerticesLength(); ii < nn; ++i, ii += 2) {
vertices[i].texCoords.u = attachment->getUVs()[ii];
vertices[i].texCoords.v = attachment->getUVs()[ii + 1];
}
attachment->setRendererObject(attachmentVertices, deleteAttachmentVertices);
}
void SpriteFrameAttachmentLoader::deleteAttachmentVertices(void* vertices)
{
// Ensure parameters are valid:
if (vertices != nullptr) {
delete (AttachmentVertices*)vertices;
}
}
I hope this helps anyone who had a similar need for such a feature in Cocos2d-x and Spine! Feel free to ask questions on, improve upon, or critique the code