cocos creator spine 换装 ( js engine )

从engine角度思考spine换肤

从webgl的角度我们思考下,engine对接spine,最终都是要将顶点数据提交到webgl,所以只要顺着提交顶点的逻辑反推,这件事就有迹可循。

之前的文章,我们了解到cocos creator会将提交的顶点数据放到一个很大的buffer,而在文档中也提到fillBuffers这个关键函数,其实当你对engine的renderFlow熟悉后,也能够想出来fillBuffers的函数在整个渲染架构中的位置。

js 复制代码
_proto._render = function (node) {
    let comp = node._renderComponent;
    comp._checkBacth(_batcher, node._cullingMask);
    comp._assembler.fillBuffers(comp, _batcher);
    this._next._func(node);
};

creator spine 渲染剖析

回到spine,我们要理解首先要找的就是Assembler

spine-asembler.js 主要的渲染逻辑都在这里面,阅读顺序可以根据数字标号看

js 复制代码
ModelBatcher.prototype = {
     getBuffer (type, vertextFormat) {
        let key = type + vertextFormat.getHash();
        let buffer = _buffers[key];
        if (!buffer) {
            if (type === 'mesh') {
                buffer = new MeshBuffer(this, vertextFormat);
            } else if (type === 'spine') {
                buffer = new SpineBuffer(this, vertextFormat);
            }
            _buffers[key] = buffer;
        }
        return buffer;
    }
}
class SpineAssembler{
    fillBuffers (comp, renderer) {
        // 3. 成员变量_buffer来自那个大的buffer,相关的逻辑可以往上看,这里不再解释
        _buffer = renderer.getBuffer('spine', _vertexFormat);
    }
    realTimeTraverser(){
        // 遍历所有的骨骼
        for (let slotIdx = 0, slotCount = locSkeleton.drawOrder.length; slotIdx < slotCount; slotIdx++) {
            slot = locSkeleton.drawOrder[slotIdx];
            // 6. Attachment来自slot的成员变量,slot也提供了相关的接口
            attachment = slot.getAttachment();

            isRegion = attachment instanceof spine.RegionAttachment;
            isMesh = attachment instanceof spine.MeshAttachment;
            isClip = attachment instanceof spine.ClippingAttachment;
            // 7. 每一帧都会设置纹理,修改纹理可以从region下手,数据来源也是Attachment
            material = _getSlotMaterial(attachment.region.texture._texture, slot.data.blendMode);
            // 纹理不同,主动提交一次
            if (_mustFlush || material.getHash() !== _renderer.material.getHash()) {
                _mustFlush = false;
                _renderer._flush();
                _renderer.node = _node;
                _renderer.material = material;
            }
            if(isRegion){
            }else if(isMesh){
                 vbuf = _buffer._vData, // 2. vbuff来自成员变量_buffer
                 triangles = attachment.triangles
                // compute vertex and fill x y
                attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, vbuf, _vertexFloatOffset, _perVertexSize);
            }

            // 5. 确认vbuf就是要渲染的buffer后,接下来问题就变成了查找uvs的计算逻辑,这里可以看到是来自Attachment
            uvs = attachment.uvs;
            for (let v = _vertexFloatOffset, n = _vertexFloatOffset + _vertexFloatCount, u = 0; v < n; v += _perVertexSize, u += 2) {
                // 1. 填充vbuf的u、v,换肤的本质就是要改uv,所以就从这里下手,查找vbuf的来源
                vbuf[v + 2] = uvs[u];           // u
                vbuf[v + 3] = uvs[u + 1];       // v
            }
         }
    }
}
function _getSlotMaterial (tex, blendMode) {
    let src, dst;
    // 省略了src, dst的逻辑
    let useModel = !_comp.enableBatch;
    let baseMaterial = _comp._materials[0];
    if (!baseMaterial) return null;

    // The key use to find corresponding material
    let key = tex.getId() + src + dst + _useTint + useModel;
    let materialCache = _comp._materialCache;
    let material = materialCache[key];
    if (!material) {
        if (!materialCache.baseMaterial) {
            material = baseMaterial;
            materialCache.baseMaterial = baseMaterial;
        } else {
            material = cc.MaterialVariant.create(baseMaterial);
        }
        
        material.define('CC_USE_MODEL', useModel);
        material.define('USE_TINT', _useTint);
        // update texture 设置纹理
        material.setProperty('texture', tex);

        // update blend function
        material.setBlend(
            true,
            gfx.BLEND_FUNC_ADD,
            src, dst,
            gfx.BLEND_FUNC_ADD,
            src, dst
        );
        materialCache[key] = material;
    }
    return material;
}

