1、需求情况

书之国中需要一个人物捏脸系统,要求可以让用户自由选择身体不同部位的形象,比如头发、眼睛、眉毛、上衣、裤子等。已经支持RegionAttacment和MeshAttachment,而且在realtime模式下也可以正常工作。cocoscreator v2.4.4版本验证通过。推荐使用外部图片进行局部换装.

2、方案探索

2.1 多attachment切换

由于spine动画的结构为bone→slot→attachment(即附件、图片),我们可以在动画文件中针对同一个部位(同一个slot)下做多个attachment,然后根据用户的选择进行切换attachment就行。
优点:web、native等多端统一代码。
缺点:随着可换装的部位越多、同一个部位皮肤越多,动画文件变得越来越大,由于spine动画文件是一次性加载进内存等,导致占用内存较多,实例化速度变慢。

主要代码如下:

// 局部换装 skinname一般默认为default
changePartialCloth(skeleton: sp.Skeleton, slotName: string, targetSkinName:string, targetAttaName:string) {// console.log('change cloth:', slotName, targetSkinName, targetAttaName);const slot = skeleton.findSlot(slotName);const skeletonData = skeleton.skeletonData.getRuntimeData();const skin = skeletonData.findSkin(targetSkinName);const slotIndex = skeletonData.findSlotIndex(slotName);const attachment = skin.getAttachment(slotIndex, targetAttaName);if (!slot || !attachment) {cc.error(slot && attachment, "slots: " + slotName + ", attach: " + targetAttaName + " not exists!");return;}slot.setAttachment(attachment);// 如果spine使用了private或者shared等缓存模式,则需要更新缓存。skeleton.invalidAnimationCache();
}

2.2 使用外部图片更新局部皮肤

由于attachemnt即是图片资源在spine内的表达,我们可以通过加载一张外部图片来更新attachment达到局部换装功能。
优点:spine动画每个部位可以只做一个attachment,这样动画文件结构简单,体积较小,内存占用较小加载速度也较快。
缺点:一是由于引擎本身不提供此功能,需要自己动手实现,而且web端和native端需要两套代码,必须修改引擎代码并重新编译引擎。二是动画在使用realtime模式时修改一个动画会影响使用同一个动画文件创建的其他动画,不过这个可以通过复制一份skeletonData来解决。如果皮肤套件特别多,这种方式不失为最佳方案。

2.2.1 web端代码:

  // 深拷贝一份skeletonData防止realtime下换装影响别的动画copySkeletonData(skeleton: sp.Skeleton) {// 复制一份skeletonData,换装时防止realtime模式下影响到别的动画let spData: sp.SkeletonData = skeleton.skeletonData;let copy = new sp.SkeletonData();cc.js.mixin(copy, spData);copy._uuid = spData._uuid + '_' + Date.now() + '_copy';let old = copy.name;let newName = copy.name + '_copy';copy.name = newName;copy.atlasText = copy.atlasText.replace(old, newName);copy.textureNames[0] = newName + '.png';copy.init && copy.init();skeleton.skeletonData = copy;}//  copySkeletonData(skeleton: sp.Skeleton) {//     // 复制一份skeletonData,换装时防止realtime模式下影响到别的动画//     let animation = skeleton.animation;//     let spData: sp.SkeletonData = skeleton.skeletonData;//     let copy = new sp.SkeletonData();//     cc.js.mixin(copy, spData);//     // @ts-ignore//     copy._uuid = Tool.uuid();//     let textureNames = copy['textureNames'];//     let copyTextureNames = [];//     for (let i = 0; i < textureNames.length; i++) {//         copy.atlasText = copy.atlasText.replace(textureNames[i], 'copy_' + textureNames[i]);//         copyTextureNames.push('copy_' + textureNames[i]);//     }//     copy['textureNames'] = copyTextureNames;//     // @ts-ignore//     copy.init && copy.init();//     skeleton.skeletonData = copy;//     skeleton.animation = animation;//     return copy;// }
