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

相关推荐
中杯可乐多加冰3 天前
【AI落地应用实战】HivisionIDPhotos AI证件照制作实践指南
人工智能·掘金·金石计划
冯志浩1 个月前
Harmony Next - 多线程技术 TaskPool
harmonyos·掘金·金石计划
宇宙之一粟1 个月前
设计快速并发哈希表
后端·rust·掘金·金石计划
宇宙之一粟1 个月前
【译】Go 迭代器的乐趣
后端·go·掘金·金石计划
雨绸缪1 个月前
ABAP 的 “小技巧 ”和 “陷阱 ”以及新语法
后端·代码规范·掘金·金石计划
冯志浩2 个月前
Harmony NEXT:如何给数据库添加自定义分词
harmonyos·掘金·金石计划
中杯可乐多加冰3 个月前
【AI落地应用实战】DAMODEL深度学习平台部署+本地调用ChatGLM-6B解决方案
人工智能·掘金·金石计划
中杯可乐多加冰3 个月前
Amazon Bedrock +Amazon Step Functions实现链式提示(Prompt Chaining)
人工智能·掘金·金石计划
阿李贝斯4 个月前
el-select海量数据渲染-分页解决方案
前端·javascript·掘金·金石计划