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
45export 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
46void 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);