cocoscreator中spine局部换皮的探索

cocoscreator 专栏收录该内容
9 篇文章 2 订阅

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_cache
        ani.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.cppSkeletonCacheAnimation.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.Skeleton
				let 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 records
        this._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使用外部图片进行局部换装

  • 0
    点赞
  • 5
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值