经过以上的分析,可以看出,我们想要换肤,只需要修改slot.attachment即可

js 复制代码
Slot.prototype.getAttachment = function () {
    return this.attachment;
};
Slot.prototype.setAttachment = function (attachment) {
    if (this.attachment == attachment)
        return;
    this.attachment = attachment;
    this.attachmentTime = this.bone.skeleton.time;
    this.deform.length = 0;
};

具体实现过程中,会遇到RegionAttachment/MeshAttachment,相关的代码量也不大,可以大致阅读下,不同的Attachment换肤实现的方式也有差异,都有关于u,v,u2,v2的相关逻辑。

当你对渲染相关的代码非常熟悉后,其实实现换肤就非常容易了,最核心的就是attachment.uvs设置好即可

多实例污染的问题

当有多个实例时,修改其中一个spine的Attachment,会导致所有的实例都发生变化,这当然不是我们想要的,究其原因:

js 复制代码
Slot.prototype.setToSetupPose = function () {
    this.color.setFromColor(this.data.color);
    if (this.darkColor != null)
        this.darkColor.setFromColor(this.data.darkColor);
    if (this.data.attachmentName == null)
        this.attachment = null;
    else {
        this.attachment = null;
        // atttachment是相同的,js的object类型参数传递的弱引用
        this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName));
    }
};

幸好attachment提供了copy函数,这样就能解决多实例的问题,虽然会增加一点内存

js 复制代码
const attachment: sp.spine.Attachment = slot.getAttachment();
const copyAttachment = attachment.copy();
slot.setAttachment(copyAttachment);

copy的实现逻辑,本质是new了一个新的object

js 复制代码
MeshAttachment.prototype.copy = function () {
    if (this.parentMesh != null)
        return this.newLinkedMesh();
    var copy = new MeshAttachment(this.name);
    copy.region = this.region; // 这个还是相等的
    copy.path = this.path;
    copy.color.setFromColor(this.color);
    this.copyTo(copy);
    copy.regionUVs = new Array(this.regionUVs.length);
    spine.Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length);
    copy.uvs = new Array(this.uvs.length);
    spine.Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, this.uvs.length);
    copy.triangles = new Array(this.triangles.length);
    spine.Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length);
    copy.hullLength = this.hullLength;
    if (this.edges != null) {
        copy.edges = new Array(this.edges.length);
        spine.Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length);
    }
    copy.width = this.width;
    copy.height = this.height;
    return copy;
};

设置纹理

attachment.region.texture._texture渲染时提交的纹理数据来自region

js 复制代码
const tex2d: cc.Texture2D;
const skeTexture = new sp.SkeletonTexture({ width: tex2d.width, height: tex2d.height });
skeTexture.setRealTexture(tex2d);

const region = new sp.spine.TextureAtlasRegion();
region.texture = skeTexture;

切换皮肤的纹理时,我们需要注意texture的类型,new sp.SkeletonTexture的参数及其含义可以参考engine的实现

  • skeleton-data.js
js 复制代码
_getTexture: function (line) {
    let names = this.textureNames;
    for (let i = 0; i < names.length; i++) {
        if (names[i] === line) {
            let texture = this.textures[i];
            // 纹理的宽高
            let tex = new sp.SkeletonTexture({ width: texture.width, height: texture.height });
            tex.setRealTexture(texture);
            return tex;
        }
    }
    cc.errorID(7506, line);
    return null;
},

总结

总体来说spine换肤的实现,需要对渲染这块非常了解,才能写的游刃有余。

相关推荐
光影少年19 小时前
webpack打包优化
webpack·掘金·金石计划·前端工程化
光影少年2 天前
Typescript工具类型
前端·typescript·掘金·金石计划
光影少年8 天前
Promise状态和方法都有哪些,以及实现原理
javascript·promise·掘金·金石计划
光影少年8 天前
next.js和nuxt与普通csr区别
nuxt.js·掘金·金石计划·next.js
光影少年8 天前
js异步解决方案以及实现原理
前端·javascript·掘金·金石计划
光影少年11 天前
前端上传切片优化以及实现
前端·javascript·掘金·金石计划
ZTStory14 天前
JS 处理生僻字字符 sm4 加密后 Java 解密字符乱码问题
javascript·掘金·金石计划
光影少年15 天前
webpack打包优化都有哪些
前端·webpack·掘金·金石计划
冯志浩16 天前
Harmony Next - 手势的使用(二)
harmonyos·掘金·金石计划
冯志浩16 天前
Harmony Next - 手势的使用(一)
harmonyos·掘金·金石计划