// 使用外部图片换装changePartialWithExternalTexture(ani: sp.Skeleton, slotName: string, tex2d: cc.Texture2D) {let slot: sp.spine.Slot = ani.findSlot(slotName);let attach: sp.spine.RegionAttachment | sp.spine.MeshAttachment = slot.getAttachment() as (sp.spine.RegionAttachment | sp.spine.MeshAttachment);let spineTexture: sp.SkeletonTexture = new sp.SkeletonTexture({ width: tex2d.width, height: tex2d.height });spineTexture.setRealTexture(tex2d);// 单张图片可以不用创建page// let page = new sp.spine.TextureAtlasPage();// page.name = tex2d.name;// page.uWrap = sp.spine.TextureWrap.ClampToEdge;// page.vWrap = sp.spine.TextureWrap.ClampToEdge;// page.texture = spineTexture;// page.texture.setWraps(page.uWrap, page.vWrap);// page.width = tex2d.width;// page.height = tex2d.height;// let region: sp.spine.TextureAtlasRegion = new sp.spine.TextureAtlasRegion();let region: sp.spine.TextureAtlasRegion = attach.region as sp.spine.TextureAtlasRegion;// region.page = page;region.width = tex2d.width;region.height = tex2d.height;region.originalWidth = tex2d.width;region.originalHeight = tex2d.height;region.rotate = false;region.u = 0;region.v = 0;region.u2 = 1;region.v2 = 1;// 换图后可以通过设置x、y偏移量来对准位置(如果切图有偏差)// region.offsetX = 300;// region.offsetY = 200;region.texture = spineTexture;region.renderObject = region;// 如果不修改attach的大小则新图片会被限制在原始图片大小范围内attach.width = tex2d.width;attach.height = tex2d.height;cc.log(attach);if (attach instanceof sp.spine.MeshAttachment) {attach.updateUVs();} else {attach.setRegion(region);attach.updateOffset();}// ani 如果使用了缓存模式则需要刷新缓存, 一般换装为了不英雄别的动画都需要设置缓存模式为privite_cacheani.invalidAnimationCache();}
下面的代码已经废弃, 请使用上面的代码
updatePartialSkin(ani: sp.Skeleton, tex2d: cc.Texture2D, slotsName: string) {let slot: sp.spine.Slot = ani.findSlot(slotsName);let attachment: sp.spine.RegionAttachment = slot.getAttachment() as sp.spine.RegionAttachment;if (!slot || !attachment) {cc.error('error...');return;}let region: sp.spine.TextureAtlasRegion = attachment.region as sp.spine.TextureAtlasRegion;let skeletonTexture = new sp.SkeletonTexture();skeletonTexture.setRealTexture(this.tex2d);region.u = 0;region.v = 0;region.u2 = 1;region.v2 = 1;region.width = tex2d.width;region.height = tex2d.height;region.originalWidth = tex2d.width;region.originalHeight = tex2d.height;region.rotate = false;region.texture = skeletonTexture;region.page = null;attachment.width = region.width;attachment.height = region.height;attachment.setRegion(region);// mark: 不需要创建新的sp.spine.TextureAtlasRegion, 直接更新原attachment下的region即可。// let region: sp.spine.TextureRegion = this.createRegion(tex2d);// attachment.setRegion(region);// attachment.width = region.width;// attachment.height = region.height;attachment.updateOffset();slot.setAttachment(attachment);// skeleton如果使用了缓存模式则需要刷新缓存ani.invalidAnimationCache();
}createRegion(tex: cc.Texture2D): sp.spine.TextureAtlasRegion {cc.log('创建region');let skeletonTexture = new sp.SkeletonTexture();skeletonTexture.setRealTexture(tex);// mark: 可以不设置page// let page = new sp.spine.TextureAtlasPage();// page.name = tex.name;// page.uWrap = sp.spine.TextureWrap.ClampToEdge;// page.vWrap = sp.spine.TextureWrap.ClampToEdge;// page.texture = skeletonTexture;// page.texture.setWraps(page.uWrap, page.vWrap);// page.width = tex.width;// page.height = tex.height;let region = new sp.spine.TextureAtlasRegion();// region.page = page;region.width = tex.width;region.height = tex.height;region.originalWidth = tex.width;region.originalHeight = tex.height;region.rotate = false;region.u = 0;region.v = 0;region.u2 = 1;region.v2 = 1;region.texture = skeletonTexture;return region;
}

2.2.2 native端代码:

native端我们需要分别修改C++实现和jsb-adapter, C++实现我们要分别在 SkeletonRenderer.cpp和SkeletonCacheAnimation.cpp 添加对应的方法。C++代码在cocos2d-x目录下,我们可以git上下载对应版本的最新代码。

SkeletonRenderer.cpp

