深度解读dragonBones使用SpriteFrame任意换肤的实现

简介: 深度解读dragonBones使用SpriteFrame任意换肤的实现

需求背景

龙骨文件比较复杂,和需求关联比较紧密的结构如下:

image.png

期望的效果如下,使用spriteFrame/atlas替换Armature,达到局部/整体换肤的效果

image.png

比如上图中使用了resources/yu.plistyu的Armature进行了换肤

源码分析

切入点

js
复制代码
// 这个是渲染龙骨纹理的核心代码
material = _getSlotMaterial(
    slot.getTexture(), // 核心思路就是在这里做文章,达到换肤的效果
    slot._blendMode
);
getTexture () {
    return this._textureData && this._textureData.spriteFrame && this._textureData.spriteFrame.getTexture();
},

slot.texture来源

image.png

kotlin

复制代码

this._displayData = this._displayDatas[this._displayIndex];

image.png

来自这个rawDisplayData

js
复制代码
set: function (value) {
    if (this._rawDisplayDatas === value) {
        return;
    }
    this._displayDirty = true;
    this._rawDisplayDatas = value;
    if (this._rawDisplayDatas !== null) {
        this._displayDatas.length = this._rawDisplayDatas.length;
        // 将rawDisplayData覆盖到_displayDatas,源头就指向了_rawDisplayDatas
        for (var i = 0, l = this._displayDatas.length; i < l; ++i) {
            var rawDisplayData = this._rawDisplayDatas[i];
            if (rawDisplayData === null) {
                rawDisplayData = this._getDefaultRawDisplayData(i);
            }
            this._displayDatas[i] = rawDisplayData;
        }
    }
}
js
复制代码
  BaseFactory.prototype._buildSlots = function (dataPackage, armature) {
        var currentSkin = dataPackage.skin; // 来着skin
        var defaultSkin = dataPackage.armature.defaultSkin;
        if (currentSkin === null || defaultSkin === null) {
            return;
        }
        var skinSlots = {};
        for (var k in defaultSkin.displays) {
            var displays = defaultSkin.getDisplays(k);
            skinSlots[k] = displays;// 来自displays
        }
        if (currentSkin !== defaultSkin) {
            for (var k in currentSkin.displays) {
                var displays = currentSkin.getDisplays(k);
                skinSlots[k] = displays;// 来自displays
            }
        }
        for (var _i = 0, _a = dataPackage.armature.sortedSlots; _i < _a.length; _i++) {
            var slotData = _a[_i];
            var displayDatas = slotData.name in skinSlots 
                ? skinSlots[slotData.name] // 往上找来源
                : null;
            var slot = this._buildSlot(dataPackage, slotData, armature);
            slot.rawDisplayDatas = displayDatas; // 这里,此时displayData.texture=null,哪里修改了texture呢?应该是某个地方
            if (displayDatas !== null) {
                var displayList = new Array();
                // for (const displayData of displays) 
                for (var i = 0, l = dragonBones.DragonBones.webAssembly ? displayDatas.size() : displayDatas.length; i < l; ++i) {
                    var displayData = dragonBones.DragonBones.webAssembly ? displayDatas.get(i) : displayDatas[i];
                    if (displayData !== null) {
                        // 这里会修改texture
                        displayList.push(this._getSlotDisplay(dataPackage, 
                        displayData,// 引用参数
                        null, slot));
                    }
                    else {
                        displayList.push(null);
                    }
                }
                slot._setDisplayList(displayList);
            }
            slot._setDisplayIndex(slotData.displayIndex, true);
        }
    };
