/******************************************************************************* * Copyright (c) 2013, Esoteric Software * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ #include "SpineTest.h" #include #include #include #include "spine/spine.h" USING_NS_AX; using namespace std; using namespace spine; #define SET_UNIFORM(ps, name, value) \ do \ { \ decltype(value) __v = value; \ auto __loc = (ps)->getUniformLocation(name); \ (ps)->setUniform(__loc, &__v, sizeof(__v)); \ } while (false) #define NUM_SKELETONS 50 #define SPINE_NODE_SCALE_FACTOR 0.4 static Cocos2dTextureLoader textureLoader; PowInterpolation pow2(2); PowOutInterpolation powOut2(2); //SwirlVertexEffect effect(400, powOut2); #define SCALE_SKELETON_NODE(node) \ do \ { \ if (node) \ node->setScale(SPINE_NODE_SCALE_FACTOR); \ } while (false) //------------------------------------------------------------------ // // SpineTestScene // //------------------------------------------------------------------ SpineTests::SpineTests() { auto fu = FileUtils::getInstance(); _searchPaths = fu->getSearchPaths(); fu->addSearchPath("spine", true); ADD_TEST_CASE(BatchingExample); ADD_TEST_CASE(CoinExample); ADD_TEST_CASE(GoblinsExample); ADD_TEST_CASE(IKExample); ADD_TEST_CASE(MixAndMatchExample); ADD_TEST_CASE(RaptorExample); ADD_TEST_CASE(SkeletonRendererSeparatorExample); ADD_TEST_CASE(SpineboyExample); ADD_TEST_CASE(TankExample); #ifdef _AX_DEBUG debugExtension = new DebugExtension(SpineExtension::getInstance()); #endif } SpineTests::~SpineTests() { FileUtils::getInstance()->setSearchPaths(_searchPaths); SkeletonBatch::destroyInstance(); SkeletonTwoColorBatch::destroyInstance(); #ifdef _AX_DEBUG debugExtension->reportLeaks(); delete debugExtension; #endif } SpineTestLayer::SpineTestLayer() : _title("") {} SpineTestLayer::~SpineTestLayer() {} std::string SpineTestLayer::title() const { return _title; } bool SpineTestLayer::init() { if (!TestCase::init()) return false; EventListenerTouchOneByOne* listener = EventListenerTouchOneByOne::create(); listener->onTouchBegan = [this](Touch* touch, ax::Event* event) -> bool { if (!skeletonNode) return true; _touchIndex = (_touchIndex + 1) % 3; if (_touchIndex == 0) { skeletonNode->setDebugBonesEnabled(false); skeletonNode->setTimeScale(1.0f); } else if (_touchIndex == 1) { skeletonNode->setDebugBonesEnabled(true); skeletonNode->setTimeScale(1.0f); } else if (_touchIndex == 2) { skeletonNode->setDebugBonesEnabled(true); skeletonNode->setTimeScale(0.3f); // refer issue: https://github.com/axmolengine/axmol/issues/482 // Get Spine node PS and update uniforms auto skeleton1PS = skeletonNode->getProgramState(); if (skeleton1PS) { Vec2 resolution{100.f, 100.f}; float blurRadius = 50.0f; float sampleNum = 7.0f; // This doesn't work on Spine node, works on sprite node SET_UNIFORM(skeleton1PS, "resolution", resolution); SET_UNIFORM(skeleton1PS, "blurRadius", blurRadius); SET_UNIFORM(skeleton1PS, "sampleNum", sampleNum); } } return true; }; _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this); SCALE_SKELETON_NODE(skeletonNode); return true; } // BatchingExample bool BatchingExample::init() { if (!SpineTestLayer::init()) return false; _title = "Batching"; _atlas = new (__FILE__, __LINE__) Atlas("spineboy.atlas", &textureLoader, true); AXASSERT(_atlas, "Error reading atlas file."); // This attachment loader configures attachments with data needed for cocos2d-x rendering. // Do not dispose the attachment loader until the skeleton data is disposed! _attachmentLoader = new (__FILE__, __LINE__) Cocos2dAtlasAttachmentLoader(_atlas); // Load the skeleton data. SkeletonJson* json = new (__FILE__, __LINE__) SkeletonJson(_attachmentLoader); json->setScale(0.6f); // Resizes skeleton data to 60% of the size it was in Spine. _skeletonData = json->readSkeletonDataFile("spineboy-pro.json"); AXASSERT(_skeletonData, (json->getError().isEmpty() ? json->getError().buffer() : "Error reading skeleton data file.")); delete json; // Setup mix times. _stateData = new (__FILE__, __LINE__) AnimationStateData(_skeletonData); _stateData->setMix("walk", "jump", 0.2f); _stateData->setMix("jump", "run", 0.2f); int xMin = _contentSize.width * 0.10f, xMax = _contentSize.width * 0.90f; int yMin = 0, yMax = _contentSize.height * 0.7f; for (int i = 0; i < NUM_SKELETONS; i++) { // Each skeleton node shares the same atlas, skeleton data, and mix times. SkeletonAnimation* skeletonNode = SkeletonAnimation::createWithData(_skeletonData, false); skeletonNode->setAnimationStateData(_stateData); skeletonNode->setAnimation(0, "walk", true); skeletonNode->addAnimation(0, "jump", true, RandomHelper::random_int(0, 300) / 100.0f); skeletonNode->addAnimation(0, "run", true); // alternative setting two color tint for groups of 10 skeletons // should end up with #skeletons / 10 batches // if (j++ < 10) // skeletonNode->setTwoColorTint(true); // if (j == 20) j = 0; // skeletonNode->setTwoColorTint(true); skeletonNode->setPosition(Vec2(RandomHelper::random_int(xMin, xMax), RandomHelper::random_int(yMin, yMax))); addChild(skeletonNode); } SCALE_SKELETON_NODE(skeletonNode); return true; } BatchingExample::~BatchingExample() { // SkeletonAnimation instances are cocos2d-x nodes and are disposed of automatically as normal, but the data created // manually to be shared across multiple SkeletonAnimations needs to be disposed of manually. delete _skeletonData; delete _stateData; delete _attachmentLoader; delete _atlas; } bool CoinExample::init() { if (!SpineTestLayer::init()) return false; _title = "Coin"; skeletonNode = SkeletonAnimation::createWithBinaryFile("coin-pro.skel", "coin.atlas", 1); skeletonNode->setAnimation(0, "animation", true); skeletonNode->setPosition(Vec2(_contentSize.width / 2, _contentSize.height / 2)); addChild(skeletonNode); scheduleUpdate(); SCALE_SKELETON_NODE(skeletonNode); return true; } bool GoblinsExample::init() { if (!SpineTestLayer::init()) return false; _title = "Goblins"; skeletonNode = SkeletonAnimation::createWithJsonFile("goblins-pro.json", "goblins.atlas", 1.5f); skeletonNode->setAnimation(0, "walk", true); skeletonNode->setSkin("goblin"); skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); addChild(skeletonNode); SCALE_SKELETON_NODE(skeletonNode); return true; } bool IKExample::init() { if (!SpineTestLayer::init()) return false; _title = "IKExample"; // Load the Spineboy skeleton and create a SkeletonAnimation node from it // centered on the screen. skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy-pro.json", "spineboy.atlas", 0.6f); skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); addChild(skeletonNode); // Queue the "walk" animation on the first track. skeletonNode->setAnimation(0, "walk", true); // Queue the "aim" animation on a higher track. // It consists of a single frame that positions // the back arm and gun such that they point at // the "crosshair" bone. By setting this // animation on a higher track, it overrides // any changes to the back arm and gun made // by the walk animation, allowing us to // mix the two. The mouse position following // is performed in the lambda below. skeletonNode->setAnimation(1, "aim", true); // Next we setup a listener that receives and stores // the current mouse location. The location is converted // to the skeleton's coordinate system. EventListenerMouse* mouseListener = EventListenerMouse::create(); mouseListener->onMouseMove = [this](ax::Event* event) -> void { // convert the mosue location to the skeleton's coordinate space // and store it. EventMouse* mouseEvent = dynamic_cast(event); position = skeletonNode->convertToNodeSpace(mouseEvent->getLocationInView()); }; _eventDispatcher->addEventListenerWithSceneGraphPriority(mouseListener, this); // Position the "crosshair" bone at the mouse // location. // // When setting the crosshair bone position // to the mouse position, we need to translate // from "skeleton space" to "local bone space". // Note that the local bone space is calculated // using the bone's parent worldToLocal() function! // // After updating the bone position based on the // converted mouse location, we call updateWorldTransforms() // again so the change of the IK target position is // applied to the rest of the skeleton. skeletonNode->setPostUpdateWorldTransformsListener([this](SkeletonAnimation* node) -> void { Bone* crosshair = node->findBone("crosshair"); // The bone should be cached float localX = 0, localY = 0; crosshair->getParent()->worldToLocal(position.x, position.y, localX, localY); crosshair->setX(localX); crosshair->setY(localY); //crosshair->setAppliedValid(false); node->getSkeleton()->updateWorldTransform(); }); SCALE_SKELETON_NODE(skeletonNode); return true; } MixAndMatchExample::~MixAndMatchExample() { delete skin; delete skin2; } bool MixAndMatchExample::init() { if (!SpineTestLayer::init()) return false; _title = "Mix and Match"; skeletonNode = SkeletonAnimation::createWithBinaryFile("mix-and-match-pro.skel", "mix-and-match.atlas", 0.5); skeletonNode->setAnimation(0, "dance", true); // Create a new skin, by mixing and matching other skins // that fit together. Items making up the girl are individual // skins. Using the skin API, a new skin is created which is // a combination of all these individual item skins. SkeletonData* skeletonData = skeletonNode->getSkeleton()->getData(); skin = new (__FILE__, __LINE__) Skin("mix-and-match"); skin->addSkin(skeletonData->findSkin("skin-base")); skin->addSkin(skeletonData->findSkin("nose/short")); skin->addSkin(skeletonData->findSkin("eyelids/girly")); skin->addSkin(skeletonData->findSkin("eyes/violet")); skin->addSkin(skeletonData->findSkin("hair/brown")); skin->addSkin(skeletonData->findSkin("clothes/hoodie-orange")); skin->addSkin(skeletonData->findSkin("legs/pants-jeans")); skin->addSkin(skeletonData->findSkin("accessories/bag")); skin->addSkin(skeletonData->findSkin("accessories/hat-red-yellow")); skeletonNode->getSkeleton()->setSkin(skin); skeletonNode->setPosition(Vec2(_contentSize.width / 3, _contentSize.height / 2 - 100)); addChild(skeletonNode); SCALE_SKELETON_NODE(skeletonNode); // load hsv as custom, we don't want batch draw auto hsvProg = ProgramManager::getInstance()->loadProgram(positionTextureColor_vert, hsv_frag, VertexLayoutType::Sprite); auto ps1 = new backend::ProgramState(hsvProg); SET_UNIFORM(ps1, "u_hsv", Vec3(92.0f, 1.0f, 1.2f)); ps1->updateBatchId(); skeletonNode->setProgramState(ps1, true); /* -------- skeletonNode2 with same spine animation file ------------ */ auto skeletonNode2 = SkeletonAnimation::createWithBinaryFile("mix-and-match-pro.skel", "mix-and-match.atlas", 0.5); skeletonNode2->setAnimation(0, "dance", true); // Create a new skin, by mixing and matching other skins // that fit together. Items making up the girl are individual // skins. Using the skin API, a new skin is created which is // a combination of all these individual item skins. SkeletonData* skeletonData2 = skeletonNode->getSkeleton()->getData(); skin2 = new (__FILE__, __LINE__) Skin("mix-and-match"); skin2->addSkin(skeletonData->findSkin("skin-base")); skin2->addSkin(skeletonData->findSkin("nose/short")); skin2->addSkin(skeletonData->findSkin("eyelids/girly")); skin2->addSkin(skeletonData->findSkin("eyes/violet")); skin2->addSkin(skeletonData->findSkin("hair/brown")); skin2->addSkin(skeletonData->findSkin("clothes/hoodie-orange")); skin2->addSkin(skeletonData->findSkin("legs/pants-jeans")); skin2->addSkin(skeletonData->findSkin("accessories/bag")); skin2->addSkin(skeletonData->findSkin("accessories/hat-red-yellow")); skeletonNode2->getSkeleton()->setSkin(skin2); skeletonNode2->setPosition(Vec2(_contentSize.width / 1.5, _contentSize.height / 2 - 100)); addChild(skeletonNode2); SCALE_SKELETON_NODE(skeletonNode2); auto ps2 = new backend::ProgramState(hsvProg); SET_UNIFORM(ps2, "u_hsv", Vec3(-45.0f, 1.0f, 1.2f)); ps2->updateBatchId(); skeletonNode2->setProgramState(ps2, true); return true; } bool RaptorExample::init() { if (!SpineTestLayer::init()) return false; _title = "Raptor"; skeletonNode = SkeletonAnimation::createWithJsonFile("raptor-pro.json", "raptor.atlas", 0.5f); skeletonNode->setAnimation(0, "walk", true); skeletonNode->addAnimation(1, "gun-grab", false, 2); skeletonNode->setTwoColorTint(true); //effect.setCenterY(200); swirlTime = 0; //skeletonNode->setVertexEffect(&effect); skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); addChild(skeletonNode); scheduleUpdate(); SCALE_SKELETON_NODE(skeletonNode); return true; } void RaptorExample::update(float fDelta) { swirlTime += fDelta; float percent = spine::MathUtil::fmod(swirlTime, 2); if (percent > 1) percent = 1 - (percent - 1); //effect.setAngle(pow2.interpolate(-60.0f, 60.0f, percent)); } bool SkeletonRendererSeparatorExample::init() { if (!SpineTestLayer::init()) return false; _title = "Seperator"; // Spineboy's back, which will manage the animation and GPU resources // will render only the front slots of Spineboy skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy-pro.json", "spineboy.atlas", 0.6f); skeletonNode->setMix("walk", "jump", 0.4); skeletonNode->setAnimation(0, "walk", true); skeletonNode->setSlotsRange(skeletonNode->findSlot("rear-upper-arm")->getData().getIndex(), skeletonNode->findSlot("rear-shin")->getData().getIndex()); skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); // A simple rectangle to go between the front and back slots of Spineboy betweenNode = DrawNode::create(); Vec2 rect[4]; rect[0] = Vec2(0, 0); rect[1] = Vec2(40, 0); rect[2] = Vec2(40, 200); rect[3] = Vec2(0, 200); betweenNode->drawPolygon(rect, 4, Color4F(1, 0, 0, 1), 1, Color4F(1, 0, 0, 1)); betweenNode->setPosition(Vec2(_contentSize.width / 2 + 30, 20)); // Spineboy's front, doesn't manage any skeleton, animation or GPU resources, but simply // renders the back slots of Spineboy. The skeleton, animatio state and GPU resources // are shared with the front node! frontNode = SkeletonRenderer::createWithSkeleton(skeletonNode->getSkeleton()); frontNode->setSlotsRange(frontNode->findSlot("neck")->getData().getIndex(), -1); frontNode->setPosition(Vec2(_contentSize.width / 2, 20)); // Add the front, between and back node in the correct order to this scene addChild(skeletonNode); addChild(betweenNode); addChild(frontNode); scheduleUpdate(); SCALE_SKELETON_NODE(skeletonNode); SCALE_SKELETON_NODE(frontNode); return true; } void SkeletonRendererSeparatorExample::update(float deltaTime) { // Test releasing memory. // Director::getInstance()->replaceScene(SpineboyExample::scene()); } bool SpineboyExample::init() { if (!SpineTestLayer::init()) return false; _title = "Spineboy"; skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy-pro.json", "spineboy.atlas", 0.6f); skeletonNode->setStartListener([](TrackEntry* entry) { log("%d start: %s", entry->getTrackIndex(), entry->getAnimation()->getName().buffer()); }); skeletonNode->setInterruptListener([](TrackEntry* entry) { log("%d interrupt", entry->getTrackIndex()); }); skeletonNode->setEndListener([](TrackEntry* entry) { log("%d end", entry->getTrackIndex()); }); skeletonNode->setCompleteListener([](TrackEntry* entry) { log("%d complete", entry->getTrackIndex()); }); skeletonNode->setDisposeListener([](TrackEntry* entry) { log("%d dispose", entry->getTrackIndex()); }); skeletonNode->setEventListener([](TrackEntry* entry, spine::Event* event) { log("%d event: %s, %d, %f, %s", entry->getTrackIndex(), event->getData().getName().buffer(), event->getIntValue(), event->getFloatValue(), event->getStringValue().buffer()); }); skeletonNode->setMix("walk", "jump", 0.4); skeletonNode->setMix("jump", "run", 0.4); skeletonNode->setAnimation(0, "walk", true); TrackEntry* jumpEntry = skeletonNode->addAnimation(0, "jump", false, 1); skeletonNode->addAnimation(0, "run", true); skeletonNode->setTrackStartListener(jumpEntry, [](TrackEntry* entry) { log("jumped!"); }); // skeletonNode->addAnimation(1, "test", true); // skeletonNode->runAction(RepeatForever::create(Sequence::create(FadeOut::create(1), FadeIn::create(1), // DelayTime::create(5), NULL))); skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); addChild(skeletonNode); auto program = ProgramManager::getInstance()->loadProgram(positionTextureColor_vert, "custom/example_Blur_fs", VertexLayoutType::Sprite); skeletonNode->setProgramState(new backend::ProgramState(program), true); //auto skeleton1PS = skeletonNode->getProgramState(); //Vec2 resolution{100.f, 100.f}; //float blurRadius = 50.0f; //float sampleNum = 7.0f; //SET_UNIFORM(skeleton1PS, "resolution", resolution); //SET_UNIFORM(skeleton1PS, "blurRadius", blurRadius); //SET_UNIFORM(skeleton1PS, "sampleNum", sampleNum); scheduleUpdate(); SCALE_SKELETON_NODE(skeletonNode); return true; } void SpineboyExample::update(float deltaTime) { // Test releasing memory. // Director::getInstance()->replaceScene(SpineboyExample::scene()); } bool TankExample::init() { if (!SpineTestLayer::init()) return false; _title = "Tank"; skeletonNode = SkeletonAnimation::createWithBinaryFile("tank-pro.skel", "tank.atlas", 0.5f); skeletonNode->setAnimation(0, "shoot", true); skeletonNode->setPosition(Vec2(_contentSize.width / 2 + 400, 20)); addChild(skeletonNode); SCALE_SKELETON_NODE(skeletonNode); return true; }