void SkeletonRenderer::updateRegion(const std::string &slotName, cocos2d::middleware::Texture2D *texture) {// auto skeletonData = _skeleton->getData();// auto slotIndex = skeletonData->findSlotIndex(String(slotName.c_str()));// auto skin = skeletonData->findSkin(String("default"));// RegionAttachment * attachment = (RegionAttachment *)skin->getAttachment(slotIndex, String("cap_1"));Slot *slot = _skeleton->findSlot(slotName.c_str());RegionAttachment *attachment = (RegionAttachment *)slot->getAttachment();// Texture *texture2D = texture->getNativeTexture();// float width = texture2D->getWidth();// float height = texture2D->getHeight();float wide = texture->getPixelsWide();float high = texture->getPixelsHigh();attachment->setUVs(0, 0, 1, 1, false);attachment->setRegionWidth(wide);attachment->setRegionHeight(high);attachment->setRegionOriginalWidth(wide);attachment->setRegionOriginalHeight(high);attachment->setWidth(wide);attachment->setHeight(high);// attachment->setRegionOffsetX(0);// attachment->setRegionOffsetY(15);// texture->setPixelsWide(width);// texture->setPixelsHigh(height);// texture->setRealTextureIndex(1);AttachmentVertices *attachV = (AttachmentVertices *)attachment->getRendererObject();if (attachV->_texture == texture) {return;}CC_SAFE_RELEASE(attachV->_texture);attachV->_texture = texture;CC_SAFE_RETAIN(texture);V2F_T2F_C4B *vertices = attachV->_triangles->verts;for (int i = 0, ii = 0; i < 4; ++i, ii += 2){vertices[i].texCoord.u = attachment->getUVs()[ii];vertices[i].texCoord.v = attachment->getUVs()[ii + 1];}attachment->updateOffset();slot->setAttachment(attachment);
}

SkeletonCacheAnimation.cpp

void SkeletonCacheAnimation::updateRegion(const std::string &slotName, cocos2d::middleware::Texture2D *texture)
{_skeletonCache->updateRegion(slotName, texture);
}

修改C++代码后我们需要重新跑一般自动绑定脚本,生成js绑定接口,目录在cocos2dx/tools/tojs/genbindings.py,绑定成功后我们需要修改jsb adapter以提供给js层调用,
adapter在引擎安装目录下/Resources/builtin/jsb-adapter/engine/jsb-spine-skeleton.js ,添加如下方法:

skeleton.updateRegion = function (slotsName, jsbTex2d) {if (this._nativeSkeleton) {this._nativeSkeleton.updateRegion(slotsName, jsbTex2d);return true;}return false;
};

全部修改完成后我们需要在creator引擎中自定义cocos2d-x引擎,指向我们刚修改的cocos2d-x目录。如果想要在模拟器预览效果我们还需要重新编译模拟器,具体教程可以在https://docs.cocos.com/creator/manual/zh/查看。

2.2.3 使用方法:

以上全部修改完成后,我们可以在js/ts代码中这样使用了:

changeClouth() {if (cc.sys.isNative) {cc.log('native 换肤.');let jsbTex = new middleware.Texture2D();jsbTex.setPixelsHigh(this.tex2d.height);jsbTex.setPixelsWide(this.tex2d.width);jsbTex.setNativeTexture(this.tex2d.getImpl());this.player.updateRegion("cap", jsbTex);} else {cc.log('web 换肤.');this.updatePartialSkin(this.player, this.tex2d, 'cap');}// 缓存模式下需要刷新缓存this.player.invalidAnimationCache();
}

2.3 使用Spine挂点功能

Spine挂点功能是cocoscreator 2.3版本开始提供的,初衷是为了动态给动画添加部分节点,比如武器等,这里也可以非常规使用来做局部换皮。具体流程为生成挂点——>获取指定节点——>给该节点添加对应的子节点。

优点: 引擎提供的功能,三端表现统一,无需hack源码

缺点: 如果需要换装的图集过多无法合并到一张图集上,则每增加一个挂载节点都会增加一个drawcall,这里要特别注意。

2.3.1 使用示例

             // this.ani: sp.Skeletonlet node = new cc.Node();let sp = node.addComponent(cc.Sprite);sp.spriteFrame = this.spf;let attachUtil = this.ani.attachUtil;// attachUtil.generateAttachedNodes("hair");// let boneNodes = attachUtil.generateAttachedNodes(boneName);// let boneNode = boneNodes[0];attachUtil.generateAllAttachedNodes();let bones = attachUtil.getAttachedNodes('hair');bones[0].destroyAllChildren();bones[0].addChild(node);bones = attachUtil.getAttachedNodes('left_hand_a');let node2 = cc.instantiate(node);bones[0].destroyAllChildren();bones[0].addChild(node2);bones = attachUtil.getAttachedNodes('right_hand_a');let node3 = cc.instantiate(node);bones[0].destroyAllChildren();bones[0].addChild(node3);// attachUtil.destroyAttachedNodes('hair');// attachUtil.destroyAllAttachedNodes();