js
复制代码
BaseFactory.prototype._getSlotDisplay = function (dataPackage, displayData, rawDisplayData, slot) {
    var dataName = dataPackage !== null ? dataPackage.dataName : displayData.parent.parent.parent.name;
    var display = null;
    switch (displayData.type) {
        case 0 /* Image */: {
            var imageDisplayData = displayData;
            if (dataPackage !== null && dataPackage.textureAtlasName.length > 0) {
                imageDisplayData.texture = this._getTextureData(dataPackage.textureAtlasName, displayData.path);
                // 最终会去dragonBones.CCTextureAtlasData.textures里面取
            }
js
复制代码
 BaseFactory.prototype.buildArmature = function (armatureName, dragonBonesName, skinName, textureAtlasName) {
        if (dragonBonesName === void 0) { dragonBonesName = ""; }
        if (skinName === void 0) { skinName = ""; }
        if (textureAtlasName === void 0) { textureAtlasName = ""; }
        var dataPackage = new BuildArmaturePackage();
        // 这里会填充dataPackage.skin,也就是上边用到的
        if (!this._fillBuildArmaturePackage(dataPackage, dragonBonesName || "", armatureName, skinName || "", textureAtlasName || "")) {
            console.warn("No armature data: " + armatureName + ", " + (dragonBonesName !== null ? dragonBonesName : ""));
            return null;
        }
        var armature = this._buildArmature(dataPackage);
        this._buildBones(dataPackage, armature);
        this._buildSlots(dataPackage, armature); // 来自这个dataPackage
        this._buildConstraints(dataPackage, armature);
        armature.invalidUpdate(null, true);
        armature.advanceTime(0.0); // Update armature pose.
        return armature;
    };
js
复制代码
BaseFactory.prototype._fillBuildArmaturePackage = function (dataPackage, dragonBonesName, armatureName, skinName, textureAtlasName) {
        var dragonBonesData = null;
        var armatureData = null;
        if (dragonBonesName.length > 0) {
            if (dragonBonesName in this._dragonBonesDataMap) {
                // 从map取得
                dragonBonesData = this._dragonBonesDataMap[dragonBonesName];
                armatureData = dragonBonesData.getArmature(armatureName);
            }
        }
        if (armatureData !== null) {
            if (dataPackage.skin === null) {
                dataPackage.skin = armatureData.defaultSkin;// skin的真正源头
            }
            return true;
        }
        return false;
    };
js
复制代码
    ObjectDataParser.prototype.parseTextureAtlasData = function (rawData, textureAtlasData, scale) {
        if (dragonBones.DataParser.SUB_TEXTURE in rawData) {
            var rawTextures = rawData[dragonBones.DataParser.SUB_TEXTURE];
            for (var i = 0, l = rawTextures.length; i < l; ++i) {
                var rawTexture = rawTextures[i];
                var textureData = textureAtlasData.createTexture();
                textureAtlasData.addTexture(textureData);
            }
        }
        return true;
    };
js
复制代码
dragonBones.CCTextureAtlasData = cc.Class({
    extends: dragonBones.TextureAtlasData,
    name: "dragonBones.CCTextureAtlasData",
    properties: {
        _renderTexture: {
            default: null,
            serializable: false
        },
        renderTexture: {
            get () {
                return this._renderTexture;
            },
            set (value) {
                this._renderTexture = value;
                if (value) {
                    for (let k in this.textures) {
                        let textureData = this.textures[k];
                        if (!textureData.spriteFrame) {
                            let rect = null;
                            if (textureData.rotated) {
                                rect = cc.rect(textureData.region.x, textureData.region.y,
                                    textureData.region.height, textureData.region.width);
                            } else {
                                rect = cc.rect(textureData.region.x, textureData.region.y,
                                    textureData.region.width, textureData.region.height);
                            }
                            let offset = cc.v2(0, 0);
                            let size = cc.size(rect.width, rect.height);
                            textureData.spriteFrame = new cc.SpriteFrame();
                            textureData.spriteFrame.setTexture(value, rect, false, offset, size);
                        }
                    }
                } else {
                    for (let k in this.textures) {
                        let textureData = this.textures[k];
                        textureData.spriteFrame = null;
                    }
                }
            },
        }
    },
    statics: {
        toString: function () {
            return "[class dragonBones.CCTextureAtlasData]";
        }
    },
    createTexture : function() {
        return dragonBones.BaseObject.borrowObject(dragonBones.CCTextureData);
    }
});

整体思路图

image.png

换肤实现

换肤的基础

上图分析过程整个链路还是挺长的,不过仍旧可以在slot.getTexture上做文章,因为在提交渲染时有material的判断,应该是考虑到dragonbones会输出多个纹理

ini
复制代码
if (_mustFlush || material.getHash() !== _renderer.material.getHash()) {
    _mustFlush = false;
    _renderer._flush();
    _renderer.node = _node;
    _renderer.material = material;
}

所以我们任意切换纹理也问题不大,Engine是支持的,唯一需要我们处理好的是顶点坐标,否则会发生纹理错位

方法1:修改slot._textureData.spriteFrame实现换肤(失败)

我观察到渲染取的是_textureData.spriteFrame,我尝试给slot增加一个setTexture接口,直接修改了_textureData.spriteFrame会导致连锁反应,所有的龙骨实例都会发生同步,这不是我想要的效果。

js
复制代码
getTexture () { 
    return this._textureData 
        && this._textureData.spriteFrame 
        && this._textureData.spriteFrame.getTexture(); 
},
setSpriteFrame(spriteFrame){
    this._textureData.spriteFrame = spriteFrame;
}

因为数据都是从_dragonBonesDataMap取的,这是一个object,因为js弱引用的原因,如果slot._textureData修改了,大家都会变。

方法2: 自定义一个SpriteFrame

既然不能对slot._textureData修改,那么我们只能自定义一个SpriteFrame

所以我这样hack了Engine:
js
复制代码
    _customSpriteFrame: null, // 自定义SpriteFrame
    getTexture () {
        // 优先使用用户设置的纹理
        if(this._customSpriteFrame){
            return this._customSpriteFrame.getTexture();
        }else{
            return this._textureData && this._textureData.spriteFrame && this._textureData.spriteFrame.getTexture();
        }
    },
    setSpriteFrame(spriteFrame){
        this._customSpriteFrame = spriteFrame;
    },

运行效果:

image.png

这样子2个db不互相干扰了,但是发现纹理发生了错位,

如果不修正顶点坐标,就必须要求纹理布局一模一样,很明显我们换肤使用的纹理肯定和源纹理的布局是不同的。

顶点计算

js
复制代码
 realTimeTraverse (armature, parentMat, parentOpacity) {
    let slots = armature._slots;
    let vertices, indices;
    for (let i = 0, l = slots.length; i < l; i++) {
        slot = slots[i];
        vertices = slot._localVertices;// 顶点来源
        // 顶点
        for (let vi = 0, vl = vertices.length; vi < vl;) {
            _x = vertices[vi++]; 
            _y = vertices[vi++];
            vbuf[_vfOffset++] = _x * _m00 + _y * _m04 + _m12; // x
            vbuf[_vfOffset++] = _x * _m01 + _y * _m05 + _m13; // y
            vbuf[_vfOffset++] = vertices[vi++]; // u
            vbuf[_vfOffset++] = vertices[vi++]; // v
            uintbuf[_vfOffset++] = _c; // color
        }
    }
}
js
复制代码
  _updateFrame () {
        this._indices.length = 0;
        let indices = this._indices,
            localVertices = this._localVertices;
        let currentTextureData = this._textureData;
        if (!this._display || this._displayIndex < 0 || !currentTextureData || !currentTextureData.spriteFrame) return;
        let texture = currentTextureData.spriteFrame.getTexture();
        let textureAtlasWidth = texture.width;
        let textureAtlasHeight = texture.height;
        let region = currentTextureData.region;
        const currentVerticesData = (this._deformVertices !== null && this._display === this._meshDisplay) ? this._deformVertices.verticesData : null;
        if (currentVerticesData) {
        }
        else {
            // 因为我这边目前只使用了这个方式,也懒得研究了,就直接复用这部分代码即可,如果使用了其他模式可能会出现其他问题
            let l = region.x / textureAtlasWidth;
            let b = (region.y + region.height) / textureAtlasHeight;
            let r = (region.x + region.width) / textureAtlasWidth;
            let t = region.y / textureAtlasHeight;
            localVertices[vfOffset++] = 0; // 0x
            localVertices[vfOffset++] = 0; // 0y
            localVertices[vfOffset++] = l; // 0u
            localVertices[vfOffset++] = b; // 0v
            localVertices[vfOffset++] = region.width; // 1x
            localVertices[vfOffset++] = 0; // 1y
            localVertices[vfOffset++] = r; // 1u
            localVertices[vfOffset++] = b; // 1v
            localVertices[vfOffset++] = 0; // 2x
            localVertices[vfOffset++] = region.height;; // 2y
            localVertices[vfOffset++] = l; // 2u
            localVertices[vfOffset++] = t; // 2v
            localVertices[vfOffset++] = region.width; // 3x
            localVertices[vfOffset++] = region.height;; // 3y
            localVertices[vfOffset++] = r; // 3u
            localVertices[vfOffset++] = t; // 3v
            indices[0] = 0;
            indices[1] = 1;
            indices[2] = 2;
            indices[3] = 1;
            indices[4] = 3;
            indices[5] = 2;
            localVertices.length = vfOffset;
            indices.length = 6;
        }
        this._visibleDirty = true;
        this._blendModeDirty = true;
        this._colorDirty = true;
    },

currentTextureData.region的来源

js
复制代码
var textureData = textureAtlasData.createTexture();
textureData.rotated = ObjectDataParser._getBoolean(rawTexture, dragonBones.DataParser.ROTATED, false);
textureData.name = ObjectDataParser._getString(rawTexture, dragonBones.DataParser.NAME, "");
textureData.region.x = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.X, 0.0);
textureData.region.y = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.Y, 0.0);
textureData.region.width = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.WIDTH, 0.0);
textureData.region.height = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.HEIGHT, 0.0);

