CcocosCreator Spine利用外部纹理进行局部换装

CcocosCreator Spine利用外部纹理进行局部换装
最近由于项目的需要用到了Spine,需求是用同一套Spine骨骼数据进行局部换装拼接成新的角色。 扒了两遍cc底层代码后,实现了其功能。下面让我们了解下其换装原理。

1.换装原理

  • Spine结构: skeleton->bones->slots->attachmnet
  • 纹理替换原理: 获取当前需要更换attachmnet的slot数据(包括 index attachment), 对solt下region进行Texture2D纹理替换并。
    在Web中,region中的texture是通过sp.SkeletonTexture类进行纹理渲染的;而native是通过jsb-spine-skeleton对部分Web中SpineSkeletonData、SkeletonAnimation等类的函数融合原生c++代码进行重载。
    节选各平台渲染贴图的方法如下:
    • 在Web中,底层是把region关联到了SkeletonTexture,SkeletonTexture包含了纹理信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
         // engine/cocos/spine/assembler/simple.ts
      function realTimeTraverse (worldMat?: Mat4) {
      // ....
      for (let slotIdx = 0, slotCount = locSkeleton.drawOrder.length; slotIdx < slotCount; slotIdx++) {
      // ....
      const texture = ((attachment as any).region.texture as SkeletonTexture).getRealTexture();
      material = _getSlotMaterial(slot.data.blendMode);
      if (!material) {
      clipper.clipEndWithSlot(slot);
      continue;
      }

      if (!_currentMaterial) _currentMaterial = material;
      if (!_buffer?.renderData.material) _buffer!.renderData.material = _currentMaterial;

      if (_mustFlush || material.hash !== _currentMaterial.hash || (texture && _currentTexture !== texture)) {
      _mustFlush = false;

      _buffer = _comp!.requestMeshRenderData(_perVertexSize);
      _currentMaterial = material;
      _currentTexture = texture;
      _buffer.texture = texture!;
      _buffer.renderData.material = _currentMaterial;
      }
      // ....
      }
      }

    • Native 分为jsb、c++两部分, 底层主要是通过中间件middleware.Texture2D的索引找到js里面对应Texture2D纹理进行渲染的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      // engine/platforms/native/engine/jsb-spine-skeleton.js
      let skeletonDataProto = cc.internal.SpineSkeletonData.prototype;
      skeletonDataProto.recordTexture = function (texture) {
      let index = _gTextureIdx;
      let texKey = _textureKeyMap[index] = {key:index};
      _textureMap.set(texKey, texture);
      _gTextureIdx++;
      return index;
      };

      skeletonDataProto.getTextureByIndex = function (textureIdx) {
      let texKey = _textureKeyMap[textureIdx];
      if (!texKey) return;
      return _textureMap.get(texKey);
      };
      let skeleton = cc.internal.SpineSkeleton.prototype;
      skeleton._render = function (ui) {
      // ...
      let renderInfoMgr = middleware.renderInfoMgr;
      let renderInfo = renderInfoMgr.renderInfo;

      let materialIdx = 0, realTextureIndex, realTexture;
      // verify render border
      let border = renderInfo[renderInfoOffset + materialIdx++];
      if (border !== 0xffffffff) return;

      let matLen = renderInfo[renderInfoOffset + materialIdx++];
      let useTint = this.useTint || this.isAnimationCached();
      let vfmt = useTint ? middleware.vfmtPosUvTwoColor : middleware.vfmtPosUvColor;

      _tempVfmt = vfmt;

      if (matLen == 0) return;

      for (let index = 0; index < matLen; index++) {
      realTextureIndex = renderInfo[renderInfoOffset + materialIdx++];
      realTexture = this.skeletonData.getTextureByIndex(realTextureIndex);
      if (!realTexture) return;

      // SpineMaterialType.TWO_COLORED 1
      // SpineMaterialType.COLORED_TEXTURED 0
      //HACK
      const mat = this.material;
      // cache material
      this.material = this.getMaterialForBlendAndTint(
      renderInfo[renderInfoOffset + materialIdx++],
      renderInfo[renderInfoOffset + materialIdx++],
      useTint ? 1 : 0);

      _tempBufferIndex = renderInfo[renderInfoOffset + materialIdx++];
      _tempIndicesOffset = renderInfo[renderInfoOffset + materialIdx++];
      _tempIndicesCount = renderInfo[renderInfoOffset + materialIdx++];

      if (middleware.indicesStart != _tempIndicesOffset ||
      middleware.preRenderBufferIndex != _tempBufferIndex ||
      middleware.preRenderBufferType != _tempVfmt) {
      ui.autoMergeBatches(middleware.preRenderComponent);
      middleware.resetIndicesStart = true;
      } else {
      middleware.resetIndicesStart = false;
      }

      ui.commitComp(this, realTexture, this._assembler, null);
      this.material = mat;
      }
      }

