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换肤的实现,需要对渲染这块非常了解,才能写的游刃有余。

相关推荐
做前端的娜娜子1 天前
什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数
javascript·面试·掘金·金石计划
做前端的娜娜子3 天前
JavaScript 闭包
前端·javascript·掘金·金石计划
做前端的娜娜子4 天前
移动端上拉加载与下拉刷新实现方案
前端·面试·掘金·金石计划
做前端的娜娜子8 天前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
做前端的娜娜子12 天前
如何实现网页加载进度条?
性能优化·掘金·金石计划
做前端的娜娜子13 天前
大文件上传
掘金·金石计划
做前端的娜娜子13 天前
React 性能优化
掘金·金石计划
做前端的娜娜子14 天前
Vue的响应式原理
掘金·金石计划
做前端的娜娜子14 天前
Web缓存策略有哪些?
掘金·金石计划
光影少年23 天前
RN 原生动画 Animated 用法、动画性能优化
react native·react.js·掘金·金石计划