整体的对应逻辑关系如下图,很直观的能看到就是龙骨配置文件里面的数据

image.png

知道了原来的计算数据来源,我们直接套用即可

js
复制代码
setSpriteFrame(spriteFrame){
    this._customSpriteFrame = spriteFrame;
    let texture = spriteFrame.getTexture();
    let textureAtlasWidth = texture.width;
    let textureAtlasHeight = texture.height;
    let region = spriteFrame.getRect(); // 这里
    // ...
},

至此,这种方式跑通了,也实现了最基础的换肤

后续问题

slot.display = null

image.png

龙骨在制作的时候,制作人员为了达到隐藏效果,直接把显示资源置空了,等到后续需要显示时,再切换显示,这就导致了slot.display=null的问题。

调试代码发现slot.display来自slot.displayList,直接修改slot.displayList即可。

顶点被重置

龙骨在播放一遍后,第二遍播放动画纹理又错位了

image.png

我首先想到的就是顶点出问题了,排查发现,顶点的确是被重置了。

image.png

相关的堆栈逻辑:

image.png

也不想改Engine了,既然他被重置了,那我在提交纹理时,再计算一次就行了,虽然可能有那么一丢丢的性能问题,但是这样不用改Engine,小游戏是可以复用微信内置的cocos引擎代码的,加快游戏加载。