2.换装实现:分为web和native两部分

  • ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    export function ReplaceSpineRegion(spineComp: sp.Skeleton, soltName: string, texture: Texture2D) {
    if (JSB) {
    // @ts-ignore
    let textureIdx = spineComp.skeletonData.recordTexture(texture);
    // @ts-ignore
    let spTex = new middleware.Texture2D();
    spTex.setRealTextureIndex(textureIdx);
    spTex.setPixelsWide(texture.width);
    spTex.setPixelsHigh(texture.height);
    // @ts-ignore
    spineComp._nativeSkeleton.replaceRegion(soltName, spTex);
    } else {
    const solt = spineComp.findSlot(soltName);
    let attachment = solt.getAttachment();
    let skeletonTexture = new sp.SkeletonTexture({
    width: texture.width,
    height: texture.height
    } as ImageBitmap);
    skeletonTexture.setRealTexture(texture);
    let region = attachment.region;
    region.texture = skeletonTexture;
    region.width = texture.width;
    region.height = texture.height;
    region.originalWidth = texture.width;
    region.originalHeight = texture.height;
    region.u = 0;
    region.v = 0;
    region.u2 = 1;
    region.v2 = 1;
    region.renderObject = region;
    // attachment.scaleX = 1;
    // attachment.scaleY = 1;
    attachment.width = texture.width;
    attachment.height = texture.height;

    // @ts-ignore
    if (attachment instanceof sp.spine.MeshAttachment) {
    attachment.updateUVs();
    } else {
    attachment.setRegion(region);
    attachment.updateOffset();
    }
    spineComp.invalidAnimationCache();
    }
    }
  • native

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    void SkeletonRenderer::replaceRegion(const std::string &slotName, cc::middleware::Texture2D *texture) {

    Slot *slot = _skeleton->findSlot(slotName.c_str());
    RegionAttachment *attachment = (RegionAttachment *)slot->getAttachment();

    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);

    AttachmentVertices *attachV = (AttachmentVertices *)attachment->getRendererObject();
    if (attachV->_texture == texture) {
    return;
    }

    // jsb把中间件middleware.Texture2D的索引放到一个私有的map中管理,当SkeletonData重置或者销毁时把外部换装的的索引关联从map中清除
    SkeletonDataMgr::getInstance()->addSkeletonDataInfoTexturesIndex(_uuid, texture->getRealTextureIndex());

    CC_SAFE_RELEASE(attachV->_texture);
    attachV->_texture = texture;
    CC_SAFE_RETAIN(texture);

    V2F_T2F_C4F *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);
    }

    void SkeletonDataMgr::addSkeletonDataInfoTexturesIndex(const std::string &uuid, int index){
    auto dataIt = _dataMap.find(uuid);
    if (dataIt == _dataMap.end()) {
    return ;
    }
    dataIt->second->texturesIndex.push_back(index);
    }

    修改了c++,要想给js用,必须将它绑定到js,用python执行engine-native\tools\bindings-generator\generator.py即可,当然你懒得配环境,直接手动绑定也行。 代码如下
    c++绑定js代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // engine-native/cocos/bindings/auto/jsb_spine_auto.cpp
    static bool js_spine_SkeletonRenderer_replaceRegion(se::State& s)
    {
    auto* cobj = SE_THIS_OBJECT<spine::SkeletonRenderer>(s);
    SE_PRECONDITION2(cobj, false, "js_spine_SkeletonRenderer_replaceRegion : Invalid Native Object");

    const auto &args = s.args();
    int argc = (int)args.size();
    if (argc != 2) {
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1);
    return false;
    }
    bool ok = false;
    std::string slotName;
    ok = seval_to_std_string(args[0], &slotName);
    SE_PRECONDITION2(ok, false, "js_spine_SkeletonRenderer_replaceRegion: Invalid slotName!");

    cc::middleware::Texture2D* texture;
    ok = seval_to_native_ptr(args[1], &texture);
    SE_PRECONDITION2(ok, false, "js_spine_SkeletonRenderer_replaceRegion: Invalid texture!");

    cobj->replaceRegion(slotName, texture);

    return true;
    }
    SE_BIND_FUNC(js_spine_SkeletonRenderer_replaceRegion)


    bool js_register_spine_SkeletonRenderer(se::Object* obj) // NOLINT(readability-identifier-naming)
    {
    auto* cls = se::Class::create("SkeletonRenderer", obj, nullptr, _SE(js_spine_SkeletonRenderer_constructor));

    // ...
    cls->defineFunction("replaceRegion", _SE(js_spine_SkeletonRenderer_replaceRegion));
    cls->defineFinalizeFunction(_SE(js_spine_SkeletonRenderer_finalize));
    cls->install();
    JSBClassType::registerClass<spine::SkeletonRenderer>(cls);

    __jsb_spine_SkeletonRenderer_proto = cls->getProto();
    __jsb_spine_SkeletonRenderer_class = cls;

    se::ScriptEngine::getInstance()->clearException();
    return true;
    }
    1
    2
    3
    4
    // engine-native/cocos/bindings/auto/jsb_spine_auto.h
    JSB_REGISTER_OBJECT_TYPE(spine::SkeletonRenderer);
    // 添加下面一行
    SE_DECLARE_FUNC(js_spine_SkeletonRenderer_replaceRegion);