本教程引擎源代码基于 cocos creator v2.4.10
从engine角度思考spine换肤
从webgl的角度我们思考下,engine对接spine,最终都是要将顶点数据提交到webgl,所以只要顺着提交顶点的逻辑反推,这件事就有迹可循。
之前的文章,我们了解到cocos creator会将提交的顶点数据放到一个很大的buffer
,而在官方文档自定义渲染中也提到fillBuffers
这个关键函数,其实当你对engine的render-flow
熟悉后,也能够想出来fillBuffers
的函数在整个渲染架构中的位置。
_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 主要的渲染逻辑都在这里面,阅读顺序可以根据数字标号看
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
即可,具体实现过程中,会遇到RegionAttachment
/MeshAttachment
,相关的代码量也不大,可以大致阅读下,不同的Attachment换肤实现的方式也有差异,都有关于u,v,u2,v2
的相关逻辑。
当你对渲染相关的代码非常熟悉后,其实实现换肤就非常容易了,最核心的就是attachment.uvs
设置好即可
对应的Attachment分析图
多实例污染的问题
当有多个实例时,修改其中一个spine的Attachment,会导致所有的实例都发生变化,这当然不是我们想要的,究其原因:
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是从缓存中获取的,也就是大家用的都是一个object
// js的object类型参数传递的弱引用
this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName));
}
};
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;
};
幸好attachment
提供了copy
函数,这样就能解决多实例的问题,虽然会增加一点内存
const attachment: sp.spine.Attachment = slot.getAttachment();
const copyAttachment = attachment.copy();
slot.setAttachment(copyAttachment);
copy
的实现逻辑,本质是new
了一个新的object
,需要注意的是,RegionAttachment
/MeshAttachment
的copy
实现是不同的,下边以MeshAttachment
举例
MeshAttachment.prototype.copy = function () {
if (this.parentMesh != null)
return this.newLinkedMesh();
var copy = new MeshAttachment(this.name);
// 注意这个region,所有的attachment用的也是同一个,同样也会发生多实例污染的问题
// 解决办法也是 new
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
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
_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换肤的实现,需要对 creator spine 渲染逻辑非常了解,才能写的游刃有余。
个人感觉,总体难度一般,并没有牵扯矩阵相关的知识,都是纯逻辑,论坛中也有现成可用的代码可以参考,但是存在多实例、MeshAttachment有bug的问题,当然如果移植过来没有问题最好,一旦出现问题,如果不熟悉底层根本无法下手。
实现过程中,我们需要时刻以提交的顶点数据
作为基准,就很容易的想到怎么实现功能,有spine相关问题的,欢迎留言讨论,一起学习一起进步。