js
复制代码
const slot_getTexture = dragonBones.CCSlot.prototype.getTexture;
dragonBones.CCSlot.prototype.getTexture = function () {
  if (this._customSpriteFrame) {
    this.setSpriteFrame(this._customSpriteFrame);
    return this._customSpriteFrame.getTexture();
  } else {
    return slot_getTexture.call(this);
  }
};

最终效果

image.png

目录
相关文章
|
6月前
|
机器学习/深度学习
阿里Animate Anyone:让任何静态图像动起来
【2月更文挑战第17天】阿里Animate Anyone:让任何静态图像动起来
714 3
阿里Animate Anyone:让任何静态图像动起来
|
4月前
|
前端开发
ElementPlus卡片如何能够一行呈四,黑马UI前端布局视频资料,element样式具体的细节无法修改,F12找到那个位置,可能在其他组件写了错误,找到那个位置,围绕着位置解决问题最快了,卡片下边
ElementPlus卡片如何能够一行呈四,黑马UI前端布局视频资料,element样式具体的细节无法修改,F12找到那个位置,可能在其他组件写了错误,找到那个位置,围绕着位置解决问题最快了,卡片下边
|
移动开发 前端开发 weex
nvue实现高性能接近原生瀑布流列表
nvue实现高性能接近原生瀑布流列表
|
JavaScript 前端开发
javascript封装函数:解决win10缩放和布局推荐125%网页无法自适应的解决方案
javascript封装函数:解决win10缩放和布局推荐125%网页无法自适应的解决方案
201 0
|
数据可视化 前端开发
【React工作记录四十二】获取页面的可视化高度和宽度
【React工作记录四十二】获取页面的可视化高度和宽度
308 0
|
数据可视化 前端开发
#yyds干货盘点# 【React工作记录四十二】获取页面的可视化高度和宽度
#yyds干货盘点# 【React工作记录四十二】获取页面的可视化高度和宽度
215 0
#yyds干货盘点# 【React工作记录四十二】获取页面的可视化高度和宽度
|
vr&ar 图形学
【Unity3D 灵巧小知识点】 ☀️ | 使用代码控制 Image图片层级渲染 顺序
Unity 小科普 老规矩,先介绍一下 Unity 的科普小知识: Unity是 实时3D互动内容创作和运营平台 。 包括游戏开发、r美术、建筑、汽车设计、影视在内的所有创作者,借助 Unity 将创意变成现实。 Unity 平台提供一整套完善的软件解决方案,可用于创作、运营和变现任何实时互动的2D和3D内容,支持平台包括手机、平板电脑、PC、游戏主机、增强现实和虚拟现实设备。
【Unity3D 灵巧小知识点】 ☀️ | 使用代码控制 Image图片层级渲染 顺序
第二十七章:自定义渲染器(六)
有趣的是,Android SeekBar小部件具有与Steps属性等效的功能,但不等同于Minimum和Maximum属性! 这怎么可能? SeekBar实际上定义了一个名为Max的整数属性,SeekBar的Progress属性始终是一个从0到Max的整数。
763 0
|
Windows
第二十七章:自定义渲染器(五)
渲染器和事件(1) 大多数Xamarin.Forms元素都是交互式的。他们通过触发事件来响应用户输入。如果在Xamarin.Forms自定义元素中实现事件,则可能还需要在呈现器中为本机控件触发的相应事件定义事件处理程序。
741 0
|
前端开发 Android开发 iOS开发
第二十七章:自定义渲染器(四)
渲染器和属性(2) 现在,对于iOS,EllipseUIView类是存在的,可以使用EllipseUIView作为本机控件来编写EllipseViewRenderer。 从结构上讲,这个类几乎与Windows渲染器相同: using System.
575 0