2.3.2 主要用到的接口在AttachUtil.js里都可以找到

/*** !#en Traverse all bones to generate the minimum node tree containing the given bone names, NOTE that make sure the skeleton has initialized before calling this interface.* !#zh 遍历所有插槽,生成包含所有给定插槽名称的最小节点树,注意,调用该接口前请确保骨骼动画已经初始化好。* @method generateAttachedNodes* @param {String} boneName* @return {Node[]} attached node array*/generateAttachedNodes (boneName) {let targetNodes = [];if (!this._inited) return targetNodes;let rootNode = this._prepareAttachNode();if (!rootNode) return targetNodes;let res = [];let bones = this._skeleton.bones;for (let i = 0, n = bones.length; i < n; i++) {let bone = bones[i];let boneData = bone.data;if (boneData.name == boneName) {res.push(bone);}}let buildBoneTree = function (bone) {if (!bone) return;let boneData = bone.data;let boneNode = this._getNodeByBoneIndex(boneData.index);if (boneNode) return boneNode;boneNode = this._buildBoneAttachedNode(bone, boneData.index);let parentBoneNode = buildBoneTree(bone.parent) || rootNode;boneNode.parent = parentBoneNode;return boneNode;}.bind(this);for (let i = 0, n = res.length; i < n; i++) {let targetNode = buildBoneTree(res[i]);targetNodes.push(targetNode);}this._sortNodeArray();return targetNodes;},/*** !#en Destroy attached node which you want.* !#zh 销毁对应的挂点* @method destroyAttachedNodes* @param {String} boneName*/destroyAttachedNodes (boneName) {if (!this._inited) return;let nodeArray = this._attachedNodeArray;let markTree = function (rootNode) {let children = rootNode.children;for (let i = 0, n = children.length; i < n; i++) {let c = children[i];if (c) markTree(c);}rootNode._toRemove = true;}for (let i = 0, n = nodeArray.length; i < n; i++) {let boneNode = nodeArray[i];if (!boneNode || !boneNode.isValid) continue;let delName = boneNode.name.split(ATTACHED_PRE_NAME)[1];if (delName === boneName) {markTree(boneNode);boneNode.removeFromParent(true);boneNode.destroy();nodeArray[i] = null;}}this._rebuildNodeArray();},/*** !#en Traverse all bones to generate a tree containing all bones nodes, NOTE that make sure the skeleton has initialized before calling this interface.* !#zh 遍历所有插槽,生成包含所有插槽的节点树,注意,调用该接口前请确保骨骼动画已经初始化好。* @method generateAllAttachedNodes* @return {cc.Node} root node*/generateAllAttachedNodes () {if (!this._inited) return;// clear all recordsthis._boneIndexToNode = {};this._attachedNodeArray.length = 0;let rootNode = this._prepareAttachNode();if (!rootNode) return;let bones = this._skeleton.bones;for (let i = 0, n = bones.length; i < n; i++) {let bone = bones[i];let boneData = bone.data;let parentNode = null;if (bone.parent) {let parentIndex = bone.parent.data.index;parentNode = this._boneIndexToNode[parentIndex];} else {parentNode = rootNode;}if (parentNode) {let boneNode = parentNode.getChildByName(ATTACHED_PRE_NAME + boneData.name);if (!boneNode || !boneNode.isValid) {boneNode = this._buildBoneAttachedNode(bone, boneData.index);parentNode.addChild(boneNode);} else {this._buildBoneRelation(boneNode, bone, boneData.index);}}}return rootNode;},/*** !#en Destroy all attached node.* !#zh 销毁所有挂点* @method destroyAllAttachedNodes*/destroyAllAttachedNodes () {this._attachedRootNode = null;this._attachedNodeArray.length = 0;this._boneIndexToNode = {};if (!this._inited) return;let rootNode = this._skeletonNode.getChildByName(ATTACHED_ROOT_NAME);if (rootNode) {rootNode.removeFromParent(true);rootNode.destroy();rootNode = null;}}

3、结论

综合对比,如果动画不大所需替换部位不太多的话可以考虑第一种多attachment切换,否则的话考虑第三种利用挂点功能,至于第二种需要修改引擎代码并进行充分测试。无论哪种解决方案都需要和动画师进行沟通协调确定制作方案。

spine使用外部图片进行局部换装

cocoscreator中spine局部换皮的探索相关推荐

  1. cocoscreator 中 spine局部换皮

    1 多attachment切换 let goblingirl = this.spine2.findSlot("left-arm");let attachment = gobling ...

  2. Unity Spine 局部换皮

    1.Spine元素主要包含皮肤(Skin).骨骼(Bone).插槽(Slot).附件(Attachment).及附件下的图片. 2.而皮肤(Skin)包含了插槽信息.附件信息,如果我们有两套相同构成的 ...

  3. spine 动态换皮功能

    前言: cocos2dx 中spine 的换皮功能是一种静态的方法,也就是在创建 spine 动画的时候就必须将所有的皮肤全部加载,然后在代码中直接换皮,并且这种换皮是整体的切换,对于我们实际开发中这 ...

  4. 梦幻西游人物局部换色补完

    梦幻西游人物局部换色补完 作者:leexuany(小宝) 小宝上次写文章简单介绍了梦幻西游中人物局部换色使用的方法,但由于没有具体的调色板变换算法,到头来也只是纸上谈兵.所以小宝花了几天时间跟踪梦幻的 ...

  5. Spine动画局部换装(切换武器)

    最近在用spine动画做微信小游戏,涉及到角色的武器升级后会切换到另一形态,就研究了一下spine的换装,搜了一些资料发现专门介绍的资料有点少,而且spine官网上说的也不太清楚,好在最后搞出来了,记 ...

  6. laya龙骨换装_分享:Dragonbones/Spine的换肤操作

    目前LayaAir下支持龙骨的局部换肤(根据插槽索引换肤.根据插槽name换肤.纹理换肤.网格换肤).全局换肤 需注意: 1.Dragonbones不支持全局换肤,Spine支持全局换肤 2.使用到I ...

  7. 【CocosCreator入门】CocosCreator组件 | Spine(骨骼动画)组件

            Cocos Creator 是一款流行的游戏开发引擎,具有丰富的组件和工具,其中Spine 是一个基于骨骼系统的 2D 动画工具,它可以让开发者通过对骨架和关键帧的调整来制作出更加自然 ...

  8. Egret之龙骨卡槽(slot)换皮

    龙骨的图片是绑定在卡槽上的.并且是一对一的关系.所以可以通过对骨架的卡槽上绑定的图片的更换来实现另一种换皮的效果. 换皮的核心代码: //针对slot设置其新内容private setNewSlot( ...

  9. 木兰编程语言python_国产编程语言木兰换皮Python 中科院重罚当事人

    最近打着中科院计算所出品.完全自主开发旗号的国产编程语言"木兰"引发广泛关注,但很快被发现是基于Python语言套壳.换皮而来的产物.面对质疑,中科院计算所编译实验室员工.&quo ...

最新文章

  1. C 家族程序设计语言发展史
  2. opencv python 图片腐蚀和膨胀
  3. 【转】 asp.net从视频文件中抓取一桢并生成图像文件的方法 实现多语言本地化应用程序 自动返回上次请求页面...
  4. C++面向对象基础(二)
  5. delphi valuelisteditor控件的使用
  6. ida调试linux程序,MAC使用IDA PRO远程调试LINUX程序
  7. 热血上头!程序员想拍桌子离职的1000个瞬间...
  8. volatile简记
  9. 从字符串中提取(“获取”)一个数字
  10. EMNLP'21 | 基于互指导和句间关系图的论点对抽取
  11. struts2 获得前端数据:
  12. 酒店订房管理系统php,酒店预定管理系统(源码+数据库+文档)
  13. 开源办公系统:支持在线Office在线编辑、文档协同
  14. web 使网站在浏览器中全屏显示 fullscreen=yes
  15. Spring Cloud如何可用于微服务架构
  16. 双非本科,三年逆袭鹅厂,靠的不仅仅是努力
  17. 西安交通大学计算机考研资料汇总
  18. 安卓recovery菜单中英文对照
  19. vue中安装和使用Dplayer视频播放器
  20. vue 移动端拨打电话

热门文章

  1. 虹科方案|用 Western Digital 和ATTO技术优化 SMR 存储解决方案的大数据工作负载
  2. Redis系列(三)-Redis哨兵模式(一篇文章让你全面的了解reids哨兵模式)
  3. 如何快速生成数据库字典
  4. Android 获取屏幕指定坐标的颜色
  5. 胎压监测系统TPMS
  6. Windows下安装NTP服务器
  7. [转]汉字转换为拼音
  8. c语言结构体定义坐标,C语言结构体定义的方式
  9. c语言实现扫雷秒杀挂
  10. 九龙证券|可转债一级市场回暖 14家上市公司可转债发行集中获批